Java. WebSocket. Spring WebSocket

Зміст

HTTP у контексті WebSockets

HTTP (HyperText Transfer Protocol) є основним протоколом для передачі веб-даних через мережу. Зазвичай, HTTP використовує модель "запит-відповідь", де клієнт відправляє запит і чекає відповіді від сервера. Це може бути неефективно для додатків, що вимагають високої інтерактивності та обміну даними в режимі реального часу. Тут на допомогу приходять WebSockets.

simple HTTP over TCP

Що таке вебсокет з’єднання?

WebSockets дозволяють створювати двосторонні з’єднання між клієнтом (наприклад, веб-браузером) і сервером. Таке з'єднання дозволяє даним вільно переміщуватися в обидва боки без потреби в постійному перевстановленні зв’язку, що є характерним для HTTP. Вебсокети використовують вже існуюче TCP з’єднання, яке залишається відкритим протягом усієї сесії.

WebSocket over HTTP

Вебсокет з технічної/мережевої точки зору

Технічно, WebSocket є протоколом, який базується на TCP (Transmission Control Protocol) для надійного зв'язку. З’єднання WebSocket починається з HTTP-запиту, званого "WebSocket handshake", який оновлює з'єднання до сталого WebSocket-каналу.

Ці особливості роблять WebSockets ідеальними для застосунків, таких як онлайн ігри, торгівельні платформи та інші сервіси, де потрібен швидкий та стабільний обмін даними.

Встановлення з’єднання: WebSocket Handshake

WebSocket handshake розпочинається, коли клієнт відправляє серверу запит HTTP UPGRADE, сигналізуючи про бажання перейти від HTTP до WebSocket. Запит виглядає приблизно так:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

У цьому запиті Sec-WebSocket-Key — унікальний ключ, який сервер використовує для формування відповіді, що гарантує, що запит справді був здійснений клієнтом.

Якщо сервер підтримує WebSocket, він відповідає зі статус-кодом 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

Тут Sec-WebSocket-Accept є закодованою відповіддю на Sec-WebSocket-Key, що підтверджує, що сервер "бачить" клієнтський запит на зміну протоколів.

Деталі протоколу

Після успішного handshake клієнт і сервер можуть вільно обмінюватися даними через встановлене з'єднання WebSocket. Дані можуть передаватися в текстовому або бінарному форматі. Комунікація через WebSocket є більш ефективною порівняно з HTTP, оскільки після встановлення з'єднання заголовки не передаються з кожним повідомленням, що знижує загальні накладні витрати.

Крім того, протоколи ws:// (незахищене з’єднання) та wss:// (захищене з’єднання) вказують на тип з'єднання. Протокол wss:// використовує шифрування TLS/SSL для забезпечення безпеки даних.

Простий приклад на JS

1) встановив NodeJS + відповідні бібліотеки

npm install websocket

npm install http

2) HTTP/WS JS сервер

const http = require("http")
const WebSocketServer = require("websocket").server
let connections = [];

const httpServer = http.createServer()

const websocket = new WebSocketServer({"httpServer": httpServer })
httpServer.listen(8082, () => console.log("My server is listening"))

websocket.on("request", requet => {
    const connection = requet.accept(null, requet.origin);
    console.log("new connection")

    connection.on("message", message => {
        connections.forEach(c => c.send(`User${connection.socket.remotePort} says: ${message.utf8Data}`));
    })

    connections.push(connection);
    connections.forEach(c => c.send(`New user joined! User${connection.socket.remotePort}`))
})
const websocket = new WebSocketServer({"httpServer": httpServer })

Вебсокет сервер

const websocket = new WebSocketServer({ "httpServer": httpServer }); - ініціалізує WebSocket сервер, який прив'язується до HTTP сервера. Таким чином, всі WebSocket запити, що надходять через вказаний порт (8082), будуть оброблятися через цей WebSocket сервер.

З’єднання

  • Коли клієнт намагається з'єднатися з сервером через WebSocket, він відправляє HTTP GET запит з заголовками, що специфікують, що це WebSocket запит. Це включає Upgrade: websocket і Connection: Upgrade.

  • websocket.on("request", request => {...}) - ця подія викликається, коли сервер отримує новий WebSocket запит. request.accept(null, request.origin) приймає з'єднання.

let ws = new WebSocket("ws://localhost:8082");

(сніффінг роблю через Wireshark — аналізатор мережевого трафіку, aka sniffer)

Wireshark. Нюхаємо мереживий трафік

Коли встановлюється з’єднання, відбувається http запит. В ході якого ми просимо змінити протокол обміну даними (хедери: Connection: Upgrade, Upgrade: websocket).

Сервер в свою чергу повертає відповідь із 101 статус кодом — що так, давай змінимо протокол між тобою і мною. Тепер будемо використовувати вебсокети.

Потім, після з’єднання, я розсилаю всім клієнтам інфомацію про нового користуча (і собі також, не робив додаткових перевірок).

Тут бачимо, що source port 8082(сервер), а dst port: 51664 (клієнт).
Від сервера прийшов меседж по протоколу WS.

Потім відбуваються PING-PONG меседжі. Ping від сервера до клієнта, а Pong навпаки. Це потрібно для:

  • Підтримки активності з'єднання

  • Виявлення втрати з'єднання

  • Вимірювання затримки мережі (latency)

Бачимо, що websocket протокол є просто додатковою фунціональністю над ip/tcp/http, яке забезпечує двосторонній зв’язок за рахунок відкритого з’єднання і пінг-понг меседжів.

3) Клієнти — інкогніто вклади браузера (відповідно при встановленні з’єднання, вони будуть різними)

let ws = new WebSocket("ws://localhost:8082");
ws.onmessage = message => console.log(`${message.data}`)
ws.send("Hi! How r u?")

Spring WebSocket

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

Одразу, щоб не заплутати, розберемо технології які будуть часто бігати у вас перед очима перед роботою з вебсокетами:

  • Spring WebSocket

Це модуль Spring Framework, який надає підтримку веб-сокетам у веб-додатках на Java. WebSocket дозволяє браузерам та веб-серверам встановлювати двостороннє з'єднання та обмінюватися даними в реальному часі.

  • SockJS

Це бібліотека JavaScript, яка надає абстракцію над різними транспортними механізмами, що підтримують WebSocket. SockJS дозволяє використовувати WebSocket там, де це можливо.

SockJS надає механізм автоматичного переходу на альтернативні транспортні протоколи, такі як HTTP Streaming або Long Polling, у випадку, якщо веб-сокети не підтримуються між клієнтом і сервером. Це важливо, оскільки не всі браузери або проксі-сервери підтримують WebSocket. Шляхом автоматичного переключення на альтернативні механізми транспорту SockJS дозволяє забезпечити роботу вашого додатка в реальному часі навіть у випадку обмежень з підтримки WebSocket.

  • Socket.IO

Socket.IO - це бібліотека JavaScript для роботи з веб-сокетами, яка надає абстракцію над різними механізмами транспорту, такими як WebSocket, AJAX, а також підтримує ряд додаткових функцій, які полегшують створення реального часу додатків.

Основні особливості Socket.IO включають:

  • Підтримка різних механізмів транспорту: Socket.IO автоматично вибирає найбільш підходящий механізм транспорту для з'єднання між клієнтом і сервером, залежно від умов на мережі. Якщо WebSocket недоступний, він може використовувати AJAX або інші техніки, такі як Long Polling.

  • Простота використання: Socket.IO надає простий та інтуїтивно зрозумілий API для роботи з веб-сокетами, що дозволяє швидко створювати додатки реального часу.

  • Додаткові функції: Socket.IO містить додаткові функціональні можливості, такі як автоматичне перепідключення у разі втрати з'єднання, підтримка кімнат та обробка помилок.

Socket.IO широко використовується для розробки real-time додатків, таких як чати, ігри та потокові додатки. Він є популярним інструментом серед розробників завдяки своїй простоті використання та розширюваності.

  • STOMP

Простий текстовий протокол повідомлень (Simple Text Oriented Messaging Protocol), є протоколом для обміну даними між клієнтами і серверами через проміжні системи, такі як веб-сервери або проміжний програмне забезпечення. STOMP може використовуватися разом з WebSocket для створення мережевих додатків в реальному часі. У Spring Framework є підтримка STOMP, що дозволяє спрощувати роботу з веб-сокетами, забезпечуючи абстракцію над рівнем WebSocket і підтримку таких функцій, як маршрутизація повідомлень та керування підпискою.

Припустимо, що у нас є веб-додаток для чату, де користувачі можуть надсилати повідомлення один одному у реальному часі.

З SockJS ми можемо створити канал для обміну повідомленнями між клієнтом і сервером. В нашому прикладі, давайте реалізуємо можливість надсилання повідомлень від клієнта до сервера та розсилки їх усім підключеним клієнтам.

На стороні клієнта (JavaScript) ми можемо мати щось таке:

var socket = new SockJS('/chat');
var stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
    console.log('Connected: ' + frame);
    stompClient.subscribe('/topic/messages', function(messageOutput) {
        showMessage(JSON.parse(messageOutput.body).content);
    });
});

function sendMessage(message) {
    stompClient.send("/app/sendMessage", {}, JSON.stringify({'content': message}));
}

function showMessage(message) {
    // Показати повідомлення в чаті
}

На стороні сервера (Spring Boot) ми можемо мати контролер WebSocket:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat").withSockJS();
    }
}

Тут ми використовуємо @EnableWebSocketMessageBroker для включення підтримки веб-сокетів у Spring Boot, а також налаштовуємо маршрутизацію повідомлень через /app та підписку на топік /topic/messages.

У методі sendMessage клієнт надсилає повідомлення на сервер, а сервер розсилає його усім підключеним клієнтам через топік /topic/messages.

SockJS приховує деталі роботи з WebSocket, дозволяючи розробникам зосередитися на логіці свого додатку.

@Controller
public class ChatController {

    @MessageMapping("/sendMessage") // Цей мапінг відповідає адресі, на яку клієнт надсилає повідомлення
    @SendTo("/topic/messages") // Цей топік, до якого буде розіслано відповідь клієнтам
    public Message sendMessage(@Payload Message message) {
        // Обробляємо повідомлення
        return message;
    }
}

Без STOMP, просто вебсокети у Spring

  • конфігурація вебсокета на сервері (прописуємо ендпоінт, по якому сервер буде фільтрувати чи взагалі намагатися вебсокет зєднання, іншими словами, чи дозволяти змінювати протокол на вебсокет)

@Configuration
@EnableWebSocket
@Slf4j
public class NoteWebSocketConfig implements WebSocketConfigurer {
    private final WebSocketHandler handler;

    public NoteWebSocketConfig(@Lazy WebSocketHandler handler) {
        this.handler = handler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        WebSocketHandlerRegistration registration =
                registry.addHandler(new ExceptionWebSocketHandlerDecorator(handler), "/init");
        registration.setAllowedOriginPatterns("*");
    }
}
private final WebSocketHandler handler;

Сюди заавтовайряться біни які реалізують цей інтерфейс. Давайте створимо якийсь вебсокет хендлер.

Потрібно перевизначити ці методи-колбеки. Що будемо робити під з’єднання з клієнтом? (можливо зберегти в базу або в якийсь кеш) Що після розриву сесії? Що під час того, коли нам хтось кидає повідомлення?

@Slf4j
@Component
public class ShoShoWebSocketHandler implements WebSocketHandler {
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("Connection established");
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        log.info("Got message={}", message.getPayload());
        session.sendMessage(message);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        log.info("Connection closed");
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }
}

Вебсокети в Postman

Обираємо WebSocket

Прописуємо шлях по визначеному шляху щоб встановити WS конекшн.

Працює :)

Це був корокий вступ для огляду цієї технології в Spring.

Додатково

Я згадував вище, що SockJS це “бібліотека JavaScript, яка надає абстракцію над різними транспортними механізмами, що підтримують WebSocket.”

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    WebSocketHandlerRegistration registration =
            registry.addHandler(new ExceptionWebSocketHandlerDecorator(handler), "/init");
    registration.setAllowedOriginPatterns("*");
    SockJsServiceRegistration withSockJS = registration.addInterceptors(new HttpSessionHandshakeInterceptor())
            .withSockJS()
            .setTransportHandlers(getDefaultTransportHandlers());
    withSockJS.setSuppressCors(true)
            .setWebSocketEnabled(true)
            .setHttpMessageCacheSize(5000)
            .setHeartbeatTime(25000)
            .setClientLibraryUrl("https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.1/sockjs.min.js");
}

private TransportHandler[] getDefaultTransportHandlers() {
    Set<TransportHandler> result = new LinkedHashSet<>(6);
    result.add(new XhrPollingTransportHandler());
    result.add(new XhrReceivingTransportHandler());
    result.add(new XhrStreamingTransportHandler());
    result.add(new EventSourceTransportHandler());
    result.add(new HtmlFileTransportHandler());
    try {
        result.add(new WebSocketTransportHandler(new DefaultHandshakeHandler()));
    }
    catch (Exception ex) {
        log.warn("Failed to create a default WebSocketTransportHandler", ex);
    }
    TransportHandler[] array = new TransportHandler[result.size()];
    return result.toArray(array);
}

Тому, в конфігурації можемо додати .withSockJs() і передавати додаткові транспортні механізми, які може підтримувати наш сервер.

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Oleksandr Klymenko
Oleksandr Klymenko@overpathz

Java Software Engineer

4.4KПрочитань
1Автори
71Читачі
На Друкарні з 19 квітня

Більше від автора

  • Secure networking. Deep Dive

    Глибоке занурення в протоколи TLS/SSL та інфраструктуру відкритих ключів (PKI). Основні поняття, процес встановлення захищеного з'єднання, роль сертифікатів та ланцюжка довіри

    Теми цього довгочиту:

    Security
  • Поширені помилки у дизайні REST API

    У довгочиті розглядаються поширені помилки при проектуванні REST API та способи їх уникнення: версіонування, використання DTO, підхід CQRS, робота з мікросервісами, та інші практики для підвищення продуктивності, безпеки й зручності API

    Теми цього довгочиту:

    Java
  • Java. Короткий огляд еволюції багатопотоковості

    У перших версіях Java багатопоточність реалізовувалася за допомогою класу Thread, який дозволяв створювати нові потоки. Проте ця модель мала багато недоліків:

    Теми цього довгочиту:

    Java

Вам також сподобається

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Вам також сподобається