Почему сетевое программирование на 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 — выбор для высоконагруженных систем: игровых серверов, биржевых терминалов, прокси-серверов.
Минус: код сложнее, чем с обычными сокетами. Легко ошибиться с буферами и состоянием канала. Но производительность того стоит.