# WebSocket

## Vad det är

WebSocket är ett protokoll som gör en enda HTTP-förfrågan till en anslutning som
båda parter kan skriva till när som helst, och som hålls öppen tills någon av
dem stänger den. IETF publicerade det som RFC 6455 i december 2011, och den del
som en webbläsare gör tillgänglig för skript, objektet `WebSocket`, är
definierad i WHATWG:s WebSockets Standard. I kod och dokumentation stavas namnet
också ofta ”websocket” eller ”web socket”, och pluralformen WebSockets brukar
syfta på tekniken som helhet.

Anslutningen börjar som en helt vanlig `GET` över HTTP/1.1. Klienten lägger till
`Upgrade: websocket` och `Connection: Upgrade`, en `Sec-WebSocket-Version` på 13
och en `Sec-WebSocket-Key`, som är ett slumpmässigt värde på 16 byte kodat i
base64. En server som säger ja svarar med statuskoden 101 Switching Protocols
och huvudfältet `Sec-WebSocket-Accept`. Värdet bevisar att servern har läst just
den här handskakningen och inte spelar upp ett gammalt svar: det är klientens
nyckel med ett fast GUID tillagt, hashat med SHA-1 och kodat i base64. Från och
med då är det slut med HTTP på anslutningen, och parterna skickar ramar i
stället.

En ram är liten. Ramhuvudet är minst två byte, växer med två eller åtta när
innehållet är längre än 125 byte, och med ytterligare fyra på ramar från
klienten, som bär en maskeringsnyckel. Varje ram har en opcode: text, som måste
vara giltig UTF-8, binärdata eller fortsättning när ett meddelande är uppdelat
på flera ramar, och därtill tre kontrollramar för stängning, ping och pong.
Varje ram som en klient skickar maskeras med en ny slumpmässig 32-bitarsnyckel,
och en server maskerar aldrig. Maskering är inte kryptering. Den finns för att
ett fientligt skript inte ska kunna välja exakt vilka byte som korsar nätverket
och använda dem för att förgifta cachen i en proxy någonstans längs vägen.

Adresserna börjar med `ws://` eller `wss://`, med port 80 och port 443 som
standard, och `wss` kör hela utbytet inuti TLS. Det är samma portar som HTTP
redan använder, och det är avsiktligt: för brandväggar och proxyer längs vägen
ser handskakningen ut som vilken annan webbförfrågan som helst.

## Varför det spelar roll

HTTP är förfrågan och svar, så servern talar bara när den blir tilltalad. Många
produkter behöver det motsatta: ett chattmeddelande, ett pris som just har
ändrats, en markör i ett delat dokument, ett bakgrundsjobb som har blivit klart.
Utan en kanal som servern kan skriva till måste klienten polla. Den frågar med
några sekunders mellanrum, de flesta svaren säger bara att ingenting har
ändrats, varje svar bär en full uppsättning huvudfält, och en nyhet väntar upp
till ett helt intervall innan någon ser den. En öppen WebSocket-anslutning tar
bort båda kostnaderna. Servern skriver i samma ögonblick som något händer, och
meddelandet färdas med några få byte ramdata i stället för en hel sida
huvudfält, så latensen hålls nere på det som nätverket självt lägger till.

Priset är att anslutningen bär tillstånd. En HTTP-förfrågan kan hamna på vilken
server som helst bakom en lastbalanserare, medan en WebSocket-anslutning hör
till en enda process så länge den lever. Den processen binder minne för varje
öppen anslutning, en driftsättning eller omstart bryter alla på en gång, och
varje proxy och lastbalanserare längs vägen måste släppa igenom uppgraderingen
och tåla en anslutning som kanske är tyst i flera minuter. Kör man mer än en
server uppstår en fråga som vanlig HTTP aldrig ställer: när något händer på
server A, hur når det fram till en klient vars anslutning lever på server B? Det
vanliga svaret är en gemensam publish/subscribe-kanal som alla servrar lyssnar
på, och det är ännu ett system att hålla i drift.

WebSocket är alltså ett medvetet val, inte en utgångspunkt. När bara servern har
något att berätta kan server-sent events, som definieras i HTML-standarden,
skicka händelser över ett vanligt HTTP-svar av typen `text/event-stream`, och
webbläsaren ansluter på nytt av sig själv när svaret bryts. WebSocket gör skäl
för sina extra rörliga delar när klienten har lika mycket att säga som servern,
som i chatt, spel och samtidig redigering av samma dokument.

## Så ser det ut i praktiken

Utbytet nedan öppnar en chattanslutning. Nyckeln är exempelvärdet från själva
RFC 6455, och värdet i `Sec-WebSocket-Accept` är det som standarden själv visar
och som varje korrekt server måste skicka tillbaka för just den nyckeln.

```http
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 tomma raden som avslutar serverns svar är ingenting på anslutningen
HTTP längre. En webbläsare sköter hela handskakningen bakom ett enda anrop till
konstruktorn och lämnar resten åt händelselyssnare:

```js
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);
});
```

Stängningshändelsen berättar hur anslutningen slutade. Kod 1000 är en normal
stängning, och 1001 betyder att ena parten går därifrån, till exempel en server
som stängs av eller en webbläsare som lämnar sidan. Kod 1006 betyder att
anslutningen försvann helt utan någon stängningsram. Ingen av parterna får
skicka den; den finns bara för att programmet ovanpå ska kunna skilja ett
onormalt slut från ett rent.

Gränsen mellan förfrågan och svar å ena sidan och en öppen socket å den andra
syns tydligt i ett projekt i webbplatsens portfölj. När ett API flyttades över
till Huma v2,
[gick de befintliga rutterna över till Humas modell med anrop och svar, utom exakt två](/sv/portfolio/migrated-the-api-to-huma-v2-at-100-parity/):
slutpunkterna för WebSocket blev kvar på ren Fiber, eftersom koden där verkligen
vill åt själva socketen och Humas modell inte passar. Ett ramverk som är byggt
kring HTTP:s egen form beskriver och validerar förfrågningar och svar väl, och
en långlivad tvåvägsanslutning är helt enkelt något annat.

## Var det går fel

**En sida på en annan webbplats öppnar en anslutning med dina besökares kakor.**
Webbläsaren kör handskakningen som en förfrågan till serverns värd och skickar
med värdens kakor, oavsett var skriptet som bad om anslutningen kom ifrån. En
server som autentiserar med kakor och aldrig tittar på `Origin` tar därför emot
en anslutning som öppnats av vilken sida som helst på webben, inloggad som den
som råkar besöka den. RFC 6455 säger att en server som bara är avsedd för vissa
webbplatser bör kontrollera `Origin` mot dem den väntar sig och avvisa resten
med 403 Forbidden. Gör det i handskakningen, före uppgraderingen.

**En åtkomsttoken hamnar i loggarna.** Webbläsarens `WebSocket`-konstruktor tar
en URL och en valfri lista med subprotokoll och inget annat, så ett skript kan
inte lägga till ett `Authorization`-fält. Den snabba genvägen är en token i
URL:en, där en omvänd proxy och åtkomstloggar skriver ned den. RFC:n lämnar
autentiseringen öppen och nämner kakor, HTTP-autentisering och TLS-autentisering
som möjligheter. En kaka tillsammans med `Origin`-kontrollen ovan fungerar, och
det gör även en kortlivad engångsbiljett som sidan hämtar via ett autentiserat
HTTP-anrop och använder i handskakningen.

**En omstart gör alla klienter till en flodvåg.** En driftsättning stänger alla
anslutningar på en gång, varje klient rapporterar kod 1006, och om alla ansluter
igen direkt och fortsätter försöka kommer servern upp till något som RFC:n
liknar vid en överbelastningsattack. RFC 6455 ber klienterna vänta en
slumpmässig tid före första försöket, föreslår något mellan 0 och 5 sekunder och
ber dem sedan vänta längre efter varje misslyckande, till exempel med avkortad
binär exponentiell backoff. En server som stänger sina anslutningar med 1001
innan den stoppar talar åtminstone om för klienterna att avbrottet var planerat.

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

**Anslutningar dör utan att någon märker det.** En anslutning utan trafik ger
ingen av parterna något tecken på att vägen fortfarande finns, och en motpart
som försvann utan en stängningsram ser precis ut som en tyst anslutning ända
tills nästa skrivning misslyckas. Det är det ping och pong är till för: en
slutpunkt måste svara på varje ping med en pong, och RFC:n nämner att en ping
kan fungera som keepalive eller som en kontroll av att andra sidan fortfarande
svarar. Webbläsarens API har inget sätt att skicka en ping, så timern hör hemma
på servern: pinga varje anslutning med ett fast intervall och stäng dem som inte
svarar med pong.

**Ett meddelande äter upp serverns minne.** Längdfältet i ett ramhuvud ger plats
för innehåll som är långt större än någon server har minne till, och RFC:n
varnar för att en fientlig motpart kan skicka en enda enorm ram eller en ändlös
rad små fragment. Implementationer måste skydda sig själva, så sätt en största
tillåtna meddelandestorlek i serverbiblioteket och stäng med kod 1009, message
too big, när en motpart går över den. Samma eftersläpning kan byggas upp åt
andra hållet. I webbläsaren visar `bufferedAmount` hur många byte från `send()`
som fortfarande väntar på att komma ut på nätverket, och WHATWG:s exempel
skickar nästa uppdatering först när talet är noll. På servern fyller en klient
som läser långsamt den utgående bufferten på samma sätt, så varje anslutning
behöver ett tak för den och en regel för vad som ska hända när taket är nått.

## Läs mer

- [RFC 6455: The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455) —
  handskakningen, ramarna, maskeringen och stängningskoderna, och
  säkerhetsresonemanget bakom var och en av dem.
- [WebSockets Standard](https://websockets.spec.whatwg.org/) — webbläsarens API:
  konstruktorn, `send()`, `close()`, `bufferedAmount` och stängningshändelsen.
- [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110) —
  huvudfältet `Upgrade` och statuskoden 101 Switching Protocols, som
  handskakningen bygger på.
- [Server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html)
  — HTML-standardens envägsalternativ med inbyggd återanslutning.

<https://engineer.company/sv/glossary/websocket/>
