WebSockets в Java: от чатов до биржевых графиков в реальном времени

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

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

Начать курс

Веб-сокеты в 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) {
        // Пре                

Блоги

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