WebSocket

WebSocket protokol, popsaný ve specifikaci RFC 6455, poskytuje způsob výměny dat mezi prohlížečem a serverem prostřednictvím trvalého připojení. Data lze předávat oběma směry jako „pakety“, aniž by došlo k přerušení spojení a nutnosti dalších HTTP požadavků.

WebSocket je zvláště skvělý pro služby, které vyžadují nepřetržitou výměnu dat, např. online hry, obchodní systémy v reálném čase a tak dále.

Jednoduchý příklad

Chcete-li otevřít připojení websocket, musíme vytvořit new WebSocket pomocí speciálního protokolu ws v adrese URL:

let socket = new WebSocket("ws://javascript.info");

Je zde také zašifrováno wss:// protokol. Je to jako HTTPS pro webové sokety.

Vždy preferujte wss://

wss:// protokol je nejen šifrovaný, ale také spolehlivější.

To proto, že ws:// data nejsou šifrovaná, viditelná pro každého zprostředkovatele. Staré proxy servery neznají WebSocket, mohou vidět „podivné“ hlavičky a přerušit připojení.

Na druhou stranu wss:// je WebSocket přes TLS (stejně jako HTTPS je HTTP přes TLS), vrstva zabezpečení přenosu šifruje data u odesílatele a dešifruje je u příjemce. Datové pakety jsou tedy předávány šifrovaně přes proxy. Nemohou vidět, co je uvnitř, a nechat je projít.

Jakmile je soket vytvořen, měli bychom na něm poslouchat události. Jsou celkem 4 události:

  • open – spojení navázáno,
  • message – přijatá data,
  • error – chyba websocket,
  • close – spojení uzavřeno.

…A pokud bychom chtěli něco poslat, pak socket.send(data) udělá to.

Zde je příklad:

let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");

socket.onopen = function(e) {
  alert("[open] Connection established");
  alert("Sending to server");
  socket.send("My name is John");
};

socket.onmessage = function(event) {
  alert(`[message] Data received from server: ${event.data}`);
};

socket.onclose = function(event) {
  if (event.wasClean) {
    alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
  } else {
    // e.g. server process killed or network down
    // event.code is usually 1006 in this case
    alert('[close] Connection died');
  }
};

socket.onerror = function(error) {
  alert(`[error] ${error.message}`);
};

Pro účely ukázky je spuštěn malý server server.js napsaný v Node.js, například výše. Odpoví „Ahoj ze serveru, Johne“, poté počká 5 sekund a uzavře spojení.

Uvidíte tedy události openmessageclose .

To je vlastně ono, už můžeme mluvit o WebSocket. Docela jednoduché, že?

Nyní si promluvme více do hloubky.

Otevření webového soketu

Když new WebSocket(url) je vytvořen, okamžitě se začne připojovat.

Během připojení se prohlížeč (pomocí hlaviček) zeptá serveru:"Podporujete Websocket?" A pokud server odpoví „ano“, pak rozhovor pokračuje v protokolu WebSocket, který vůbec není HTTP.

Zde je příklad záhlaví prohlížeče pro požadavek ze strany new WebSocket("wss://javascript.info/chat") .

GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
  • Origin – původ stránky klienta, např. https://javascript.info . Objekty WebSocket jsou přirozeně křížově původem. Neexistují žádná speciální záhlaví ani jiná omezení. Staré servery stejně nedokážou zpracovat WebSocket, takže neexistují žádné problémy s kompatibilitou. Ale Origin hlavička je důležitá, protože umožňuje serveru rozhodnout se, zda s touto webovou stránkou bude komunikovat prostřednictvím WebSocket.
  • Connection: Upgrade – signalizuje, že by klient chtěl protokol změnit.
  • Upgrade: websocket – požadovaný protokol je „websocket“.
  • Sec-WebSocket-Key – náhodný klíč vygenerovaný prohlížečem pro zabezpečení.
  • Sec-WebSocket-Version – Verze protokolu WebSocket, aktuální je 13.
Handshake WebSocket nelze emulovat

Nemůžeme použít XMLHttpRequest nebo fetch vytvořit tento druh požadavku HTTP, protože JavaScript nesmí nastavovat tyto hlavičky.

Pokud server souhlasí s přechodem na WebSocket, měl by odeslat odpověď s kódem 101:

101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=

Zde Sec-WebSocket-Accept je Sec-WebSocket-Key , překódované pomocí speciálního algoritmu. Prohlížeč jej používá, aby se ujistil, že odpověď odpovídá požadavku.

Následně jsou data přenášena pomocí protokolu WebSocket, brzy uvidíme jeho strukturu („rámce“). A to vůbec není HTTP.

Rozšíření a podprotokoly

Mohou existovat další záhlaví Sec-WebSocket-Extensions a Sec-WebSocket-Protocol které popisují rozšíření a podprotokoly.

Například:

  • Sec-WebSocket-Extensions: deflate-frame znamená, že prohlížeč podporuje kompresi dat. Rozšíření je něco, co souvisí s přenosem dat, funkce, která rozšiřuje protokol WebSocket. Záhlaví Sec-WebSocket-Extensions automaticky odesílá prohlížeč se seznamem všech rozšíření, která podporuje.

  • Sec-WebSocket-Protocol: soap, wamp znamená, že bychom rádi přenášeli nejen jakákoli data, ale data v protokolech SOAP nebo WAMP („The WebSocket Application Messaging Protocol“). Podprotokoly WebSocket jsou registrovány v katalogu IANA. Toto záhlaví tedy popisuje formáty dat, které budeme používat.

    Tato volitelná hlavička se nastavuje pomocí druhého parametru new WebSocket . To je řada podprotokolů, např. pokud bychom chtěli použít SOAP nebo WAMP:

    let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);

Server by měl odpovědět seznamem protokolů a rozšíření, s jejichž používáním souhlasí.

Například požadavek:

GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.info
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp

Odpověď:

101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap

Zde server odpoví, že podporuje rozšíření „deflate-frame“ a pouze SOAP požadovaných podprotokolů.

Přenos dat

Komunikace WebSocket se skládá z „rámců“ – datových fragmentů, které lze odesílat z obou stran a mohou být několika druhů:

  • „textové rámce“ – obsahují textová data, která si strany posílají.
  • „binární datové rámce“ – obsahují binární data, která si strany posílají.
  • „ping/pongové rámce“ se používají ke kontrole připojení, odeslané ze serveru, prohlížeč na ně automaticky reaguje.
  • existuje také „rámec uzavření spojení“ a několik dalších rámců služeb.

V prohlížeči přímo pracujeme pouze s textovými nebo binárními rámečky.

WebSocket .send() metoda může odesílat textová nebo binární data.

Volání socket.send(body) umožňuje body v řetězci nebo v binárním formátu, včetně Blob , ArrayBuffer atd. Nejsou vyžadována žádná nastavení:stačí jej odeslat v libovolném formátu.

Když obdržíme data, text vždy přijde jako řetězec. A pro binární data si můžeme vybrat mezi Blob a ArrayBuffer formátů.

To je nastaveno socket.binaryType vlastnost, je to "blob" ve výchozím nastavení, takže binární data přicházejí jako Blob objektů.

Blob je binární objekt na vysoké úrovni, přímo se integruje s <a> , <img> a další značky, takže je to rozumné výchozí nastavení. Ale pro binární zpracování, pro přístup k jednotlivým datovým bajtům, to můžeme změnit na "arraybuffer" :

socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
  // event.data is either a string (if text) or arraybuffer (if binary)
};

Omezení sazby

Představte si, že naše aplikace generuje velké množství dat k odeslání. Ale uživatel má pomalé připojení k síti, možná na mobilním internetu, mimo město.

Můžeme zavolat socket.send(data) znovu a znovu. Data však budou ukládána do vyrovnávací paměti (ukládána) a odesílána pouze tak rychle, jak to rychlost sítě dovolí.

socket.bufferedAmount vlastnost ukládá, kolik bajtů v tuto chvíli zůstává ve vyrovnávací paměti a čeká na odeslání přes síť.

Můžeme jej prozkoumat, abychom zjistili, zda je soket skutečně dostupný pro přenos.

// every 100ms examine the socket and send more data
// only if all the existing data was sent out
setInterval(() => {
  if (socket.bufferedAmount == 0) {
    socket.send(moreData());
  }
}, 100);

Uzavření připojení

Normálně, když chce strana ukončit spojení (prohlížeč i server mají stejná práva), pošle „snímek uzavření spojení“ s číselným kódem a textovým důvodem.

Metoda pro to je:

socket.close([code], [reason]);
  • code je speciální uzavírací kód WebSocket (volitelné)
  • reason je řetězec, který popisuje důvod uzavření (volitelné)

Poté druhá strana v close obsluha události získá kód a důvod, např.:

// closing party:
socket.close(1000, "Work complete");

// the other party
socket.onclose = event => {
  // event.code === 1000
  // event.reason === "Work complete"
  // event.wasClean === true (clean close)
};

Nejběžnější hodnoty kódu:

  • 1000 – výchozí, normální uzavření (používá se, pokud není code dodáno),
  • 1006 – není možné takový kód nastavit ručně, znamená to, že spojení bylo ztraceno (bez uzavření rámce).

Existují další kódy jako:

  • 1001 – večírek odchází, např. server se vypíná nebo prohlížeč opustí stránku,
  • 1009 – zpráva je příliš velká na zpracování,
  • 1011 – neočekávaná chyba na serveru,
  • …a tak dále.

Úplný seznam lze nalézt v RFC6455, §7.4.1.

Kódy WebSocket jsou trochu podobné kódům HTTP, ale liší se. Zejména kódy nižší než 1000 jsou rezervovány, pokud se pokusíme nastavit takový kód, dojde k chybě.

// in case connection is broken
socket.onclose = event => {
  // event.code === 1006
  // event.reason === ""
  // event.wasClean === false (no closing frame)
};

Stav připojení

Chcete-li zjistit stav připojení, je zde navíc socket.readyState vlastnost s hodnotami:

  • 0 – „CONNECTING“:připojení ještě nebylo navázáno,
  • 1 – „OPEN“:komunikující,
  • 2 – “ZAVÍRÁNÍ”:spojení se uzavírá,
  • 3 – „UZAVŘENO“:připojení je uzavřeno.

Příklad chatu

Podívejme se na příklad chatu pomocí rozhraní WebSocket API prohlížeče a modulu Node.js WebSocket https://github.com/websockets/ws. Hlavní pozornost budeme věnovat straně klienta, ale server je také jednoduchý.

HTML:potřebujeme <form> pro odesílání zpráv a <div> pro příchozí zprávy:

<!-- message form -->
<form name="publish">
  <input type="text" name="message">
  <input type="submit" value="Send">
</form>

<!-- div with messages -->
<div id="messages"></div>

Od JavaScriptu chceme tři věci:

  1. Otevřete připojení.
  2. Při odeslání formuláře – socket.send(message) pro zprávu.
  3. U příchozí zprávy – připojte ji k div#messages .

Zde je kód:

let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");

// send message from the form
document.forms.publish.onsubmit = function() {
  let outgoingMessage = this.message.value;

  socket.send(outgoingMessage);
  return false;
};

// message received - show the message in div#messages
socket.onmessage = function(event) {
  let message = event.data;

  let messageElem = document.createElement('div');
  messageElem.textContent = message;
  document.getElementById('messages').prepend(messageElem);
}

Kód na straně serveru je trochu mimo náš rozsah. Zde použijeme Node.js, ale nemusíte. Jiné platformy mají také své prostředky pro práci s WebSocket.

Algoritmus na straně serveru bude:

  1. Vytvořte clients = new Set() – sada zásuvek.
  2. Pro každý přijatý websocket jej přidejte do sady clients.add(socket) a nastavte message posluchač události, aby získal jeho zprávy.
  3. Když obdržíte zprávu:iterujte klienty a odešlete ji všem.
  4. Když je spojení uzavřeno:clients.delete(socket) .
const ws = new require('ws');
const wss = new ws.Server({noServer: true});

const clients = new Set();

http.createServer((req, res) => {
  // here we only handle websocket connections
  // in real project we'd have some other code here to handle non-websocket requests
  wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);
});

function onSocketConnect(ws) {
  clients.add(ws);

  ws.on('message', function(message) {
    message = message.slice(0, 50); // max message length will be 50

    for(let client of clients) {
      client.send(message);
    }
  });

  ws.on('close', function() {
    clients.delete(ws);
  });
}

Zde je pracovní příklad:

Můžete si jej také stáhnout (pravé horní tlačítko v prvku iframe) a spustit lokálně. Jen si nezapomeňte nainstalovat Node.js a npm install ws před spuštěním.

Shrnutí

WebSocket je moderní způsob trvalého připojení mezi prohlížečem a serverem.

  • WebSockets nemají omezení mezi různými zdroji.
  • V prohlížečích jsou dobře podporovány.
  • Umí odesílat/přijímat řetězce a binární data.

API je jednoduché.

Metody:

  • socket.send(data) ,
  • socket.close([code], [reason]) .

Události:

  • open ,
  • message ,
  • error ,
  • close .

WebSocket sám o sobě nezahrnuje opětovné připojení, ověřování a mnoho dalších mechanismů na vysoké úrovni. Na to tedy existují knihovny klient/server a je také možné tyto funkce implementovat ručně.

Někdy za účelem integrace WebSocket do existujících projektů lidé provozují server WebSocket paralelně s hlavním HTTP serverem a sdílejí jedinou databázi. Požadavky na WebSocket používají wss://ws.site.com , subdoména, která vede na server WebSocket, zatímco https://site.com přejde na hlavní HTTP server.

Jistě jsou možné i jiné způsoby integrace.


No