Посібник із розетки Java

1. Огляд

Термін програмування сокетів стосується написання програм, які виконуються на декількох комп'ютерах, в яких всі пристрої з'єднані між собою за допомогою мережі.

Існує два протоколи зв'язку, які можна використовувати для програмування сокетів: Протокол користувацьких датаграм (UDP) і Протокол управління передачею (TCP) .

Основна відмінність між ними полягає в тому, що UDP без підключення, тобто немає сеансу між клієнтом та сервером, тоді як TCP орієнтований на зв’язок, тобто спочатку між клієнтом та сервером потрібно встановити ексклюзивне зв’язок для здійснення зв’язку.

Цей посібник представляє вступ до програмування сокетів через мережі TCP / IP та демонструє, як писати клієнтські / серверні програми на Java. UDP не є основним протоколом, і як такий може траплятися не часто.

2. Налаштування проекту

Java забезпечує колекцію класів та інтерфейсів, які дбають про низькорівневі деталі зв'язку між клієнтом та сервером.

Вони в основному містяться в пакеті java.net , тому нам потрібно зробити такий імпорт:

import java.net.*;

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

import java.io.*;

Для простоти ми будемо запускати наші клієнтські та серверні програми на одному комп'ютері. Якби ми виконували їх на різних мережевих комп'ютерах, єдине, що змінилося б, це IP-адреса, в цьому випадку ми будемо використовувати localhost на 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()));

Відтепер сервер здатний нескінченно обмінюватися повідомленнями з клієнтом, поки сокет не буде закритий своїми потоками.

Однак у нашому прикладі сервер може надіслати відповідь на привітання лише до того, як закриє з'єднання, це означає, що якщо ми повторно проведемо тест, з'єднання буде відмовлено.

Для того, щоб забезпечити спадкоємність в спілкуванні, ми будемо читати з вхідного потоку всередині під час циклу , і тільки вихід , коли клієнт відправляє запит на завершення, ми побачимо це в дії в наступному розділі.

Для кожного нового клієнта серверу потрібен новий сокет, який повертає прийнятий дзвінок. ServerSocket використовується для продовжувати прослуховувати запити на підключення , а прагне до потреб підключених клієнтів. Ми ще не дозволили цього в нашому першому прикладі.

4.2. Клієнт

Клієнт повинен знати ім'я хосту або IP-адресу машини, на якій запущений сервер, та номер порту, на якому сервер прослуховує.

Щоб зробити запит на підключення, клієнт намагається провести зустріч із сервером на машині та порту сервера:

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

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

The above constructor only creates a new socket when the server has accepted the connection, otherwise, we will get a connection refused exception. When successfully created we can then obtain input and output streams from it to communicate with the server:

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

The input stream of the client is connected to the output stream of the server, just like the input stream of the server is connected to the output stream of the client.

5. Continuous Communication

Our current server blocks until a client connects to it and then blocks again to listen to a message from the client, after the single message, it closes the connection because we have not dealt with continuity.

So it is only helpful in ping requests, but imagine we would like to implement a chat server, continuous back and forth communication between server and client would definitely be required.

We will have to create a while loop to continuously observe the input stream of the server for incoming messages.

Let's create a new server called EchoServer.java whose sole purpose is to echo back whatever messages it receives from clients:

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

Notice that we have added a termination condition where the while loop exits when we receive a period character.

We will start EchoServer using the main method just as we did for the GreetServer. This time, we start it on another port such as 4444 to avoid confusion.

The EchoClient is similar to GreetClient, so we can duplicate the code. We are separating them for clarity.

In a different test class, we shall create a test to show that multiple requests to the EchoServer will be served without the server closing the socket. This is true as long as we are sending requests from the same client.

Dealing with multiple clients is a different case, which we shall see in a subsequent section.

Let's create a setup method to initiate a connection with the server:

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

We will equally create a tearDown method to release all our resources, this is best practice for every case where we use network resources:

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

Let's then test our echo server with a few requests:

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

This is an improvement over the initial example, where we would only communicate once before the server closed our connection; now we send a termination signal to tell the server when we're done with the session.

6. Server With Multiple Clients

Much as the previous example was an improvement over the first one, it is still not that great a solution. A server must have the capacity to service many clients and many requests simultaneously.

Handling multiple clients is what we are going to cover in this section.

Another feature we will see here is that the same client could disconnect and reconnect again, without getting a connection refused exception or a connection reset on the server. Previously we were not able to do this.

This means that our server is going to be more robust and resilient across multiple requests from multiple clients.

How we will do this is to create a new socket for every new client and service that client's requests on a different thread. The number of clients being served simultaneously will equal the number of threads running.

The main thread will be running a while loop as it listens for new connections.

Enough talk, let's create another server called EchoMultiServer.java. Inside it, we will create a handler thread class to manage each client's communications on its socket:

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

Notice that we now call accept inside a while loop. Every time the while loop is executed, it blocks on the accept call until a new client connects, then the handler thread, EchoClientHandler, is created for this client.

What happens inside the thread is what we previously did in the EchoServer where we handled only a single client. So the EchoMultiServer delegates this work to EchoClientHandler so that it can keep listening for more clients in the while loop.

Ми все одно будемо використовувати 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.