Spring til indhold

WebSocket

en protokol til en tovejsforbindelse, der forbliver åben

  • Sentosa beach, singapore, rope bridge
    Foto: Arulonline / Pixabay
  • Crossing, river, rope
    Foto: It_was_a_pleasure / Pixabay

Brug begrebet i en beslutning

Brug »Hvad er WebSocket?« i en konkret beslutning. Vi kan knytte begrebet til det system, du bygger eller køber. Den del, brugerne aldrig ser: hvor data ligger, hvilke regler der gælder, og hvordan andre systemer må tale med den.

Se vores ydelser Kontakt os

Undersøg antagelserne bag ordet

Brug forklaringen og det tilknyttede arbejde til at formulere spørgsmål til dit projekt. Vi kan undersøge antagelserne sammen, før et værktøj vælges. Har designet og drevet 6 ETL/ELT‑pipelines — 6 ETL‑pipelines på tre databasemotorer

Har designet og drevet 6 ETL/ELT‑pipelines Kontakt os

Hvad det er

WebSocket er en protokol, der gør en enkelt HTTP-forespørgsel til en forbindelse, som begge parter kan skrive på når som helst, og som forbliver åben, indtil en af dem lukker den. IETF udgav den som RFC 6455 i december 2011, og den del, en browser stiller til rådighed for scripts, objektet WebSocket, er defineret i WHATWG’s WebSockets Standard. I kode og dokumentation ser man også navnet stavet »websocket« eller »web socket«, og flertalsformen WebSockets bruges som regel om teknologien som helhed.

Forbindelsen begynder som en helt almindelig GET over HTTP/1.1. Klienten tilføjer Upgrade: websocket og Connection: Upgrade, en Sec-WebSocket-Version på 13 og en Sec-WebSocket-Key, som er en tilfældig værdi på 16 byte kodet i base64. En server, der siger ja, svarer med statuskoden 101 Switching Protocols og headerfeltet Sec-WebSocket-Accept. Værdien beviser, at serveren har læst netop dette håndtryk og ikke genbruger et gammelt svar: den er klientens nøgle med et fast GUID hængt på, hashet med SHA-1 og kodet i base64. Fra det øjeblik er HTTP færdig på forbindelsen, og parterne udveksler rammer i stedet.

En ramme er lille. Headeren fylder mindst to byte, vokser med to eller otte, når indholdet er længere end 125 byte, og med yderligere fire på rammer fra klienten, som bærer en maskeringsnøgle. Hver ramme har en opcode: tekst, der skal være gyldig UTF-8, binær, eller fortsættelse, når en besked er delt over flere rammer, og dertil tre kontrolrammer til lukning, ping og pong. Hver ramme, en klient sender, er maskeret med en ny tilfældig 32-bit-nøgle, og en server maskerer aldrig. Maskering er ikke kryptering. Den findes, så et fjendtligt script ikke kan vælge præcis de byte, der krydser netværket, og bruge dem til at forgifte en cachende proxy et sted på vejen.

Adresser begynder med ws:// eller wss://, med port 80 og port 443 som standard, og wss kører hele udvekslingen inde i TLS. Det er de porte, HTTP allerede bruger, og det er med vilje: håndtrykket ligner enhver anden webforespørgsel for de firewalls og proxyer, det kommer igennem.

Hvorfor det betyder noget

HTTP er forespørgsel og svar, så serveren taler kun, når den bliver spurgt. Mange produkter har brug for det modsatte: en chatbesked, en pris, der lige har ændret sig, en markør i et delt dokument, et baggrundsjob, der er blevet færdigt. Uden en kanal, serveren kan skrive på, må klienten polle. Den spørger med få sekunders mellemrum, de fleste svar siger blot, at intet er ændret, hvert svar bærer et helt sæt headerfelter, og en nyhed venter op til et helt interval, før nogen ser den. En åben WebSocket-forbindelse fjerner begge omkostninger. Serveren skriver i samme øjeblik, noget sker, og beskeden rejser med et par byte til rammen i stedet for en side fuld af headerfelter, så latenstiden holdes nede på det, netværket selv lægger til.

Prisen er, at forbindelsen bærer tilstand. En HTTP-forespørgsel kan lande på en hvilken som helst server bag en load balancer, mens en WebSocket-forbindelse hører til én proces, så længe den lever. Den proces holder hukommelse for hver åben forbindelse, en udrulning eller genstart kapper dem alle på én gang, og hver proxy og load balancer på vejen skal lade opgraderingen passere og finde sig i en forbindelse, der måske er stille i flere minutter. Kører man mere end én server, opstår et spørgsmål, almindelig HTTP aldrig stiller: når noget sker på server A, hvordan når det så frem til en klient, hvis forbindelse lever på server B? Det sædvanlige svar er en fælles publish/subscribe-kanal, som alle serverne lytter på, og det er endnu et system at drive.

WebSocket er altså noget, man vælger til, ikke noget, man starter med. Når kun serveren har noget at fortælle, kan server-sent events, som er defineret i HTML-standarden, skubbe hændelser over et almindeligt HTTP-svar af typen text/event-stream, og browseren forbinder selv igen, når svaret bliver afbrudt. WebSocket gør sig fortjent til sine ekstra bevægelige dele, når klienten har lige så meget at sige som serveren, som i chat, spil og samtidig redigering af det samme dokument.

Sådan ser det ud i praksis

Udvekslingen nedenfor åbner en chatforbindelse. Nøglen er eksempelværdien fra selve RFC 6455, og accept-værdien er den, standarden selv viser, og som enhver korrekt server skal sende tilbage for netop den nøgle.

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

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Efter den tomme linje, der afslutter serverens svar, er intet på forbindelsen HTTP længere. En browser klarer hele håndtrykket bag ét kald til konstruktøren og overlader resten til hændelseslyttere:

const socket = new WebSocket("wss://example.com/chat");
socket.addEventListener("open", () => socket.send("hej"));
socket.addEventListener("message", (event) => console.log(event.data));
socket.addEventListener("close", (event) => {
  console.log(event.code, event.reason, event.wasClean);
});

Lukkehændelsen fortæller, hvordan forbindelsen sluttede. Kode 1000 er en normal lukning, og 1001 betyder, at den ene part går sin vej, for eksempel en server, der lukker ned, eller en browser, der forlader siden. Kode 1006 betyder, at forbindelsen forsvandt helt uden en lukkeramme. Intet endpoint må sende den; den findes kun, så programmet ovenpå kan skelne en unormal afslutning fra en ren.

Grænsen mellem forespørgsel og svar på den ene side og en åben socket på den anden ses tydeligt i et projekt i sitets portefølje. Da et API flyttede over på Huma v2, kom alle ruter over på Humas model med forespørgsel og svar, undtagen præcis to: WebSocket-endpointene blev på rå Fiber, fordi koden dér reelt har brug for selve socketten, og fordi Humas model ikke passer. Et framework, der er bygget op om HTTP’s egen form, beskriver og validerer forespørgsler og svar godt, og en langlivet tovejsforbindelse er simpelthen noget andet.

Hvor det går galt

En side på et andet site åbner en forbindelse med dine besøgendes cookies. Browseren kører håndtrykket som en forespørgsel til serverens vært og sender den værts cookies med, uanset hvor scriptet, der bad om forbindelsen, kom fra. En server, der autentificerer med cookies og aldrig kigger på Origin, tager derfor imod en forbindelse åbnet af en hvilken som helst side på nettet, logget ind som den, der tilfældigvis besøger den. RFC 6455 siger, at en server, der kun er beregnet til bestemte sites, bør tjekke Origin mod dem, den forventer, og afvise resten med 403 Forbidden. Gør det i håndtrykket, før opgraderingen.

Adgangstokenet ender i loggene. Browserens WebSocket-konstruktør tager en URL og en valgfri liste af subprotokoller og intet andet, så et script kan ikke tilføje et Authorization-felt. Den hurtige omvej er et token i URL’en, hvor reverse proxyer og adgangslogge skriver det ned. RFC’en lader autentificering stå åben og nævner cookies, HTTP-autentificering og TLS-autentificering som muligheder. En cookie kombineret med Origin-tjekket ovenfor virker, og det gør en kortlivet engangsbillet også, som siden henter over et autentificeret HTTP-kald og bruger på håndtrykket.

En genstart gør alle klienter til en stormflod. En udrulning lukker alle forbindelser på én gang, hver klient melder kode 1006, og hvis de alle sammen forbinder igen med det samme og bliver ved, kommer serveren op til noget, som RFC’en sammenligner med et denial-of-service-angreb. RFC 6455 beder klienter vente et tilfældigt stykke tid før første forsøg, foreslår noget mellem 0 og 5 sekunder og beder dem derefter vente længere efter hver fejl, for eksempel med afkortet binær eksponentiel backoff. En server, der lukker sine forbindelser med 1001, før den stopper, fortæller i det mindste klienterne, at afbrydelsen var planlagt.

function reconnectDelay(attempt) {
  if (attempt === 0) return Math.random() * 5000;
  const ceiling = Math.min(60_000, 1000 * 2 ** attempt);
  return Math.random() * ceiling;
}

Forbindelser dør, uden at nogen opdager det. En forbindelse uden trafik giver ingen af parterne tegn på, at vejen stadig findes, og en modpart, der forsvandt uden en lukkeramme, ligner præcis en stille forbindelse, indtil næste skrivning fejler. Det er det, ping og pong er til: et endpoint skal besvare hvert ping med et pong, og RFC’en nævner, at et ping kan tjene som keepalive eller som et tjek af, at den anden side stadig svarer. Browserens API har ingen måde at sende et ping på, så timeren hører hjemme på serveren: ping hver forbindelse med et fast interval, og luk dem, der ikke svarer med pong.

Én besked æder serverens hukommelse. Længdefeltet i en rammes header giver plads til langt større indhold, end nogen server har hukommelse til, og RFC’en advarer om, at en fjendtlig modpart kan sende én enorm ramme eller en endeløs strøm af små fragmenter. Implementeringer skal beskytte sig selv, så sæt en maksimal beskedstørrelse i serverbiblioteket, og luk med kode 1009, message too big, når en modpart går over den. Det samme efterslæb kan hobe sig op den anden vej. I browseren fortæller bufferedAmount, hvor mange byte fra send() der stadig venter på at komme ud på netværket, og WHATWG’s eksempel sender først den næste opdatering, når tallet er nul. På serveren fylder en klient, der læser langsomt, den udgående buffer på samme måde, så hver forbindelse har brug for et loft over den og en regel for, hvad der sker, når loftet er nået.

Læs mere

Ordliste

Vælg ud fra dine faktiske vilkår

Medbring arbejdsgangen, de eksisterende grænseflader og de brugere, der har brug for ændringen. Vi kan definere en testbar leverance og en overdragelse, teamet kan vedligeholde.

Se, hvad det koster Kontakt os

Gør begreberne praktiske

Spørg, hvordan »Hvad er WebSocket?« gælder i din organisation. Et konkret eksempel fra dit nuværende system er nok til at begynde samtalen.

Se vores ydelser
welcome@engineer.company +45 50 10 61 84 København, Danmark
QR-kode til WebSocket
Tag denne side med dighttps://engineer.company/da/glossary/websocket/