Сетевое программирование на Java: от сокетов до NIO и WebSockets

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему сетевое программирование на Java — это суперсила?

Представьте: вы пишете код, и вдруг ваша программа начинает общаться с другой программой на другом конце света. Она отправляет запросы, получает ответы, синхронизирует данные — и всё это за миллисекунды. Это не магия, это сетевое программирование на Java. Если вы хотите создавать не просто локальные утилиты, а настоящие распределённые системы, чаты, онлайн-игры или REST-сервисы — без сетевого программирования никуда.

Java даёт для этого мощный инструментарий: от классических Socket и ServerSocket до высокопроизводительного Java NIO (Non-blocking I/O) и современных WebSockets. В этой статье мы разберём все три подхода, напишем живой код и поймём, как выбрать правильный инструмент для вашей задачи. Погнали!

1. Классические сокеты: TCP-чат «на коленке»

Начнём с фундамента. Сокеты в Java — это точка входа в мир TCP/IP. Сервер создаёт ServerSocket, слушает порт, принимает подключения. Клиент создаёт Socket и подключается к серверу. Всё просто, как дважды два.

Пишем простой эхо-сервер

Сервер принимает сообщение от клиента и отправляет его обратно (эхо). Код минимален, но иллюстрирует всю суть:

import java.io.*;
import java.net.*;

public class EchoServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(12345)) {
            System.out.println("Сервер запущен на порту 12345...");
            Socket clientSocket = serverSocket.accept(); // блокируется до подключения
            System.out.println("Клиент подключён: " + clientSocket.getInetAddress());

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

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println("Получено: " + inputLine);
                out.println("Эхо: " + inputLine);
                if (inputLine.equalsIgnoreCase("exit")) break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

А вот клиент, который отправляет сообщения:

import java.io.*;
import java.net.*;

public class EchoClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost", 12345);
        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
        BufferedReader in = new BufferedReader(
            new InputStreamReader(socket.getInputStream()));
        BufferedReader stdIn = new BufferedReader(
            new InputStreamReader(System.in));

        String userInput;
        System.out.println("Введите сообщение (exit для выхода):");
        while ((userInput = stdIn.readLine()) != null) {
            out.println(userInput);
            System.out.println("Ответ сервера: " + in.readLine());
            if (userInput.equalsIgnoreCase("exit")) break;
        }
        socket.close();
    }
}

Что здесь важно? Метод accept() и readLine() блокируют поток. Это значит, что пока один клиент общается, другие ждут. Решение — многопоточность.

Многопоточный сервер: обрабатываем много клиентов

Для каждого нового подключения создаём отдельный поток. Используем пул потоков ExecutorService, чтобы не плодить тысячи потоков впустую:

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class MultiThreadedServer {
    private static final int PORT = 12345;
    private static final ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws IOException {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("Многопоточный сервер запущен...");
            while (true) {
                Socket clientSocket = serverSocket.accept();
                threadPool.submit(() -> handleClient(clientSocket));
            }
        }
    }

    private static void handleClient(Socket socket) {
        try (socket;
             BufferedReader in = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()));
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {

            String message;
            while ((message = in.readLine()) != null) {
                System.out.println("От " + socket.getInetAddress() + ": " + message);
                out.println("Принято: " + message);
            }
        } catch (IOException e) {
            System.err.println("Ошибка обработки клиента: " + e.getMessage());
        }
    }
}

Такой подход — классика сетевого программирования на Java. Он идеален для небольших проектов, учебных задач или серверов с десятками клиентов. Но если клиентов тысячи — потоки начнут «жрать» память. Тут на сцену выходит NIO.

2. Java NIO: когда скорость и масштаб имеют значение

Java NIO (New I/O) появился в JDK 1.4 и перевернул представление о вводе-выводе. Вместо блокирующих потоков — каналы (SocketChannel), буферы (ByteBuffer) и селекторы (Selector). Один поток может обслуживать тысячи соединений! Как? Селектор «слушает» все каналы и сообщает, когда в каком из них появились данные.

Простой NIO-сервер: один поток — много клиентов

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NioEchoServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(12346));
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("NIO сервер запущен на порту 12346...");

        ByteBuffer buffer = ByteBuffer.allocate(256);

        while (true) {
            selector.select(); // блокируется, пока не появится событие
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();

            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();

                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("Подключён новый клиент: " + client.getRemoteAddress());
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    buffer.clear();
                    int bytesRead = client.read(buffer);
                    if (bytesRead == -1) {
                        client.close();
                        System.out.println("Клиент отключился");
                    } else {
                        buffer.flip();
                        client.write(buffer); // эхо-ответ
                    }
                }
                iterator.remove();
            }
        }
    }
}

Этот код обрабатывает сотни подключений в одном потоке. Нет накладных расходов на создание потоков, нет блокировок. Java NIO — выбор для высоконагруженных систем: игровых серверов, биржевых терминалов, прокси-серверов.

Минус: код сложнее, чем с обычными сокетами. Легко ошибиться с буферами и состоянием канала. Но производительность того стоит.

3. WebSockets: двусторонняя связь для веба

Блоги

Рекомендаций по книгам