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.