frontend

WebSocket: ce este, când îl folosești și alternativele moderne

WebSocket deschide un canal bidirecțional persistent peste TCP. Când îl alegi față de SSE sau polling, și capcanele operaționale reale.

Cuprins

WebSocket este un protocol de comunicare bidirecțional peste TCP, standardizat în RFC 6455, care menține conexiunea deschisă pentru schimb de mesaje asincron între client și server. Sesiunea începe cu o cerere HTTP obișnuită care cere un upgrade la WebSocket; după ce serverul acceptă, HTTP-ul dispare și rămâne un canal TCP persistent, cu latență mică în ambele direcții.

Față de modelul clasic cerere-răspuns HTTP, WebSocket elimină costul unui handshake la fiecare mesaj. Față de Server-Sent Events, adaugă direcția client-server. Această asimetrie explică de ce WebSocket nu este răspunsul universal la „am nevoie de ceva în timp real": pentru un subset important de cazuri, o alternativă mai simplă funcționează mai bine.

Ce este un WebSocket mai exact?

Un canal de comunicare full-duplex peste o singură conexiune TCP, deschisă printr-un mecanism de upgrade HTTP. Schimbul de deschidere arată astfel: clientul trimite un GET cu headerele Upgrade: websocket și Connection: Upgrade, iar serverul răspunde cu 101 Switching Protocols. De la acel punct, conexiunea TCP subiacentă rămâne deschisă și ambele capete pot trimite frames (cadre) în orice moment, fără să mai aștepte o cerere.

Câteva proprietăți care contează în practică:

  • Bidirecțional și persistent. Ambele capete pot trimite mesaje în orice moment, fără să aștepte o cerere. Conexiunea rămâne deschisă până când una din părți o închide explicit.
  • Latență mică. Fără handshake la fiecare mesaj, latența per mesaj este ordinul milisecundelor.
  • Date binare și text. Suportă frames UTF-8 și binare, potrivit atât pentru JSON cât și pentru fluxuri de date compacte.
  • Fără CORS. Politica de origine se verifică manual pe server via headerul Origin.

Cum diferă WebSocket de HTTP polling și Server-Sent Events?

Trei abordări pentru comunicare în timp real, cu compromisuri diferite:

HTTP polling înseamnă că browserul trimite periodic cereri HTTP pentru a verifica dacă există date noi. Modelul este simplu, dar are un cost fix per cerere: overhead TCP, TLS, headers HTTP, procesare pe server. La un interval de o secundă și o mie de clienți, asta înseamnă o mie de cereri pe secundă indiferent dacă există date noi sau nu. Long polling ține cererea deschisă până când serverul are ceva de trimis, dar nu elimină reluarea handshake-ului la fiecare ciclu.

Server-Sent Events (SSE) ține o conexiune HTTP deschisă și împinge date de la server la client. Este unidirecțional, funcționează pe HTTP/1.1 și HTTP/2, trece prin proxy-uri fără configurare suplimentară și se reconectează automat cu reluarea de la ultimul event ID primit. Pentru feed-uri de observabilitate, notificări și actualizări de stare, SSE acoperă nevoia cu mult mai puțin overhead operațional decât WebSocket.

WebSocket este alegerea corectă când ambele direcții sunt la fel de importante: chat, editare colaborativă, jocuri multiplayer, terminale web interactive. Când fluxul este majoritar server-client, SSE câștigă prin simplitate.

Care sunt capcanele operaționale ale WebSocket?

Conexiunile persistente introduc o clasă de probleme care nu există în modelul HTTP clasic cerere-răspuns:

  • Absența reconnect logic-ului. O conexiune WebSocket cade la orice întrerupere de rețea, restart de server sau timeout de proxy. Fără reconnect logic cu exponential backoff și jitter, clienții rămân deconectați definitiv. Prea mulți clienți care se reconectează simultan după un restart provoacă thundering herd; componenta aleatoare din backoff distribuie în timp reîncercările.
  • Scaling orizontal fără coordonare. Când rulezi mai multe instanțe ale serverului, clientul conectat la instanța A nu primește mesajele destinate lui de pe instanța B. Soluțiile sunt sticky sessions (simplu, dar fragil la cădere de instanță) sau un broker pub-sub central (Redis Pub/Sub, NATS, Kafka) care distribuie mesajele între instanțe. Fără una din ele, o topologie cu N instanțe înseamnă N insule de clienți izolați.
  • Proxy-uri neinformate. Nginx, HAProxy și majority CDN-urilor nu fac upgrade la WebSocket implicit. Headerele Upgrade și Connection: upgrade trebuie configurate explicit în blocul location al API gateway-ului, altfel conexiunea se închide după primul răspuns HTTP. Adaugă și un timeout generos pe conexiune (proxy_read_timeout 3600) pentru a preveni închiderea silențioasă a conexiunilor inactive.
  • Lipsa rate-limiting per conexiune. Rate-limiting-ul la nivel WebSocket este mai complex decât cel HTTP: nu există headerul standard 429 Too Many Requests în protocol. Limitarea trebuie implementată la nivel aplicație, cu închiderea conexiunii și un mesaj de eroare custom, sau la nivel de conexiuni simultane per IP la intrarea în gateway.
  • Starea conexiunii nu este vizibilă prin monitoring standard. Conexiunile WebSocket persistente nu generează un log per mesaj implicit; trebuie instrumentare explicită pentru a ști câte conexiuni sunt active și câte mesaje pe secundă trec. Fără această instrumentare, observabilitatea stack-ului tău are o gaură.

Care sunt alternativele moderne la WebSocket?

Trei alternative cu cazuri de utilizare clare:

  • SSE (Server-Sent Events). API browser nativ (EventSource), strict unidirecțional (server-client), funcționează pe orice versiune HTTP, trece prin proxy-uri fără configurare specială și se reconectează automat. Alegerea corectă pentru notificări, feed-uri de actualizări și feed-uri de observabilitate. Dezavantaj: nu poate trimite date de la client la server; trebuie combinat cu cereri HTTP clasice dacă ai nevoie și de direcția inversă.
  • WebTransport. API browser pe HTTP/3 cu QUIC, latență mai mică decât WebSocket (QUIC elimină head-of-line blocking de la TCP), fluxuri concurente pe aceeași conexiune. Alegerea pentru aplicații cu cerințe extreme: jocuri, video conferencing, telemetrie densă. Suportul browser este limitat la Chrome și Firefox recent; Safari și infrastructura de server au suport fragmentat.
  • gRPC streaming. Comunicare internă între servicii cu schemă strictă pe HTTP/2, cu Protocol Buffers și generare de cod din .proto. Nu funcționează în browser direct (necesită gRPC-Web sau un proxy). Vezi și comparația REST vs GraphQL pentru contextul alegerii unui protocol API.

Când nu îți trebuie real-time?

Înainte de a alege orice protocol de comunicare persistentă, merită un test simplu: cât de proaspete trebuie să fie datele pentru ca utilizatorul să nu observe diferența? Dacă răspunsul este „la 10 secunde e suficient", polling la 10 secunde este o soluție validă și mult mai simplă de operat decât o conexiune persistentă.

Câteva scenarii unde real-time nu este necesar, chiar dacă pare intuitiv că ar fi:

  • Dashboard-uri de business cu granularitate de minute. Un dashboard de vânzări care se actualizează la fiecare minut nu are nevoie de WebSocket; polling la 60 de secunde sau SSE cu interval mare acoperă nevoia.
  • Notificări care nu necesită livrare imediată. Push notifications prin serviciile native ale platformei (APNs, FCM) sunt adesea mai fiabile decât o conexiune WebSocket menținută în fundal și nu necesită infrastructură de server persistent.
  • Actualizări de stare pentru procese batch. Un raport generat în 30 de secunde poate fi verificat prin polling simplu la 5 secunde. O coadă de mesaje cu webhook HTTP este mai robustă dacă procesele sunt frecvente.

Regula practică: alege cel mai simplu mecanism care satisface cerința de freshness a datelor. WebSocket adaugă valoare reală numai când direcția client-server este la fel de critică ca direcția server-client și când latența sub secundă este necesară. Instrumentarea conexiunilor active, a mesajelor pe secundă și a erorilor de reconectare este parte din practica de site reliability engineering. La autentificare, un JWT cu expirare scurtă în momentul upgrade-ului asigură că sesiunile WebSocket persistente nu supraviețuiesc revocării.

Întrebări frecvente

WebSocket sau SSE, care să aleg pentru notificări în timp real?

SSE pentru fluxuri unidirecționale (server către client), WebSocket când clientul trimite și el date frecvent. Notificări, feed-uri live, actualizări de preț: SSE e mai simplu de implementat, funcționează pe HTTP standard, trece prin orice proxy și se recomectează automat. Alege WebSocket când latența de la client la server contează la fel de mult ca latența de la server la client (chat, gaming, editare colaborativă).

WebSocket funcționează prin nginx?

Da, dar cere configurare explicită. Nginx nu face upgrade la WebSocket implicit. Trebuie să adaugi în blocul location: proxy_http_version 1.1;, proxy_set_header Upgrade $http_upgrade;, proxy_set_header Connection "upgrade";. Fără aceste linii, proxy-ul închide conexiunea după primul răspuns HTTP.

Cum scalez WebSocket orizontal pe mai multe instanțe?

Cu sticky sessions sau cu un broker pub-sub central (Redis Pub/Sub, NATS, Kafka). Fără unul din cele două, un mesaj trimis de client pe instanța A nu ajunge la alți clienți conectați pe instanța B. Sticky sessions sunt mai simple dar creează dependență de o singură instanță; brokerul pub-sub este mai robust dar adaugă o componentă de infrastructură în plus.

HTTP/2 și HTTP/3 suportă WebSocket nativ?

HTTP/2 nu suportă upgrade-ul clasic WebSocket; există RFC 8441 pentru WebSocket over HTTP/2 dar suportul e limitat. HTTP/3 (QUIC) nu suportă WebSocket deloc. Dacă ai clienți pe HTTP/2 sau HTTP/3, alternativele recomandate sunt SSE (funcționează pe orice versiune HTTP) sau WebTransport (construit nativ pe HTTP/3 cu QUIC).

Ce face reconnect logic-ul și de ce contează?

Reconnect logic-ul reconectează automat clientul după o deconectare, cu pauze progresive între încercări. Fără el, o deconectare temporară (restart server, schimbare de rețea) pierde conexiunea definitiv. Implementarea corectă folosește exponential backoff cu jitter: prima reîncercare după 1s, apoi 2s, 4s, 8s, cu un maxim de 30–60s și o componentă aleatoare pentru a preveni thundering herd când sute de clienți se reconectează simultan după un restart.