HTTP-сервер на Java: пишем свой сервер и учебный чат на HttpServer

HTTP-сервер на Java: пишем свой сервер и учебный чат на HttpServer Полезное

HTTP-сервер — программа, которая принимает TCP-соединения, разбирает по ним HTTP-запросы и отправляет ответы. В Java это доступно на трех уровнях: сырой Socket, встроенный com.sun.net.httpserver.HttpServer и сервлет-контейнер под фреймворком. Дальше — рабочий сервер на первых двух уровнях и учебный чат поверх HttpServer, с проговоренным результатом каждого запуска.

Три уровня HTTP-сервера в Java: не путать термины

Эти понятия часто смешивают, хотя они решают разные задачи:

  • Сокет (java.net.ServerSocket) — только транспорт, поток байт по TCP. HTTP разбираете сами: строку запроса, заголовки, тело.
  • HttpServer (com.sun.net.httpserver.HttpServer) — минимальная HTTP-обвязка из состава JDK. Разбирает запрос сам, роутинг — по префиксу пути (createContext).
  • Сервлет (HttpServlet) — контракт для контейнера вроде Tomcat: жизненный цикл, сессии, фильтры.
  • Фреймворк (Spring Boot и аналоги) — надстройка над контейнером или Netty: аннотации, DI, готовые интеграции.
Уровень Что дает Что реализуете сами Пример
Socket TCP-поток Разбор HTTP, keep-alive new ServerSocket(8002)
HttpServer Разбор HTTP, роутинг Логику в обработчике HttpServer.create(...)
Servlet + контейнер Сессии, фильтры Класс-обработчик HttpServlet.doGet()
Spring Boot Аннотации, DI Контроллеры @RestController

Дальше — первые два уровня на коде: они в составе JDK и лучше всего показывают, что делает «сервер» под капотом.

Минимальный сервер на HttpServer

HttpServer — часть модуля jdk.httpserver, доступен в любом JDK без Maven и Gradle. Полный рабочий пример:

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

public class SimpleHttpServer {
    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8001), 0);
        server.createContext("/hello", new HelloHandler());
        server.setExecutor(Executors.newFixedThreadPool(10));
        server.start();
        System.out.println("Сервер запущен: http://localhost:8001/hello");
    }

    static class HelloHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            byte[] body = "Привет от HttpServer!".getBytes(StandardCharsets.UTF_8);
            exchange.sendResponseHeaders(200, body.length);
            try (OutputStream os = exchange.getResponseBody()) {
                os.write(body);
            }
        }
    }
}

Запуск без сборщика: javac SimpleHttpServer.java, затем java SimpleHttpServer. Запрос curl http://localhost:8001/hello вернет тело Привет от HttpServer! с кодом 200.

Второй аргумент HttpServer.create(addr, 0) — размер очереди ожидающих соединений (backlog); 0 значит «системное значение по умолчанию», а не «без очереди». createContext направляет запросы по префиксу пути к обработчику, а setExecutor задает пул потоков — без него сервер обрабатывает запросы по очереди в один поток. В handle() тело пишется в OutputStream, который обязательно закрывается — иначе клиент не получит ответ целиком.

Тот же сервер на сыром Socket

Чтобы увидеть, что именно берет на себя HttpServer, вот эквивалент на голом ServerSocket, вручную собирающий HTTP-ответ:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class SimpleSocketServer {
    public static void main(String[] args) throws IOException {
        try (ServerSocket serverSocket = new ServerSocket(8002)) {
            System.out.println("Сокет-сервер слушает порт 8002");
            while (true) {
                Socket client = serverSocket.accept();
                handle(client);
            }
        }
    }

    private static void handle(Socket client) throws IOException {
        try (client;
             BufferedReader in = new BufferedReader(
                     new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
             OutputStream out = client.getOutputStream()) {
            String requestLine = in.readLine();
            System.out.println("Запрос: " + requestLine);
            String body = "Привет от сырого сокета!";
            byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
            String response = "HTTP/1.1 200 OK\r\n"
                    + "Content-Type: text/plain; charset=utf-8\r\n"
                    + "Content-Length: " + bodyBytes.length + "\r\n"
                    + "\r\n" + body;
            out.write(response.getBytes(StandardCharsets.UTF_8));
        }
    }
}

Запуск: javac SimpleSocketServer.java && java SimpleSocketServer. Запрос curl http://localhost:8002/ выведет Привет от сырого сокета!, в консоли появится Запрос: GET / HTTP/1.1.

Граница: код читает только первую строку запроса и не разбирает заголовки и тело — для POST пришлось бы вручную вычитывать Content-Length. Именно эту рутину и берет на себя HttpServer.

Учебный чат поверх HttpServer

Чат — тот же HTTP-сервер с двумя обработчиками: прием сообщения и отдача истории, без WebSocket:

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;

public class ChatServer {
    private static final List<String> messages = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8003), 0);
        server.createContext("/chat/send", ChatServer::handleSend);
        server.createContext("/chat/history", ChatServer::handleHistory);
        server.setExecutor(Executors.newFixedThreadPool(10));
        server.start();
        System.out.println("Чат-сервер: http://localhost:8003");
    }

    private static void handleSend(HttpExchange exchange) throws IOException {
        if (!"POST".equals(exchange.getRequestMethod())) {
            exchange.sendResponseHeaders(405, -1);
            return;
        }
        String text = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
        messages.add(text);
        respond(exchange, "ok");
    }

    private static void handleHistory(HttpExchange exchange) throws IOException {
        respond(exchange, String.join("\n", messages));
    }

    private static void respond(HttpExchange exchange, String text) throws IOException {
        byte[] body = text.getBytes(StandardCharsets.UTF_8);
        exchange.sendResponseHeaders(200, body.length);
        try (OutputStream os = exchange.getResponseBody()) {
            os.write(body);
        }
    }
}

Проверка: curl -X POST --data "Привет всем" http://localhost:8003/chat/send вернет ok, а curl http://localhost:8003/chat/history после этого вернет Привет всем. Это модель с опросом (polling): клиент сам периодически запрашивает /chat/history, сервер не «толкает» сообщения сам. Мгновенную доставку без опроса дает WebSocket — отдельный протокол, здесь не разбираю.

Где проходит граница: почему это не прод-сервер

HttpServer и голый Socket подходят для инструментов, тестов и учебных задач, но не для прод-нагрузки: без setExecutor обработка идет в один поток, нет TLS из коробки (нужен HttpsServer с SSLContext), нет таймаутов и лимитов на тело запроса, а история чата живет только в памяти процесса без аутентификации.

Решается это контейнером (Tomcat, Jetty) или фреймворком со встроенным сервером (Spring Boot), обычно за reverse-proxy вроде nginx, который берет TLS и часть лимитов на себя.

Выводы

  • HTTP-сервер в Java существует на трех уровнях: сырой Socket, встроенный HttpServer из JDK, сервлет-контейнер/фреймворк — у каждого своя зона ответственности.
  • HttpServer разбирает HTTP за вас без внешних зависимостей, но без явного setExecutor работает в один поток.
  • Учебный чат — два HTTP-обработчика (прием и выдача истории) поверх HttpServer, без WebSocket, через опрос, а не мгновенную доставку.
  • Ни Socket-, ни HttpServer-вариант не готовы к продакшену: нет TLS, лимитов и персистентности из коробки.

Где применяется / связь с практикой

Освойте тему на практике

Понимание уровней HTTP-сервера — стандартный вход в серверную разработку на Java, полезный и перед Spring Boot, и перед Netty. Пройти путь от сокетов и HttpServer до полноценного веб-приложения системно, с разбором сервлетов, Spring и многопоточности, помогает курс Java Backend Developer. Сориентироваться, что важно на старте, помогают бесплатные открытые уроки Otus.

FAQ

Можно ли ставить com.sun.net.httpserver.HttpServer в продакшн?
Не рекомендуется: нет TLS и обработки нагрузки из коробки, как у Tomcat, Jetty или Netty. Годится для локальных инструментов, тестов и обучения.

Нужны ли Maven или Gradle для примеров из статьи?
Нет, HttpServer входит в стандартный JDK (модуль jdk.httpserver), достаточно javac и java.

Как добавить HTTPS к HttpServer?
Через класс HttpsServer из того же пакета с настроенным SSLContext под ваш сертификат. Отдельная тема, здесь не разбирается.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)