Server-Sent Events в Java: как отправлять данные в реальном времени без WebSocket

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Server-Sent Events в Java: когда WebSocket — это перебор, а HTTP — недостаточно

Вы когда-нибудь задумывались, как работают ленты новостей в Twitter, уведомления в Telegram или live-результаты спортивных матчей? Вроде бы данные приходят сами собой, без обновления страницы. Секрет прост: Server-Sent Events (SSE) — технология, которая позволяет серверу отправлять данные клиенту в реальном времени через обычное HTTP-соединение.

В этой статье мы разберем Server-Sent Events в Java. Вы узнаете, почему SSE часто удобнее WebSocket, как настроить сервер на Java Servlet API, и как принимать события на клиенте. Код будет рабочим, стиль — живым, а примеры — максимально приближенными к реальным задачам.

Что такое Server-Sent Events и зачем они нужны?

Server-Sent Events (SSE) — это стандарт W3C, который позволяет серверу «пушить» данные клиенту через одно долгоживущее HTTP-соединение. Клиент подписывается на поток событий, а сервер отправляет сообщения по мере их появления.

SSE идеально подходит для:

  • Уведомлений — новых писем, сообщений, изменений статуса.
  • Лент новостей — посты, твиты, курсы валют.
  • Мониторинга — логи, метрики сервера, прогресс задач.
  • Live-данных — котировки акций, результаты матчей, погода.

В отличие от WebSocket, SSE работает только в одну сторону (сервер → клиент), но этого достаточно для большинства сценариев. Плюс SSE использует обычный HTTP, не требует дополнительных портов и легко проходит через прокси и фаерволы.

Server-Sent Events vs WebSocket: что выбрать?

Если вам нужно только отправлять данные от сервера клиенту (без обратной связи), выбирайте SSE. Он проще, легче и дешевле. WebSocket оправдан, когда клиент тоже активно отправляет данные (например, в чатах или онлайн-играх).

Критерий Server-Sent Events WebSocket
Направление Сервер → Клиент Двустороннее
Протокол HTTP/1.1, HTTP/2 ws:// или wss://
Автоматическое переподключение Да (встроено в браузер) Нет (нужно реализовывать)
Сложность реализации Низкая Средняя/высокая
Поддержка браузеров Все современные Все современные

Для образовательного сайта PythonLib мы выбрали Java, потому что это надежный язык для серверной разработки. Поехали!

Серверная часть: реализация SSE на Java с помощью Servlet API

Начнем с сервера. Самый простой способ — использовать стандартные Java Servlets. Нам нужно:

  • Установить заголовок Content-Type: text/event-stream.
  • Отключить кэширование (Cache-Control: no-cache).
  • Держать соединение открытым.
  • Отправлять данные в формате data: ...\n\n.

Пример 1: Простой SSE-сервер на Java Servlet

Создадим сервлет, который отправляет текущее время каждые 5 секунд.

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@WebServlet("/time-stream")
public class TimeStreamServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        resp.setContentType("text/event-stream");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Cache-Control", "no-cache");
        resp.setHeader("Connection", "keep-alive");
        
        PrintWriter writer = resp.getWriter();
        
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(() -> {
            try {
                String time = LocalTime.now().toString();
                writer.write("data: Текущее время: " + time + "\n\n");
                writer.flush();
            } catch (Exception e) {
                scheduler.shutdown();
            }
        }, 0, 5, TimeUnit.SECONDS);
        
        // Важно: не закрываем writer до завершения соединения
        // Клиент сам разрывает соединение, или сервер по таймауту
    }
}

Как это работает:

  • Устанавливаем заголовки для SSE.
  • Получаем PrintWriter и отправляем данные в формате data: ...\n\n.
  • Используем ScheduledExecutorService для периодической отправки.
  • Важно: не закрываем поток, пока клиент не отключится.

Пример 2: SSE с именованными событиями и ID

Иногда нужно отправлять разные типы событий. SSE поддерживает event и id для идентификации.

@WebServlet("/news-stream")
public class NewsStreamServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        resp.setContentType("text/event-stream");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Cache-Control", "no-cache");
        
        PrintWriter writer = resp.getWriter();
        int eventId = 0;
        
        while (true) {
            try {
                eventId++;
                String news = "Новость #" + eventId + ": Java 21 вышла!";
                
                writer.write("id: " + eventId + "\n");
                writer.write("event: news\n");
                writer.write("data: " + news + "\n\n");
                writer.flush();
                
                Thread.sleep(3000); // ждем 3 секунды
            } catch (InterruptedException e) {
                break;
            } catch (Exception e) {
                // клиент отключился
                break;
            }
        }
    }
}

Формат сообщения SSE:

  • id — уникальный идентификатор события (нужен для автоматического восстановления).
  • event — тип события (на клиенте можно подписаться на конкретный тип).
  • data — полезная нагрузка (может быть JSON, текст и т.д.).
  • Каждое сообщение завершается двумя переносами строки \n\n.

Пример 3: SSE с JSON-данными (реальный сценарий)

Блоги

Рекомендаций по книгам