Long Polling vs WebSocket в Java: дуэль технологий реального времени
Представьте: вы разрабатываете чат, биржевой терминал или онлайн-игру на Java. Пользователь ждет обновлений, а сервер молчит. Как заставить данные приходить мгновенно? Раньше был один выход — Long Polling. Сегодня на арене WebSocket. Но кто победит в реальном бою?
В этой статье мы не просто сравним концепции. Мы напишем живой код на Java, посмотрим на внутренности обеих технологий и решим, что использовать в вашем следующем проекте. Добро пожаловать на ринг!
Что такое Long Polling (и почему это не «опрос»)?
Классический HTTP — это как письмо: отправил запрос → получил ответ. Если серверу нечего сказать, он возвращает пустой ответ. Клиент вынужден спрашивать снова каждые N секунд. Это Short Polling — тупиковый путь.
Long Polling работает хитрее. Клиент отправляет запрос, а сервер… засыпает. Он держит соединение открытым до тех пор, пока не появится новое событие (или не истечет таймаут). Как только событие происходит — сервер просыпается и отправляет ответ. Клиент тут же создает новый Long Polling запрос. Цикл замкнут.
Long Polling на Java: простая реализация
Давайте представим, что мы пишем сервер уведомлений на чистом Servlet API (без Spring, чтобы было понятнее). Для Long Polling нам понадобится асинхронная обработка (AsyncContext), иначе сервер заблокирует все потоки.
import javax.servlet.AsyncContext;
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.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@WebServlet(urlPatterns = "/long-poll", asyncSupported = true)
public class LongPollingServlet extends HttpServlet {
// Очередь для хранения "спящих" соединений
private static final Queue<AsyncContext> WAITING_CLIENTS = new ConcurrentLinkedQueue<>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(30_000); // Таймаут 30 секунд
// Сохраняем контекст
WAITING_CLIENTS.add(asyncContext);
// Если таймаут истек — возвращаем пустой ответ
asyncContext.addListener(new javax.servlet.AsyncListener() {
public void onTimeout(javax.servlet.AsyncEvent event) {
WAITING_CLIENTS.remove(asyncContext);
try {
HttpServletResponse response = (HttpServletResponse) event.getAsyncContext().getResponse();
response.getWriter().write("timeout");
response.setStatus(HttpServletResponse.SC_OK);
asyncContext.complete();
} catch (IOException e) { e.printStackTrace(); }
}
// Другие методы слушателя опущены для краткости
});
}
// Этот метод вызывается, когда появляется новое событие
public static void sendEventToClients(String message) {
for (AsyncContext ctx : WAITING_CLIENTS) {
try {
HttpServletResponse response = (HttpServletResponse) ctx.getResponse();
response.getWriter().write(message);
response.setStatus(HttpServletResponse.SC_OK);
ctx.complete();
WAITING_CLIENTS.remove(ctx);
} catch (IOException e) { e.printStackTrace(); }
}
}
}
Как это работает:
- Клиент делает GET-запрос к
/long-poll. - Сервер переводит запрос в асинхронный режим и «засыпает» (ждет).
- Когда появляется сообщение (метод
sendEventToClients), сервер отдает его всем ожидающим клиентам. - Клиент получает ответ и тут же отправляет новый запрос.
Минусы Long Polling:
- Высокая нагрузка на сервер при большом количестве клиентов (каждый держит поток/контекст).
- Задержка на HTTP-заголовки при каждом запросе-ответе.
- Сложность с балансировкой нагрузки (нужно привязывать клиента к конкретному серверу или использовать общее хранилище).
WebSocket: постоянный канал связи
WebSocket — это уже не письмо, а телефонный разговор. Один раз устанавливаете соединение (через HTTP-рукопожатие), и дальше сервер и клиент могут обмениваться сообщениями в обе стороны в реальном времени. Никаких повторных запросов, никаких таймаутов.
В Java WebSocket стандартизирован спецификацией JSR 356 (Java API for WebSocket). Есть реализации от Tomcat, Jetty, Netty, а также удобная абстракция в Spring Framework.
WebSocket на Java: пример с JSR 356
Напишем простой эхо-сервер. Все, что нужно — это аннотация @ServerEndpoint.
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/ws")
public class WebSocketServer {
// Храним все активные сессии
private static final Set<Session> sessions = new CopyOnWriteArraySet<>();
@OnOpen
public void onOpen(Session session) {
sessions.add(session);
System.out.println("Новое соединение: " + session.getId());
}
@OnMessage
public void onMessage(String message, Session session) throws IOException {
System.out.println("Получено: " + message);
// Отправляем сообщение обратно всем клиентам
for (Session s : sessions) {
if (s.isOpen()) {
s.getBasicRemote().sendText("Эхо: " + message);
}
}
}
@OnClose
public void onClose(Session session) {
sessions.remove(session);
System.out.println("Соединение закрыто: " + session.getId());
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
sessions.remove(session);
}
}
Как это работает:
- Клиент открывает WebSocket соединение по адресу
ws://ваш-сервер/ws. - Сервер вызывает
@OnOpen. - При получении сообщения —
@OnMessage. - Сервер может в любой момент отправить сообщение клиенту (push).
WebSocket в Spring Boot (еще проще)
Если вы используете Spring, то WebSocket становится тривиальным. Добавляете зависимость spring-boot-starter-websocket и создаете конфигурацию:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/ws").setAllowedOrigins("*");
}
}
А сам обработчик:
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
public class MyWebSocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) {
System.out.println("Подключен: " + session.getId());
}
@Override
protected void handleTextMessage(Web