Руководство по сокетам Java

Руководство по сокетам Java

1. обзор

Терминsocketprogramming относится к написанию программ, которые выполняются на нескольких компьютерах, в которых все устройства подключены друг к другу с помощью сети.

Есть два протокола связи, которые можно использовать для программирования сокетов:User Datagram Protocol (UDP) and Transfer Control Protocol (TCP).

Основное различие между ними заключается в том, что UDP не использует соединения, то есть между клиентом и сервером нет сеанса, а TCP ориентирован на соединение, то есть для установления связи между клиентом и сервером необходимо сначала установить эксклюзивное соединение.

В этом руководстве представлены сетиan introduction to sockets programming over TCP/IP и показано, как писать клиент-серверные приложения на Java. UDP не является основным протоколом, и поэтому его можно встретить не часто.

2. Настройка проекта

Java предоставляет набор классов и интерфейсов, которые заботятся о низкоуровневых деталях связи между клиентом и сервером.

В основном они содержатся в пакетеjava.net, поэтому нам нужно сделать следующий импорт:

import java.net.*;

Нам также нужен пакетjava.io, который дает нам входные и выходные потоки для записи и чтения при общении:

import java.io.*;

Для простоты мы будем запускать наши клиентские и серверные программы на одном компьютере. Если бы мы выполняли их на разных сетевых компьютерах, единственное, что изменилось бы, - это IP-адрес, в этом случае мы будем использоватьlocalhost на127.0.0.1.

3. Простой пример

Давайте поработаем руками с наибольшим количествомbasic of examples involving a client and a server. Это будет приложение для двусторонней связи, в котором клиент приветствует сервер, а сервер отвечает.

Давайте создадим серверное приложение в классе с именемGreetServer.java со следующим кодом.

Мы включаем методmain и глобальные переменные, чтобы привлечь внимание к тому, как мы будем запускать все серверы в этой статье. В остальных примерах статей мы опускаем этот более повторяющийся код:

public class GreetServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        String greeting = in.readLine();
            if ("hello server".equals(greeting)) {
                out.println("hello client");
            }
            else {
                out.println("unrecognised greeting");
            }
    }

    public void stop() {
        in.close();
        out.close();
        clientSocket.close();
        serverSocket.close();
    }
    public static void main(String[] args) {
        GreetServer server=new GreetServer();
        server.start(6666);
    }
}

Давайте также создадим клиента с именемGreetClient.java с помощью этого кода:

public class GreetClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port) {
        clientSocket = new Socket(ip, port);
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    }

    public String sendMessage(String msg) {
        out.println(msg);
        String resp = in.readLine();
        return resp;
    }

    public void stopConnection() {
        in.close();
        out.close();
        clientSocket.close();
    }
}

Let’s start the server; в вашей среде IDE вы делаете это, просто запуская его как приложение Java.

А теперь давайте отправим приветствие на сервер с помощью модульного теста, который подтверждает, что сервер действительно отправляет приветствие в ответ:

@Test
public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() {
    GreetClient client = new GreetClient();
    client.startConnection("127.0.0.1", 6666);
    String response = client.sendMessage("hello server");
    assertEquals("hello client", response);
}

Не волнуйтесь, если вы не совсем понимаете, что здесь происходит, поскольку этот пример предназначен для того, чтобы дать нам почувствовать, чего ожидать в дальнейшем в статье.

В следующих разделах мы рассмотримsocket communication, используя этот простой пример, и углубимся в детали с другими примерами.

4. Как работают сокеты

Мы будем использовать приведенный выше пример, чтобы пройтись по различным частям этого раздела.

По определениюsocket является одной из конечных точек двустороннего канала связи между двумя программами, работающими на разных компьютерах в сети. Сокет привязан к номеру порта, так что транспортный уровень может идентифицировать приложение, которому предназначены данные для отправки.

4.1. Сервер

Обычно сервер работает на определенном компьютере в сети и имеет сокет, связанный с определенным номером порта. В нашем случае мы используем тот же компьютер, что и клиент, и запускаем сервер на порту6666:

ServerSocket serverSocket = new ServerSocket(6666);

Сервер просто ждет, слушая сокет для клиента, чтобы сделать запрос на подключение. Это происходит на следующем шаге:

Socket clientSocket = serverSocket.accept();

Когда код сервера встречает методaccept, он блокируется, пока клиент не запросит подключение к нему.

Если все идет хорошо, серверacceptsустанавливает соединение. После принятия сервер получает новый сокет,clientSocket, привязанный к тому же локальному порту,6666, а также его удаленная конечная точка устанавливается на адрес и порт клиента.

На этом этапе новый объектSocket устанавливает прямое соединение сервера с клиентом, после чего мы можем получить доступ к потокам вывода и ввода для записи и получения сообщений от клиента и от него соответственно:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

С этого момента сервер может обмениваться сообщениями с клиентом бесконечно, пока сокет не будет закрыт его потоками.

Однако в нашем примере сервер может отправить только приветственный ответ, прежде чем он закроет соединение, это означает, что если мы снова запустим наш тест, соединение будет отклонено.

Чтобы обеспечить непрерывность связи, нам нужно будет читать из входного потока внутри циклаwhile и выходить только тогда, когда клиент отправляет запрос на завершение, мы увидим это в действии в следующем разделе.

Для каждого нового клиента серверу нужен новый сокет, возвращаемый вызовомaccept. serverSocket используется для продолжения прослушивания запросов на соединение, одновременно обслуживая потребности подключенных клиентов. Мы еще не допустили этого в нашем первом примере.

4.2. Клиент

Клиент должен знать имя хоста или IP-адрес компьютера, на котором работает сервер, и номер порта, на котором сервер прослушивает.

Чтобы сделать запрос на соединение, клиент пытается встретиться с сервером на машине и порту сервера:

Socket clientSocket = new Socket("127.0.0.1", 6666);

Клиент также должен идентифицировать себя с сервером, чтобы он привязывался к номеру локального порта, назначенному системой, который он будет использовать во время этого соединения. Мы сами этим не занимаемся.

Вышеупомянутый конструктор создает новый сокет только тогда, когда сервер имеетaccepted соединение, в противном случае мы получим исключение отказа в соединении. После успешного создания мы можем получить от него потоки ввода и вывода для связи с сервером:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

Входной поток клиента подключается к выходному потоку сервера так же, как входной поток сервера подключается к выходному потоку клиента.

5. Непрерывное общение

Наш текущий сервер блокируется до тех пор, пока клиент не подключится к нему, а затем снова блокирует прослушивание сообщения от клиента, после одного сообщения он закрывает соединение, потому что мы не имели дело с непрерывностью.

Так что это полезно только при запросах ping, но представьте, что мы хотели бы реализовать сервер чата, поэтому однозначно потребуется непрерывное взаимодействие между сервером и клиентом.

Нам нужно будет создать цикл while, чтобы непрерывно наблюдать входной поток сервера на наличие входящих сообщений.

Давайте создадим новый сервер с именемEchoServer.java, единственная цель которого - возвращать все сообщения, которые он получает от клиентов:

public class EchoServer {
    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

        String inputLine;
        while ((inputLine = in.readLine()) != null) {
        if (".".equals(inputLine)) {
            out.println("good bye");
            break;
         }
         out.println(inputLine);
    }
}

Обратите внимание, что мы добавили условие завершения, при котором цикл while завершается, когда мы получаем символ точки.

Мы начнемEchoServer, используя основной метод, так же, как мы это делали дляGreetServer. На этот раз мы запускаем его на другом порту, таком как4444, чтобы избежать путаницы.

EchoClient похож наGreetClient, поэтому мы можем продублировать код. Мы разделяем их для ясности.

В другом тестовом классе мы создадим тест, чтобы показать, что несколько запросов кEchoServer будут обслуживаться без закрытия сокета сервером. Это верно до тех пор, пока мы отправляем запросы от одного и того же клиента.

Работа с несколькими клиентами - это другой случай, который мы рассмотрим в следующем разделе.

Давайте создадим методsetup для установления соединения с сервером:

@Before
public void setup() {
    client = new EchoClient();
    client.startConnection("127.0.0.1", 4444);
}

Мы также создадим методtearDown для высвобождения всех наших ресурсов, это лучшая практика для каждого случая, когда мы используем сетевые ресурсы:

@After
public void tearDown() {
    client.stopConnection();
}

Давайте затем протестируем наш эхо-сервер с помощью нескольких запросов:

@Test
public void givenClient_whenServerEchosMessage_thenCorrect() {
    String resp1 = client.sendMessage("hello");
    String resp2 = client.sendMessage("world");
    String resp3 = client.sendMessage("!");
    String resp4 = client.sendMessage(".");

    assertEquals("hello", resp1);
    assertEquals("world", resp2);
    assertEquals("!", resp3);
    assertEquals("good bye", resp4);
}

Это улучшение по сравнению с первоначальным примером, в котором мы общались только один раз, прежде чем сервер закрыл наше соединение; now we send a termination signal to tell the server when we’re done with the session.

6. Сервер с несколькими клиентами

Несмотря на то, что предыдущий пример был улучшением по сравнению с первым, он все еще не так уж и хорош. Сервер должен иметь возможность обслуживать множество клиентов и множество запросов одновременно.

В этом разделе мы рассмотрим работу с несколькими клиентами.

Еще одна особенность, которую мы увидим здесь, заключается в том, что один и тот же клиент может отключиться и снова подключиться, не получая исключение отказа в соединении или сброса соединения на сервере. Ранее мы не могли этого сделать.

Это означает, что наш сервер будет более устойчивым и устойчивым к нескольким запросам от нескольких клиентов.

Мы будем это делать, создавая новый сокет для каждого нового клиента и службы, которые клиент запрашивает в другом потоке. Количество одновременно обслуживаемых клиентов будет равно числу запущенных потоков.

Основной поток будет выполнять цикл while, поскольку он прослушивает новые соединения.

Хватит разговоров, давайте создадим еще один сервер с именемEchoMultiServer.java.. Внутри него мы создадим класс потока обработчика для управления коммуникациями каждого клиента в его сокете:

public class EchoMultiServer {
    private ServerSocket serverSocket;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        while (true)
            new EchoClientHandler(serverSocket.accept()).start();
    }

    public void stop() {
        serverSocket.close();
    }

    private static class EchoClientHandler extends Thread {
        private Socket clientSocket;
        private PrintWriter out;
        private BufferedReader in;

        public EchoClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        public void run() {
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(
              new InputStreamReader(clientSocket.getInputStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                if (".".equals(inputLine)) {
                    out.println("bye");
                    break;
                }
                out.println(inputLine);
            }

            in.close();
            out.close();
            clientSocket.close();
    }
}

Обратите внимание, что теперь мы вызываемaccept внутри циклаwhile. Каждый раз, когда выполняется циклwhile, он блокирует вызовaccept до тех пор, пока не подключится новый клиент, затем для этого клиента создается поток обработчикаEchoClientHandler.

То, что происходит внутри потока, - это то, что мы ранее делали вEchoServer, где мы обрабатывали только одного клиента. Таким образом,EchoMultiServer делегирует эту работуEchoClientHandler, чтобы он мог продолжать прослушивать больше клиентов в циклеwhile.

Мы по-прежнему будем использоватьEchoClient для тестирования сервера, на этот раз мы создадим несколько клиентов, каждый из которых будет отправлять и получать несколько сообщений от сервера.

Давайте запустим наш сервер, используя его основной метод на порту5555.

Для наглядности мы все равно поместим тесты в новый пакет:

@Test
public void givenClient1_whenServerResponds_thenCorrect() {
    EchoClient client1 = new EchoClient();
    client1.startConnection("127.0.0.1", 5555);
    String msg1 = client1.sendMessage("hello");
    String msg2 = client1.sendMessage("world");
    String terminate = client1.sendMessage(".");

    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

@Test
public void givenClient2_whenServerResponds_thenCorrect() {
    EchoClient client2 = new EchoClient();
    client2.startConnection("127.0.0.1", 5555);
    String msg1 = client2.sendMessage("hello");
    String msg2 = client2.sendMessage("world");
    String terminate = client2.sendMessage(".");

    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

Мы можем создать столько тестов, сколько пожелаем, каждый из них порождает нового клиента и сервер будет обслуживать их все.

7. Заключение

В этом руководстве мы сосредоточились наan introduction to sockets programming over TCP/IP и написали простое клиент-серверное приложение на Java.

Полный исходный код статьи, как обычно, можно найти в проектеGitHub.