Создаём HTTP-сервер на Java NIO

от автора

Привет, Хабр!

Сегодня создадим HTTP‑сервер на чистом Java NIO, без всяких Spring Boot, Jetty и прочих фреймворков. Будем разбираться, как работает неблокирующее I/O, что такое Selector, SocketChannel, и как заставить сервер обрабатывать тысячи запросов одновременно без запуска тысяч потоков.

Почему Java NIO, а не обычный ServerSocket?

Если вы писали сетевые приложения в Java, то наверняка использовали ServerSocket и Socket. Но у этого подхода серьёзные проблемы:

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

  • Плохая масштабируемость. Если у нас 10k клиентов, у нас 10k потоков. Это дорого.

  • Блокирующий ввод/вывод. Пока один клиент что‑то читает, другие вынуждены ждать.

Как Java NIO решает эти проблемы?

  • Один поток обрабатывает все соединения благодаря Selector.

  • Нет блокировок — сервер асинхронно читает данные, не простаивая впустую.

  • Гораздо меньше оверхеда на потоки → можно обрабатывать сотни тысяч запросов без краха JVM.

Запускаем сервер

Начнём с создания серверного сокета, который будет слушать порт 8080 и принимать входящие соединения.

import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*;  public class NioHttpServer {     private static final int PORT = 8080;      public static void main(String[] args) throws IOException {         // 1. Открываем неблокирующий серверный сокет         ServerSocketChannel serverSocket = ServerSocketChannel.open();         serverSocket.bind(new InetSocketAddress(PORT));         serverSocket.configureBlocking(false);          // 2. Создаём селектор для обработки событий         Selector selector = Selector.open();         serverSocket.register(selector, SelectionKey.OP_ACCEPT);          System.out.println("Сервер запущен на порту " + PORT);          while (true) {             selector.select(); // Ожидаем события             for (SelectionKey key : selector.selectedKeys()) {                 if (key.isAcceptable()) accept(selector, serverSocket);                 if (key.isReadable()) handleRequest(key);             }             selector.selectedKeys().clear();         }     } }

Открываем ServerSocketChannel, привязываем его к порту 8080. Переключаем в неблокирующий режим configureBlocking(false). Создаём Selector, который будет уведомлять нас о событиях (новое подключение, готовность к чтению).

Запускаем цикл обработки событий:

  1. Если пришёл новый клиент key.isAcceptable() → принимаем соединение.

  2. Если клиент отправил данные key.isReadable() → читаем HTTP‑запрос и отвечаем.

Обрабатываем входящие соединения

Теперь напишем метод accept(), который будет принимать новых клиентов и регистрировать их в Selector.

private static void accept(Selector selector, ServerSocketChannel serverSocket) throws IOException {     SocketChannel client = serverSocket.accept(); // Принимаем подключение     client.configureBlocking(false); // Делаем неблокирующим     client.register(selector, SelectionKey.OP_READ); // Ждём, когда клиент пришлёт данные     System.out.println("Новое соединение: " + client.getRemoteAddress()); }

accept() принимает соединение, но не создаёт новый поток. client.configureBlocking(false) делает клиентский сокет неблокирующим. register(selector, SelectionKey.OP_READ) говорит селектору: «Скажи мне, когда клиент что‑то пришлёт»

Читаем HTTP-запрос и отвечаем

Теперь напишем обработку входящих HTTP‑запросов.

private static void handleRequest(SelectionKey key) throws IOException {     SocketChannel client = (SocketChannel) key.channel();     ByteBuffer buffer = ByteBuffer.allocate(1024);     int bytesRead = client.read(buffer);          if (bytesRead == -1) {         client.close();         return;     }      buffer.flip();     String request = new String(buffer.array(), 0, bytesRead);     System.out.println("Запрос:\n" + request);      // Отправляем простой HTTP-ответ     String response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, Habr!";     ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());     client.write(responseBuffer);     client.close(); // Закрываем соединение }

Читаем данные от клиента в ByteBuffer. Если bytesRead == -1, клиент закрыл соединение — закрываем SocketChannel. Конвертируем ByteBuffer в строку и печатаем запрос в консоль. Формируем HTTP‑ответ (простой 200 OK). Отправляем ответ и закрываем соединение (для простоты).

Запускаем сервер и тестируем

Компилируем и запускаем:

javac NioHttpServer.java java NioHttpServer

Теперь тестируем через браузер или curl:

curl -v http://localhost:8080/

Ожидаемый ответ:

HTTP/1.1 200 OK Content-Length: 13  Hello, Habr!

Сервер работает, не блокирует потоки, может обслуживать тысячи соединений и полностью основан на Java NIO.


Если интересно, в следующей статье разберём, как сделать асинхронный HTTP‑сервер с поддержкой POST и обработкой маршрутов.

Java-разработчикам, желающим углубить знания в устройстве JVM, принципах профилирования и оптимизации приложений в облачной инфраструктуре, рекомендую обратить внимание на онлайн-курс «Java Developer. Advanced».


ссылка на оригинал статьи https://habr.com/ru/articles/889062/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *