Привет, Хабр!
Сегодня создадим 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».
ссылка на оригинал статьи https://habr.com/ru/articles/889062/
Добавить комментарий