Java ServerSocket
Принимайте входящие TCP-соединения в Java с помощью ServerSocket и создайте простой сервер.
Клиентская сторона TCP-соединения — это Socket. Серверная сторона — java.net.ServerSocket: она привязывается к порту, ожидает входящих соединений и возвращает обычный Socket для каждого подключившегося клиента. Один ServerSocket принимает множество клиентов; каждый вызов accept() возвращает отдельное соединение.
В этой главе рассматривается привязка к порту и принятие соединений, обслуживание нескольких клиентов одновременно, роль очереди соединений и жизненный цикл долго работающего сервера.
Привязка, принятие, обслуживание
ServerSocket server = new ServerSocket(8080); // bind to port 8080
while (true) {
Socket client = server.accept(); // blocks until a client connects
handle(client); // read/write the client's streams
}new ServerSocket(port)привязывается и начинает прослушивание. Используйте0, чтобы позволить ОС выбрать свободный порт (затем получите его черезgetLocalPort()).accept()блокируется до подключения клиента, затем возвращаетSocket, представляющий это соединение. Серверный сокет продолжает прослушивать следующие соединения.
Соединение, возвращаемое accept(), — это обычный Socket, идентичный тому, что создаёт клиент, поэтому чтение и запись в его потоки работают точно так же.
По умолчанию accept() блокируется бесконечно. Вызовите server.setSoTimeout(ms) заранее, если хотите, чтобы метод выбрасывал SocketTimeoutException после ожидания — это позволяет потоку сервера проверять флаг «продолжать ли работу?» вместо блокировки на тихом порту.
Параллельное обслуживание клиентов
accept() блокирует выполнение, и каждый клиент может держать соединение открытым, поэтому однопоточный цикл способен обслуживать только одного клиента за раз. Классическое решение — один поток (или задача из пула) на соединение:
ExecutorService pool = Executors.newFixedThreadPool(8);
while (running) {
Socket client = server.accept();
pool.submit(() -> handle(client)); // serve this client on a worker thread
}Цикл принятия соединений остаётся свободным для следующего подключения, пока рабочие потоки обслуживают существующих клиентов. О размере пула и управлении им читайте в разделах Executor framework и пулы потоков. С виртуальными потоками (Java 21+) подход «один поток на соединение» остаётся дешёвым даже при десятках тысяч клиентов, поэтому зачастую можно обойтись без пула и просто запускать каждое соединение в своём виртуальном потоке.
Очередь соединений (backlog)
new ServerSocket(port, backlog) задаёт backlog — количество соединений, которые ОС может поставить в очередь, пока ваш код занят между вызовами accept(). Сверх этого лимита новые соединения отклоняются. Значение по умолчанию обычно равно 50.
Четырёхаргументный конструктор добавляет адрес привязки: new ServerSocket(port, backlog, address). Передача InetAddress.getLoopbackAddress() делает сервер доступным только с той же машины (127.0.0.1) — удобно для локальных сервисов и для примера ниже.
Освобождение порта: SO_REUSEADDR
При остановке сервера его порт может оставаться в состоянии TIME_WAIT в ОС, и новый запуск может завершиться ошибкой «Address already in use». Установка SO_REUSEADDR позволяет новому ServerSocket привязаться к порту, ещё находящемуся в состоянии TIME_WAIT:
ServerSocket server = new ServerSocket(); // unbound
server.setReuseAddress(true);
server.bind(new InetSocketAddress(8080)); // now bind explicitlyПоэтому промышленные серверы обычно создают несвязанный ServerSocket, устанавливают опции, а затем вызывают bind() — вместо передачи порта в конструктор.
Пример: параллельный сервер на петлевом интерфейсе
Эта программа привязывает ServerSocket к петлевому интерфейсу, принимает трёх клиентов в фоновом потоке и обслуживает каждого в пуле потоков — затем запускает трёх клиентов. Каждый рабочий поток приветствует своего клиента и сообщает имя обслуживающего потока, делая параллелизм наглядным.
Что можно извлечь из этого примера:
- Вся работа сервера — это bind →
accept()→ serve, повторяемая снова и снова.accept()блокировался до подключения каждого клиента, а затем возвращал обычныйSocketдля этого конкретного клиента, покаServerSocketпродолжал прослушивать следующих — один слушатель, множество соединений. - Привязка к порту
0позволила ОС выбрать свободный порт, который был получен черезgetLocalPort()и передан клиентам. Третий аргумент конструктора также привязал сервер кgetLoopbackAddress(), так что он слушал только на127.0.0.1— тот же адрес и порт, к которым подключались клиенты. - Каждое принятое соединение передавалось в пул потоков, поэтому цикл принятия никогда не блокировался на медленном клиенте. В ответах указывались разные рабочие потоки (
pool-1-thread-1,-2,-3), наглядно демонстрируя параллелизм на уровне соединений: три клиента обслуживались одновременно, а не поочерёдно. handle()использовал try-with-resources для клиентского сокета (try (client; …)), гарантируя закрытие каждого соединения после обмена данными. Сервер, забывающий закрывать принятые сокеты, быстро исчерпывает файловые дескрипторы, поскольку открывает по одному на каждого клиента.- Завершение работы было явным и упорядоченным: прекращение приёма,
pool.shutdown(), ожидание завершения задач, затемserver.close(). Долгоработающий сервер должен намеренно освобождать прослушиваемый порт, а незавершённые рабочие задачи должны иметь возможность завершиться — такой подход масштабируется от этого демо с тремя клиентами до реального сервера.
Когда использовать ServerSocket
ServerSocket — правильный инструмент, когда вам нужен ориентированный на соединения (TCP) сервер с управлением на уровне байтов и потоков: пользовательский протокол, чат или игровой бэкенд, прокси или учебный проект. Для обмена запросами и ответами по HTTP обычно используют высокоуровневый серверный фреймворк вместо написания цикла принятия соединений вручную. Если вам нужен обмен без установки соединения (UDP) — где нет accept() и каждый пакет самостоятелен — используйте датаграммные сокеты. О фоновых знаниях по адресам, портам и стеку протоколов читайте в введении в сетевое программирование.