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

4a1b71297e7f5da379da21703586f9b9.jpg

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

Сегодня создадим 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».

© Habrahabr.ru