Konrad Kowalski (rootsher)Principal Platform & Reliability Architect101110111001110100100100110010111001010011100100

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:

text
client -> request
server -> stream of responses/events

WebSocket zmienia abstrakcję.

Po zestawieniu połączenia obie strony mogą wysyłać wiadomości niezależnie:

text
client <-> server

Nie ma już relacji:

text
one request -> one response

Zaczynamy od handshake

Klasyczny handshake WebSocket wykorzystuje HTTP upgrade:

http
GET /socket HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13

Serwer akceptuje:

http
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:

js
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:

text
connection
message framing
text/binary payloads
ping/pong at protocol level
close handshake

Semantykę wiadomości definiujemy sami.

Connection jest stanem

W klasycznym HTTP:

text
request 1 -> instance A
request 2 -> instance C
request 3 -> instance B

Przy WebSocket:

text
client ───────── instance A
       persistent connection

To zmienia architekturę infrastruktury.

Jeżeli event powstaje na instance B, ale klient siedzi na A:

text
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:

text
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:

text
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:

text
application protocol
       |
       v
WebSocket protocol
       |
       v
transport

Socket.IO:

text
application events
       |
       v
Socket.IO protocol
       |
       v
Engine.IO
       |
       v
WebSocket / HTTP long-polling

Socket.IO daje wyższą warstwę:

js
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:

text
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:

text
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.