Перейти к основному содержимому

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

· 10 мин. чтения

1. Обзор

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

Существует два протокола связи, которые мы можем использовать для программирования сокетов: протокол пользовательских дейтаграмм (UDP) и протокол управления передачей (TCP) .

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

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

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

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

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

import java.net.*;

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

import java.io.*;

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

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

Давайте запачкаем руки самыми простыми примерами с участием клиента и сервера . Это будет приложение для двусторонней связи, в котором клиент приветствует сервер, а сервер отвечает.

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

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

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();
}
}

Теперь запустим сервер. В нашей среде 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);
}

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

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

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

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

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

4.1. Сервер

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

ServerSocket serverSocket = new ServerSocket(6666);

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

Socket clientSocket = serverSocket.accept();

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

Если все в порядке, сервер принимает соединение. После принятия сервер получает новый сокет 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);

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

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

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, используя метод main, точно так же, как мы это делали для GreetServer . На этот раз мы запускаем его на другом порту, например 4444, чтобы избежать путаницы.

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

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

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

Теперь давайте создадим метод настройки для инициации соединения с сервером:

@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);
}

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

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. Заключение

В этой статье мы сосредоточились на введении в программирование сокетов через TCP/IP и написали простое клиент-серверное приложение на Java.

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