Создание HTTP-сервера на чистом Java: разработка без фреймворков
Для кого эта статья:
- Разработчики, стремящиеся углубить свои знания Java и веб-технологий
- Программисты, заинтересованные в создании HTTP-серверов с нуля
Специалисты, ищущие практический опыт и понимание низкоуровневых механизмов работы веб-приложений
Откройте для себя истинный потенциал Java без лишних оберток! Когда вы строите HTTP-сервер "голыми руками", вы не просто пишете код — вы прикасаетесь к фундаментальным основам веб-технологий. Многие разработчики прячутся за фреймворками, но настоящее мастерство рождается из понимания низкоуровневых механизмов. Готовы заглянуть под капот HTTP и почувствовать себя творцом, а не просто пользователем готовых решений? 🔍 Давайте создадим полноценный HTTP-сервер, используя только стандартный Java SE API.
Хотите углубить свои знания Java и стать разработчиком, который понимает технологии на фундаментальном уровне? Курс Java-разработки от Skypro даст вам не только теоретическую базу, но и практический опыт создания реальных проектов. Наши студенты учатся писать эффективный код без избыточных абстракций, понимая все процессы изнутри — от HTTP-серверов до многопоточных приложений. Превратите свое хобби в профессию под руководством опытных практиков!
Основы архитектуры HTTP-сервера в Java SE API
HTTP-сервер — это фундамент веб-разработки, который принимает запросы от клиентов и отправляет ответы. Большинство разработчиков используют готовые решения вроде Tomcat или Jetty, но Java SE содержит все необходимые инструменты для создания легковесного сервера своими руками.
Стандартная библиотека Java предлагает класс HttpServer из пакета com.sun.net.httpserver, который появился ещё в Java 6. Этот класс предоставляет базовую функциональность для обработки HTTP-запросов без необходимости подключения внешних зависимостей.
Антон Петров, Lead Java Developer
Когда я только начинал карьеру разработчика, я не понимал, почему на собеседованиях часто спрашивают о низкоуровневых аспектах HTTP. «Зачем это знать, если есть Spring?» — думал я. Однажды мой сервис столкнулся с проблемой производительности, и выяснилось, что фреймворк создавал избыточную нагрузку для простых операций. Реализовав критичную часть на чистом Java SE, я увеличил пропускную способность на 40%. С тех пор я всегда стараюсь понимать, что происходит "под капотом" используемых технологий.
Основные компоненты HTTP-сервера на Java SE:
- HttpServer — основной класс, отвечающий за прослушивание порта и распределение запросов
- HttpHandler — интерфейс для обработки входящих запросов
- HttpExchange — объект, содержащий всю информацию о запросе и позволяющий формировать ответ
- HttpContext — контекст, связывающий URL-путь с обработчиком
Минималистичная архитектура HTTP-сервера выглядит следующим образом:
| Компонент | Функция | Аналог в фреймворках |
|---|---|---|
| HttpServer | Запуск сервера, прослушивание порта | ApplicationServer, TomcatServer |
| HttpContext | Маппинг URL на обработчики | DispatcherServlet, Router |
| HttpHandler | Обработка запросов | Controller, RequestHandler |
| HttpExchange | Запрос/ответ | HttpServletRequest/Response |
Простейшая реализация HTTP-сервера может выглядеть так:
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
public class SimpleHttpServer {
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/api", new MyHandler());
server.setExecutor(null); // Используем поток по умолчанию
server.start();
System.out.println("Сервер запущен на порту 8080");
}
static class MyHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
String response = "Hello, World!";
exchange.sendResponseHeaders(200, response.length());
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
}
Этот пример демонстрирует минимальную реализацию HTTP-сервера, который отвечает "Hello, World!" на любой запрос к пути "/api". Давайте рассмотрим более детальную настройку в следующем разделе.

Настройка HttpServer: порты, очереди и потоки
Создание действительно эффективного HTTP-сервера требует глубокого понимания сетевых параметров и управления потоками. Рассмотрим ключевые аспекты конфигурации HttpServer, которые напрямую влияют на производительность и стабильность вашего приложения. 🛠️
Создание и настройка сервера
Базовый шаблон создания сервера выглядит так:
HttpServer server = HttpServer.create(new InetSocketAddress(hostname, port), backlog);
server.setExecutor(executor);
server.start();
Параметры, которые следует учитывать:
- hostname — имя хоста или IP-адрес (используйте "0.0.0.0" для прослушивания всех интерфейсов)
- port — порт, на котором будет работать сервер (обычно 80 для HTTP или 8080 для разработки)
- backlog — максимальное количество входящих соединений в очереди
- executor — пул потоков для обработки запросов
Мария Соколова, Java Architect
На проекте финтех-стартапа мы столкнулись с неожиданной проблемой — наш сервер отлично работал на тестовых нагрузках, но падал при запуске в продакшене. Анализ показал, что мы не настроили параметр backlog и использовали дефолтный Executor. При пиковых нагрузках в 5000 одновременных пользователей сервер просто не справлялся с очередью запросов. После тонкой настройки backlog=100 и внедрения ThreadPoolExecutor с правильно рассчитанными параметрами производительность выросла в 7 раз, а время отклика снизилось с 2-3 секунд до 200-300 мс. Этот случай наглядно показал, что даже простой HTTP-сервер требует глубокого понимания его внутренних механизмов.
Значение backlog особенно важно для высоконагруженных систем. Если оно слишком маленькое, клиенты могут получать ошибки подключения. Если слишком большое — можно столкнуться с исчерпанием ресурсов системы.
Для параметра executor существует несколько вариантов:
| Тип Executor | Преимущества | Недостатки | Рекомендуется для |
|---|---|---|---|
| null (по умолчанию) | Простота, минимум кода | Новый поток для каждого запроса | Тестирования, малых нагрузок |
| Executors.newFixedThreadPool() | Ограничение количества потоков | Неизменяемый размер пула | Стабильные нагрузки |
| Executors.newCachedThreadPool() | Адаптация к нагрузке | Потенциально неограниченный рост потоков | Переменные нагрузки |
| ThreadPoolExecutor (ручная настройка) | Полный контроль над поведением | Сложность настройки | Высоконагруженные системы |
Пример создания оптимизированного HTTP-сервера:
// Создаем пул потоков с оптимальными параметрами
int cores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(cores * 2);
// Настраиваем параметры очереди
threadPoolExecutor.setKeepAliveTime(30, TimeUnit.SECONDS);
threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// Создаем и настраиваем сервер
HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", 8080), 100);
server.setExecutor(threadPoolExecutor);
// Добавляем обработчики и запускаем
server.createContext("/api", new ApiHandler());
server.start();
// Добавляем корректное завершение работы
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Останавливаем сервер...");
server.stop(5); // Ожидаем завершения запросов 5 секунд
threadPoolExecutor.shutdown();
System.out.println("Сервер остановлен");
}));
Важно отметить необходимость корректной остановки сервера. Метод server.stop(delay) позволяет определить время в секундах, в течение которого сервер будет ожидать завершения текущих запросов перед принудительной остановкой.
Создание обработчиков HttpHandler для маршрутизации запросов
Эффективная маршрутизация запросов — критически важный компонент HTTP-сервера. Java SE предлагает интерфейс HttpHandler для обработки входящих запросов, который можно использовать для создания гибкой и расширяемой системы маршрутизации. 🧭
Базовая идея заключается в реализации интерфейса HttpHandler для каждого типа запросов:
public interface HttpHandler {
void handle(HttpExchange exchange) throws IOException;
}
Объект HttpExchange содержит всю информацию о HTTP-запросе, включая метод, заголовки, тело запроса и позволяет формировать HTTP-ответ.
Для создания маршрутизатора мы можем организовать структуру обработчиков несколькими способами:
- По URL-путям — отдельный обработчик для каждого эндпоинта
- По HTTP-методам — единый обработчик, который внутри разделяет логику по методам
- Комбинированный подход — матрица из путей и методов
Рассмотрим пример реализации маршрутизатора, который поддерживает разные HTTP-методы и пути:
public class Router {
private final HttpServer server;
private final Map<String, Map<String, HttpHandler>> routes = new HashMap<>();
public Router(HttpServer server) {
this.server = server;
// Инициализируем карту маршрутов для каждого поддерживаемого пути
routes.put("/api/users", new HashMap<>());
routes.put("/api/products", new HashMap<>());
routes.put("/api/orders", new HashMap<>());
// Регистрируем глобальный обработчик
server.createContext("/api", this::handleRequest);
}
public void addRoute(String path, String method, HttpHandler handler) {
if (!routes.containsKey(path)) {
routes.put(path, new HashMap<>());
}
routes.get(path).put(method, handler);
}
private void handleRequest(HttpExchange exchange) throws IOException {
String path = exchange.getRequestURI().getPath();
String method = exchange.getRequestMethod();
// Проверяем, поддерживается ли данный путь
if (!routes.containsKey(path)) {
sendError(exchange, 404, "Not Found");
return;
}
// Проверяем, поддерживается ли метод для этого пути
Map<String, HttpHandler> methodHandlers = routes.get(path);
if (!methodHandlers.containsKey(method)) {
sendError(exchange, 405, "Method Not Allowed");
return;
}
// Вызываем соответствующий обработчик
try {
methodHandlers.get(method).handle(exchange);
} catch (Exception e) {
e.printStackTrace();
sendError(exchange, 500, "Internal Server Error");
}
}
private void sendError(HttpExchange exchange, int code, String message) throws IOException {
exchange.sendResponseHeaders(code, message.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(message.getBytes());
}
}
}
Теперь мы можем легко добавлять новые обработчики:
Router router = new Router(server);
// Добавляем обработчики для разных путей и методов
router.addRoute("/api/users", "GET", exchange -> {
String response = "[{\"id\": 1, \"name\": \"Иван\"}, {\"id\": 2, \"name\": \"Мария\"}]";
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
});
router.addRoute("/api/users", "POST", exchange -> {
// Логика создания пользователя
String response = "{\"id\": 3, \"name\": \"Новый пользователь\", \"status\": \"created\"}";
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(201, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
});
Для более сложной маршрутизации можно реализовать поддержку URL-параметров и шаблонов путей:
// Класс для хранения информации о маршруте с шаблоном
class RoutePattern {
private final Pattern pattern;
private final List<String> paramNames;
private final Map<String, HttpHandler> methods;
public RoutePattern(String patternString) {
paramNames = new ArrayList<>();
methods = new HashMap<>();
// Преобразуем шаблон вида "/users/:id" в regex "/users/([^/]+)"
// и сохраняем имена параметров
String regex = patternString;
Matcher matcher = Pattern.compile(":(\\w+)").matcher(patternString);
while (matcher.find()) {
paramNames.add(matcher.group(1));
regex = regex.replace(matcher.group(0), "([^/]+)");
}
this.pattern = Pattern.compile(regex);
}
// Остальные методы класса...
}
Для организации обработчиков рекомендуется использовать следующие практики:
- Единая точка обработки ошибок — централизованная логика формирования ответов для исключительных ситуаций
- Валидация входящих данных — проверка параметров запроса перед основной обработкой
- Логирование запросов — фиксация информации о входящих запросах для мониторинга и диагностики
- Разделение ответственности — отдельные обработчики для бизнес-логики и формирования HTTP-ответов
Реализация методов GET и POST с использованием Java SE
Эффективная обработка GET и POST запросов — ключевое умение при создании HTTP-сервера. Эти два метода составляют основу взаимодействия с веб-приложениями, и правильная их реализация критична для работы любого API. 🔄
Рассмотрим специфику обработки каждого метода и создадим полноценные обработчики.
Обработка GET-запросов
GET-запросы используются для получения данных и не должны изменять состояние сервера. Параметры запроса передаются в URL.
Основные компоненты обработки GET-запроса:
- Извлечение параметров запроса из URI
- Формирование ответа (обычно в формате JSON)
- Установка соответствующих заголовков ответа
Пример обработчика GET-запроса с параметрами:
public class UserHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"GET".equals(exchange.getRequestMethod())) {
sendMethodNotAllowed(exchange);
return;
}
// Разбор URI и извлечение параметров
URI uri = exchange.getRequestURI();
String query = uri.getQuery();
Map<String, String> params = parseQueryParameters(query);
// Логика обработки запроса
String response;
if (params.containsKey("id")) {
// Получение пользователя по ID
String id = params.get("id");
response = getUserById(id);
} else {
// Получение списка пользователей
response = getAllUsers();
}
// Формирование ответа
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
private Map<String, String> parseQueryParameters(String query) {
Map<String, String> params = new HashMap<>();
if (query != null) {
String[] pairs = query.split("&");
for (String pair : pairs) {
String[] keyValue = pair.split("=");
if (keyValue.length == 2) {
params.put(keyValue[0], keyValue[1]);
}
}
}
return params;
}
private String getUserById(String id) {
// Имитация получения данных из базы
return "{\"id\": " + id + ", \"name\": \"Пользователь " + id + "\", \"email\": \"user" + id + "@example.com\"}";
}
private String getAllUsers() {
// Имитация получения всех пользователей
return "[{\"id\": 1, \"name\": \"Иван\"}, {\"id\": 2, \"name\": \"Мария\"}]";
}
private void sendMethodNotAllowed(HttpExchange exchange) throws IOException {
String response = "{\"error\": \"Method not allowed\"}";
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(405, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
}
Обработка POST-запросов
POST-запросы используются для отправки данных на сервер, например, для создания новых ресурсов. Данные обычно передаются в теле запроса в формате JSON, URL-encoded или multipart/form-data.
Ключевые аспекты обработки POST-запросов:
- Чтение тела запроса
- Десериализация данных (например, из JSON)
- Обработка данных и формирование ответа
Пример обработчика POST-запроса с JSON-данными:
public class CreateUserHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!"POST".equals(exchange.getRequestMethod())) {
sendMethodNotAllowed(exchange);
return;
}
// Проверка Content-Type
Headers headers = exchange.getRequestHeaders();
String contentType = headers.getFirst("Content-Type");
if (contentType == null || !contentType.contains("application/json")) {
sendBadRequest(exchange, "Content-Type must be application/json");
return;
}
// Чтение тела запроса
try (InputStream is = exchange.getRequestBody()) {
String requestBody = new String(is.readAllBytes());
// Здесь можно использовать библиотеку для парсинга JSON
// или написать простой парсер для учебных целей
Map<String, String> userData = parseJson(requestBody);
// Валидация данных
if (!userData.containsKey("name") || !userData.containsKey("email")) {
sendBadRequest(exchange, "Name and email are required");
return;
}
// Создание пользователя (имитация)
String response = createUser(userData);
// Отправка ответа
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(201, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
} catch (Exception e) {
sendInternalError(exchange, e.getMessage());
}
}
// Простой парсер JSON (для учебных целей)
private Map<String, String> parseJson(String json) {
Map<String, String> result = new HashMap<>();
// Удаляем фигурные скобки и разбиваем по запятым
String content = json.replaceAll("[{}\"]", "").trim();
String[] pairs = content.split(",");
for (String pair : pairs) {
String[] keyValue = pair.split(":");
if (keyValue.length == 2) {
result.put(keyValue[0].trim(), keyValue[1].trim());
}
}
return result;
}
private String createUser(Map<String, String> userData) {
// Имитация создания пользователя
int id = (int) (Math.random() * 1000);
return "{\"id\": " + id + ", \"name\": \"" + userData.get("name") + "\", \"email\": \"" + userData.get("email") + "\", \"status\": \"created\"}";
}
private void sendMethodNotAllowed(HttpExchange exchange) throws IOException {
String response = "{\"error\": \"Method not allowed\"}";
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(405, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
private void sendBadRequest(HttpExchange exchange, String message) throws IOException {
String response = "{\"error\": \"" + message + "\"}";
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(400, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
private void sendInternalError(HttpExchange exchange, String message) throws IOException {
String response = "{\"error\": \"Internal server error\", \"message\": \"" + message + "\"}";
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(500, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
}
Для более сложных сценариев можно реализовать обработку различных форматов тела запроса:
| Content-Type | Формат данных | Особенности обработки |
|---|---|---|
| application/json | JSON-объект | Парсинг JSON-строки в объект |
| application/x-www-form-urlencoded | key1=value1&key2=value2 | Разбор строки запроса, как для GET-параметров |
| multipart/form-data | Составные данные, включая файлы | Требует сложного парсинга границ частей |
| text/plain | Произвольный текст | Прямое чтение как строки |
Рекомендации по обработке запросов:
- Всегда проверяйте Content-Type перед обработкой POST-запроса
- Добавляйте валидацию данных для предотвращения ошибок
- Используйте try-with-resources для корректного закрытия потоков
- Устанавливайте корректные коды ответов: 201 Created для успешного создания, 400 Bad Request для ошибок валидации и т.д.
- Логируйте ошибки для упрощения отладки
Расширение функционала: статика, заголовки и сессии
Создав базовую структуру HTTP-сервера, вы можете расширить его функционал для поддержки более сложных сценариев использования. Рассмотрим несколько ключевых улучшений, которые сделают ваш сервер более функциональным. 📊
Обслуживание статических файлов
Для полноценного веб-приложения часто требуется обслуживание статических файлов (HTML, CSS, JavaScript, изображения). Реализуем обработчик, который будет отдавать файлы из указанной директории:
public class StaticFileHandler implements HttpHandler {
private final String rootDirectory;
private final Map<String, String> mimeTypes;
public StaticFileHandler(String rootDirectory) {
this.rootDirectory = rootDirectory;
this.mimeTypes = new HashMap<>();
// Инициализация MIME-типов
mimeTypes.put("html", "text/html");
mimeTypes.put("css", "text/css");
mimeTypes.put("js", "application/javascript");
mimeTypes.put("jpg", "image/jpeg");
mimeTypes.put("jpeg", "image/jpeg");
mimeTypes.put("png", "image/png");
mimeTypes.put("gif", "image/gif");
mimeTypes.put("ico", "image/x-icon");
}
@Override
public void handle(HttpExchange exchange) throws IOException {
String path = exchange.getRequestURI().getPath();
// Нормализация пути запроса
if (path.equals("/")) {
path = "/index.html";
}
// Формируем полный путь к файлу
File file = new File(rootDirectory + path);
if (!file.exists() || file.isDirectory()) {
sendNotFound(exchange);
return;
}
// Определяем MIME-тип по расширению файла
String contentType = getContentType(file.getName());
// Отправляем файл
try (FileInputStream fis = new FileInputStream(file)) {
exchange.getResponseHeaders().set("Content-Type", contentType);
exchange.sendResponseHeaders(200, file.length());
try (OutputStream os = exchange.getResponseBody()) {
byte[] buffer = new byte[8192]; // 8KB буфер
int count;
while ((count = fis.read(buffer)) != -1) {
os.write(buffer, 0, count);
}
}
} catch (IOException e) {
e.printStackTrace();
sendInternalError(exchange);
}
}
private String getContentType(String fileName) {
String extension = "";
int i = fileName.lastIndexOf('.');
if (i > 0) {
extension = fileName.substring(i + 1).toLowerCase();
}
return mimeTypes.getOrDefault(extension, "application/octet-stream");
}
private void sendNotFound(HttpExchange exchange) throws IOException {
String response = "404 Not Found";
exchange.sendResponseHeaders(404, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
private void sendInternalError(HttpExchange exchange) throws IOException {
String response = "500 Internal Server Error";
exchange.sendResponseHeaders(500, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
}
Подключение статического обработчика к серверу:
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8080), 0);
// Регистрация обработчика для статики
server.createContext("/static", new StaticFileHandler("./public"));
// Регистрация API-обработчиков
server.createContext("/api", new ApiHandler());
server.setExecutor(Executors.newFixedThreadPool(10));
server.start();
Управление HTTP-заголовками
Правильная работа с HTTP-заголовками критична для безопасности и производительности. Реализуем фильтр для добавления стандартных заголовков:
public class HeaderFilter {
private final HttpHandler next;
public HeaderFilter(HttpHandler next) {
this.next = next;
}
public HttpHandler wrap() {
return exchange -> {
// Добавляем стандартные заголовки безопасности
Headers headers = exchange.getResponseHeaders();
// Защита от XSS
headers.set("X-XSS-Protection", "1; mode=block");
// Запрет встраивания в фреймы
headers.set("X-Frame-Options", "DENY");
// Content Security Policy
headers.set("Content-Security-Policy", "default-src 'self'");
// CORS-заголовки для API
if (exchange.getRequestURI().getPath().startsWith("/api")) {
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
// Обработка preflight-запросов
if ("OPTIONS".equals(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(204, -1);
return;
}
}
// Передаем запрос дальше
next.handle(exchange);
};
}
}
Применение фильтра к обработчикам:
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8080), 0);
// Создаем и регистрируем обработчики с фильтром заголовков
HeaderFilter headerFilter = new HeaderFilter(new ApiHandler());
server.createContext("/api", headerFilter.wrap());
server.start();
Управление сессиями
Для хранения состояния между запросами реализуем простой механизм сессий на основе cookie:
public class SessionManager {
private final Map<String, Session> sessions = new ConcurrentHashMap<>();
private final String cookieName = "JSESSIONID";
private final int sessionTimeout = 1800; // 30 минут в секундах
public Session getSession(HttpExchange exchange, boolean create) {
String sessionId = getSessionIdFromCookie(exchange);
Session session = null;
if (sessionId != null) {
session = sessions.get(sessionId);
// Если сессия найдена, обновляем время последнего доступа
if (session != null) {
session.access();
}
}
if (session == null && create) {
// Создаем новую сессию
sessionId = generateSessionId();
session = new Session(sessionId);
sessions.put(sessionId, session);
// Устанавливаем cookie с ID сессии
setCookie(exchange, sessionId);
}
return session;
}
public void invalidateSession(HttpExchange exchange) {
String sessionId = getSessionIdFromCookie(exchange);
if (sessionId != null) {
sessions.remove(sessionId);
}
// Удаляем cookie
deleteCookie(exchange);
}
private String getSessionIdFromCookie(HttpExchange exchange) {
Headers headers = exchange.getRequestHeaders();
List<String> cookies = headers.get("Cookie");
if (cookies != null) {
for (String cookie : cookies) {
String[] parts = cookie.split(";");
for (String part : parts) {
part = part.trim();
if (part.startsWith(cookieName + "=")) {
return part.substring(cookieName.length() + 1);
}
}
}
}
return null;
}
private void setCookie(HttpExchange exchange, String sessionId) {
exchange.getResponseHeaders().add("Set-Cookie",
cookieName + "=" + sessionId + "; Path=/; HttpOnly; SameSite=Strict");
}
private void deleteCookie(HttpExchange exchange) {
exchange.getResponseHeaders().add("Set-Cookie",
cookieName + "=; Path=/; Max-Age=0; HttpOnly");
}
private String generateSessionId() {
return UUID.randomUUID().toString();
}
// Фоновая задача для удаления устаревших сессий
public void startSessionCleanup() {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(this::cleanupSessions, 10, 10, TimeUnit.MINUTES);
}
private void cleanupSessions() {
long currentTime = System.currentTimeMillis();
sessions.entrySet().removeIf(entry -> {
Session session = entry.getValue();
return (currentTime – session.getLastAccessTime()) / 1000 > sessionTimeout;
});
}
// Класс для хранения данных сессии
public static class Session {
private final String id;
private long lastAccessTime;
private final Map<String, Object> attributes = new ConcurrentHashMap<>();
public Session(String id) {
this.id = id;
this.lastAccessTime = System.currentTimeMillis();
}
public void setAttribute(String name, Object value) {
attributes.put(name, value);
}
public Object getAttribute(String name) {
return attributes.get(name);
}
public void removeAttribute(String name) {
attributes.remove(name);
}
public String getId() {
return id;
}
public long getLastAccessTime() {
return lastAccessTime;
}
void access() {
this.lastAccessTime = System.currentTimeMillis();
}
}
}
Использование менеджера сессий в обработчике:
public class SessionAwareHandler implements HttpHandler {
private final SessionManager sessionManager;
public SessionAwareHandler(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
// Получаем сессию (создаем, если не существует)
SessionManager.Session session = sessionManager.getSession(exchange, true);
// Получаем счетчик посещений из сессии
Integer visits = (Integer) session.getAttribute("visits");
if (visits == null) {
visits = 0;
}
// Увеличиваем счетчик
visits++;
session.setAttribute("visits", visits);
// Формируем ответ
String response = "Вы посетили эту страницу " + visits + " раз(а).";
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
exchange.sendResponseHeaders(200, response.getBytes().length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
}
Подключение компонентов:
HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8080), 0);
// Создаем менеджер сессий
SessionManager sessionManager = new SessionManager();
sessionManager.startSessionCleanup();
// Регистрируем обработчик с поддержкой сессий
server.createContext("/counter", new SessionAwareHandler(sessionManager));
// Другие обработчики...
server.setExecutor(Executors.newFixedThreadPool(10));
server.start();
Дополнительные улучшения, которые можно реализовать:
- Кэширование — добавление заголовков Cache-Control и ETag для статических ресурсов
- Сжатие контента — поддержка GZIP или Deflate для уменьшения объема передаваемых данных
- Загрузка файлов — обработка multipart/form-data для загрузки файлов
- Аутентификация — базовая или на основе токенов
- Мониторинг и логирование — отслеживание производительности и ведение журналов
HTTP-сервер на Java SE API дает уникальную возможность глубоко понять протокол HTTP и механизмы сетевого взаимодействия. Реализовав базовую версию сервера, вы можете постепенно расширять его функционал, добавляя поддержку сессий, аутентификацию, кэширование и другие возможности. Главное преимущество такого подхода — полный контроль над всеми аспектами работы сервера и отсутствие зависимостей от внешних библиотек. Это идеальный способ научиться разрабатывать эффективные и оптимизированные веб-приложения, понимая каждую деталь их функционирования.