W3docs

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 к петлевому интерфейсу, принимает трёх клиентов в фоновом потоке и обслуживает каждого в пуле потоков — затем запускает трёх клиентов. Каждый рабочий поток приветствует своего клиента и сообщает имя обслуживающего потока, делая параллелизм наглядным.

java— editable, runs on the server

Что можно извлечь из этого примера:

  • Вся работа сервера — это 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() и каждый пакет самостоятелен — используйте датаграммные сокеты. О фоновых знаниях по адресам, портам и стеку протоколов читайте в введении в сетевое программирование.

Практика

Практика
Сервер использует однопоточный цикл: 'while (true) { Socket c = server.accept(); handle(c); }', где 'handle' держит соединение открытым на всё время сессии клиента. При нагрузке новые клиенты подключаются, но не получают ответа, пока более ранние не отключатся. В чём причина и каково стандартное решение?
Сервер использует однопоточный цикл: 'while (true) { Socket c = server.accept(); handle(c); }', где 'handle' держит соединение открытым на всё время сессии клиента. При нагрузке новые клиенты подключаются, но не получают ответа, пока более ранние не отключатся. В чём причина и каково стандартное решение?
Was this page helpful?