Веб-сокеты в Java: когда HTTP уже не справляется
Представьте себе онлайн-биржу. Вы смотрите на график цены биткоина. Каждую секунду цифры меняются. Или многопользовательскую игру, где вы двигаете персонажа, а сотня других игроков видит это мгновенно. Или просто корпоративный чат, где сообщения приходят без нажатия F5.
Как это работает под капотом? Раньше разработчики изворачивались: polling (клиент каждую секунду долбит сервер вопросом "ну что, есть новости?"), long polling (сервер держит соединение, пока не появится новость), Server-Sent Events (односторонний поток). Всё это — костыли. Протокол HTTP изначально создавался как запрос-ответ. Он не умеет "толкать" данные сам.
Тут и приходят на помощь WebSockets. Это протокол, который поднимает соединение один раз — и держит его открытым. Сервер может в любой момент отправить данные клиенту, и наоборот. Двусторонняя связь в реальном времени, без оверхеда на заголовки HTTP.
В этой статье мы разберем WebSockets в Java от и до. Напишем полноценный чат, разберем подводные камни и сравним популярные библиотеки. Если вы Java-разработчик и хотите делать real-time приложения на Java — вы по адресу.
Как работает WebSocket? Краткий ликбез
WebSocket — это не магия, а просто протокол поверх TCP. Он начинается как обычный HTTP-запрос (handshake), а потом "апгрейдится" до постоянного двустороннего канала.
Этап 1: Рукопожатие (Handshake)
Клиент отправляет обычный HTTP GET-запрос с заголовком Upgrade: websocket. Сервер отвечает кодом 101 Switching Protocols. Всё. С этого момента данные летят не как HTTP-пакеты, а как фреймы WebSocket.
Этап 2: Фреймы данных
Данные передаются маленькими пакетами (фреймами). Фрейм может быть текстовым (обычная строка) или бинарным (картинка, аудио). Java-библиотеки скрывают эту сложность — вы работаете просто со строками или байтами.
Этап 3: Закрытие
Любая сторона может инициировать закрытие соединения, отправив специальный фрейм Close.
Звучит просто? Так и есть. Но дьявол кроется в деталях: таймауты, реконнекты, масштабирование. Но об этом позже.
Библиотеки для WebSockets в Java
В экосистеме Java есть три основных подхода:
- Jakarta WebSocket (JSR 356) — стандарт Java EE / Jakarta EE. Работает на любом совместимом сервере (Tomcat, Jetty, GlassFish). Аннотаций много, кода мало.
- Spring WebSocket — надстройка над стандартом в экосистеме Spring. Даёт интеграцию с Security, STOMP (протокол поверх WebSocket для подписок на топики) и удобные шаблоны.
- Netty / Vert.x / Undertow — низкоуровневые асинхронные фреймворки. Максимальная производительность, но больше кода.
Для 90% задач хватит Jakarta или Spring. Мы рассмотрим оба варианта с реальными примерами.
Пример 1: Простой чат на Jakarta WebSocket (без Spring)
Допустим, у нас есть старый добрый Tomcat 10+ (Jakarta EE). Нам нужен серверный endpoint и клиент (HTML+JS).
Сервер: класс-эндпоинт
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/chat")
public class ChatEndpoint {
// Храним все активные сессии (потокобезопасное множество)
private static final Set<Session> sessions = new CopyOnWriteArraySet<>();
@OnOpen
public void onOpen(Session session) {
sessions.add(session);
broadcast("Пользователь подключился. Всего в чате: " + sessions.size());
}
@OnMessage
public void onMessage(String message, Session session) {
// Можно добавить логику: отправить только одному, или всем
broadcast("Сообщение: " + message);
}
@OnClose
public void onClose(Session session) {
sessions.remove(session);
broadcast("Пользователь отключился. Осталось: " + sessions.size());
}
@OnError
public void onError(Session session, Throwable throwable) {
System.err.println("Ошибка у сессии " + session.getId() + ": " + throwable.getMessage());
}
private void broadcast(String message) {
for (Session s : sessions) {
if (s.isOpen()) {
try {
s.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Это весь сервер. Пять методов. Аннотации @OnOpen, @OnMessage, @OnClose, @OnError делают всю магию. Обратите внимание на CopyOnWriteArraySet — это важно для многопоточности.
Клиент: простой HTML
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>
<input id="msgInput" type="text" />
<button onclick="sendMsg()">Отправить</button>
<script>
const ws = new WebSocket("ws://localhost:8080/chat");
const messagesDiv = document.getElementById('messages');
ws.onmessage = function(event) {
const msg = document.createElement('p');
msg.textContent = event.data;
messagesDiv.appendChild(msg);
};
function sendMsg() {
const input = document.getElementById('msgInput');
ws.send(input.value);
input.value = '';
}
</script>
</body>
</html>
Работает? Да. Но есть нюанс: это не чат "один-одному", а лобби, где все видят всё. Для приватных сообщений нужно добавить логику идентификации пользователей.
Пример 2: Spring WebSocket + STOMP (продвинутый уровень)
Spring добавляет поверх WebSocket протокол STOMP (Simple Text Oriented Messaging Protocol). Это как MQTT, но для веба. Вы подписываетесь на топики (/topic/public, /user/queue/private) и получаете сообщения только из нужных каналов.
Это стандарт для real-time приложений на Java в enterprise-среде.
Конфигурация Spring WebSocket
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// Пре