HTTP у контексті WebSockets
HTTP (HyperText Transfer Protocol) є основним протоколом для передачі веб-даних через мережу. Зазвичай, HTTP використовує модель "запит-відповідь", де клієнт відправляє запит і чекає відповіді від сервера. Це може бути неефективно для додатків, що вимагають високої інтерактивності та обміну даними в режимі реального часу. Тут на допомогу приходять WebSockets.
Що таке вебсокет з’єднання?
WebSockets дозволяють створювати двосторонні з’єднання між клієнтом (наприклад, веб-браузером) і сервером. Таке з'єднання дозволяє даним вільно переміщуватися в обидва боки без потреби в постійному перевстановленні зв’язку, що є характерним для HTTP. Вебсокети використовують вже існуюче TCP з’єднання, яке залишається відкритим протягом усієї сесії.
Вебсокет з технічної/мережевої точки зору
Технічно, 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() і передавати додаткові транспортні механізми, які може підтримувати наш сервер.