Создаём 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, который будет уведомлять нас о событиях (новое подключение, готовность к чтению).
Запускаем цикл обработки событий:
Если пришёл новый клиент
key.isAcceptable()
→ принимаем соединение.Если клиент отправил данные
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».