WebSockets: trwały kanał komunikacji w obu kierunkach
- data
- kategoria
- Backend
- także w
- Frontend · Networking
- czytanie
- 2 min / 355 słów
SSE nadal zachowuje model:
client -> request
server -> stream of responses/events
WebSocket zmienia abstrakcję.
Po zestawieniu połączenia obie strony mogą wysyłać wiadomości niezależnie:
client <-> server
Nie ma już relacji:
one request -> one response
Zaczynamy od handshake
Klasyczny handshake WebSocket wykorzystuje HTTP upgrade:
GET /socket HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13
Serwer akceptuje:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ...
Od tego momentu komunikacja nie składa się z HTTP requestów i response'ów.
Połączenie przenosi WebSocket frames.
Frames, nie endpointy
Browser API jest proste:
const socket =
new WebSocket("wss://example.com/socket");
socket.onmessage = event => {
console.log(event.data);
};
socket.send(JSON.stringify({
type: "cursor.move",
x: 120,
y: 80
}));
WebSocket nie wie, czym jest cursor.move.
To protokół aplikacji.
Native WebSocket daje nam przede wszystkim:
connection
message framing
text/binary payloads
ping/pong at protocol level
close handshake
Semantykę wiadomości definiujemy sami.
Connection jest stanem
W klasycznym HTTP:
request 1 -> instance A
request 2 -> instance C
request 3 -> instance B
Przy WebSocket:
client ───────── instance A
persistent connection
To zmienia architekturę infrastruktury.
Jeżeli event powstaje na instance B, ale klient siedzi na A:
instance B
|
v
shared pub/sub
|
v
instance A
|
v
client
Pojawia się potrzeba warstwy fan-out:
- Redis Pub/Sub,
- NATS,
- Kafka,
- broker aplikacyjny,
- własny connection registry.
Sam WebSocket nie rozwiązuje dystrybucji eventów pomiędzy backend instances.
Deploy przestaje być tylko restartem procesu
Jeżeli instancja utrzymuje 50 000 aktywnych connections, jej wyłączenie oznacza 50 000 reconnectów.
Potrzebujemy:
stop accepting new connections
|
v
connection draining
|
v
clients reconnect elsewhere
|
v
terminate old instance
Do tego dochodzą:
- load balancer idle timeout,
- heartbeat,
- reconnect backoff,
- connection storms,
- state restoration.
Persistent connection jest funkcją aplikacji i operacji.
Backpressure
Klient może wysyłać lub odbierać dane wolniej niż druga strona je produkuje.
W browser API WebSocket istnieje m.in. bufferedAmount, pokazujące liczbę bajtów zakolejkowanych do wysłania.
Jeżeli:
producer rate > network/consumer rate
kolejka rośnie.
Aplikacja musi zdecydować:
- czekać,
- agregować wiadomości,
- odrzucać stare update'y,
- zamknąć connection.
W realtime często „latest state wins” jest lepszą semantyką niż niezawodne dostarczenie każdej historycznej zmiany.
WebSocket to nie Socket.IO
To szczególnie ważne.
Native WebSocket:
application protocol
|
v
WebSocket protocol
|
v
transport
Socket.IO:
application events
|
v
Socket.IO protocol
|
v
Engine.IO
|
v
WebSocket / HTTP long-polling
Socket.IO daje wyższą warstwę:
socket.emit("order.updated", order);
socket.on("order.updated", handler);
oraz mechanizmy takie jak:
- reconnect,
- acknowledgements,
- rooms,
- namespaces,
- broadcasting,
- fallback transport.
Jeżeli WebSocket nie jest dostępny, Socket.IO może korzystać z HTTP long-polling.
To oznacza, że:
Socket.IO client
!=
plain WebSocket client
Nie są wire-compatible tylko dlatego, że Socket.IO może użyć WebSocketu jako transportu.
Porównanie „Socket.IO vs WebSocket” miesza więc dwie warstwy.
Lepsze pytanie brzmi:
czy potrzebuję niskopoziomowego kanału wiadomości, czy gotowego protokołu/frameworka realtime?
Czy WebSocket jest końcem historii?
WebSocket daje jeden niezawodny, uporządkowany kanał komunikacji.
Ale nie każdy realtime potrzebuje:
reliable + ordered + every message
Jeżeli wysyłamy pozycję gracza 60 razy na sekundę, retransmisja starej pozycji może być mniej użyteczna niż dostarczenie najnowszej.
Potrzebujemy czasem wielu niezależnych streamów oraz danych, które można zgubić.
Tu zaczyna się WebTransport.