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.
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 open
→ message
→ close
.
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. AleOrigin
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.
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:
- Otevřete připojení.
- Při odeslání formuláře –
socket.send(message)
pro zprávu. - 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:
- Vytvořte
clients = new Set()
– sada zásuvek. - Pro každý přijatý websocket jej přidejte do sady
clients.add(socket)
a nastavtemessage
posluchač události, aby získal jeho zprávy. - Když obdržíte zprávu:iterujte klienty a odešlete ji všem.
- 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.