Protokol WebSocket, popsaný ve specifikaci RFC 6455, poskytuje způsob výmÄny dat mezi prohlÞeÄem a serverem pomocà trvalého spojenÃ. Data mohou být posÃlána obÄma smÄry jako âpaketyâ bez pÅeruÅ¡enà spojenà a bez nutnosti posÃlat dalšà HTTP požadavky.
WebSocket je obzvláštÄ vhodný 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 podobnÄ.
Jednoduchý pÅÃklad
Abychom otevÅeli websocketové spojenÃ, musÃme vytvoÅit new WebSocket se speciálnÃm protokolem ws v URL:
let socket = new WebSocket("ws://javascript.info");
Existuje i Å¡ifrovaný protokol wss://. Je to nÄco jako HTTPS pro websockety.
wss://Protokol wss:// je nejen Å¡ifrovaný, ale i spolehlivÄjÅ¡Ã.
Je to proto, že data ve ws:// nejsou zaÅ¡ifrovaná, a tedy jsou viditelná pro vÅ¡echny prostÅednÃky. Staré proxy servery neznajà WebSocket, proto mohou spatÅit âpodivnéâ hlaviÄky a ukonÄit spojenÃ.
Na druhou stranu wss:// je WebSocket nad TLS (stejnÄ jako HTTPS je HTTP nad TLS). TLS (transport security layer â pÅenosová zabezpeÄovacà vrstva) zaÅ¡ifruje data u odesÃlatele a deÅ¡ifruje je u pÅÃjemce. Datové pakety se tedy pÅes proxy pÅenášejà zaÅ¡ifrované, takže proxy nevidà jejich obsah a nechá je projÃt.
Když je socket vytvoÅen, můžeme naslouchat jeho událostem. Tyto události jsou celkem ÄtyÅi:
openâ spojenà vytvoÅeno,messageâ data pÅijata,errorâ chyba websocketu,closeâ spojenà uzavÅeno.
â¦A když chceme nÄco odeslat, sloužà k tomu socket.send(data).
Zde je pÅÃklad:
let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");
socket.onopen = function(e) {
alert("[open] Spojenà vytvoÅeno");
alert("PosÃlám na server");
socket.send("My name is John");
};
socket.onmessage = function(událost) {
alert(`[message] PÅijata data ze serveru: ${událost.data}`);
};
socket.onclose = function(událost) {
if (událost.wasClean) {
alert(`[close] Spojenà ÄistÄ uzavÅeno, kód=${událost.code} důvod=${událost.reason}`);
} else {
// napÅ. proces serveru je zastaven nebo spadne sÃÅ¥
// událost.code je v takovém pÅÃpadÄ obvykle 1006
alert('[close] Spojenà ztraceno');
}
};
socket.onerror = function(chyba) {
alert(`[error]`);
};
Pro demonstraÄnà úÄely je pro uvedený pÅÃklad spuÅ¡tÄn malý server server.js, napsaný v Node.js. Odpovà âHello from server, Johnâ, pak poÄká 5 sekund a uzavÅe spojenÃ.
UvidÃte tedy události open â message â close.
To je v zásadÄ vÅ¡echno, nynà můžeme komunikovat WebSocketem. Docela jednoduché, že?
Nynà si o tom promluvme podrobnÄji.
OtevÅenà websocketu
Když je vytvoÅen new WebSocket(url), zaÄne se okamžitÄ pÅipojovat.
PÅi spojenà se prohlÞeÄ (pomocà hlaviÄek) zeptá serveru: âPodporujeÅ¡ WebSocket?â A pokud server odpovà âanoâ, pak hovor pokraÄuje v protokolu WebSocket, což je nÄco úplnÄ jiného než HTTP.
Následuje pÅÃklad hlaviÄek prohlÞeÄe v požadavku, který byl vytvoÅen volánÃm 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 klientské stránky, napÅ.https://javascript.info. Objekty WebSocketu jsou ze své povahy jiného původu. Nejsou zde žádné speciálnà hlaviÄky ani jiná omezenÃ. Staré servery stejnÄ nedokážà WebSocket zpracovávat, takže problémy s kompatibilitou se nevyskytnou. Důležitá je vÅ¡ak hlaviÄkaOrigin, která umožÅuje serveru rozhodnout se, zda bude s tÃmto webovým sÃdlem komunikovat WebSocketem nebo ne.Connection: Upgradeâ signalizuje, že klient chce zmÄnit protokol.Upgrade: websocketâ požadovaný protokol je âwebsocketâ.Sec-WebSocket-Keyâ náhodný klÃÄ generovaný prohlÞeÄem, použÃvaný k ujiÅ¡tÄnÃ, že server podporuje protokol WebSocket. Je náhodný, aby si proxy servery následnou komunikaci neukládaly do mezipamÄti.Sec-WebSocket-Versionâ verze protokolu WebSocket, aktuálnà je 13.
HTTP požadavek tohoto druhu nemůžeme vytvoÅit pomocà XMLHttpRequest nebo fetch, protože JavaScript nemá dovoleno tyto hlaviÄky nastavovat.
Jestliže server souhlasà s pÅepnutÃm na WebSocket, mÄl by poslat 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ý speciálnÃm algoritmem. Když ho prohlÞeÄ uvidÃ, pozná, že server skuteÄnÄ podporuje protokol WebSocket.
Poté jsou data pÅenášena protokolem WebSocket. Jeho strukturu (ârámceâ) brzy uvidÃme. A ten nemá nic spoleÄného s HTTP.
RozÅ¡ÃÅenà a subprotokoly
Požadavek může obsahovat dalšà hlaviÄky Sec-WebSocket-Extensions a Sec-WebSocket-Protocol, které popisujà rozÅ¡ÃÅenà a subprotokoly.
NapÅÃklad:
-
Sec-WebSocket-Extensions: deflate-frameznamená, že prohlÞeÄ podporuje kompresi dat. RozÅ¡ÃÅenà je nÄco, co má nÄjaký vztah k pÅenosu dat, funkcionalita, která rozÅ¡iÅuje protokol WebSocket. ProhlÞeÄ automaticky posÃlá hlaviÄkuSec-WebSocket-Extensionsse seznamem vÅ¡ech rozÅ¡ÃÅenÃ, která podporuje. -
Sec-WebSocket-Protocol: soap, wampznamená, že nechceme posÃlat jen tak nÄjaká data, ale data v protokolech SOAP nebo WAMP (âThe WebSocket Application Messaging Protocolâ â Websocketový protokol pro zprávy aplikacÃ). Subprotokoly WebSocketu jsou registrovány v katalogu IANA. Tato hlaviÄka tedy popisuje formáty dat, které se chystáme použÃt.Tato nepovinná hlaviÄka se nastavuje druhým parametrem konstruktoru
new WebSocket, který obsahuje pole subprotokolů, napÅ. když chceme 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Ã.
PÅÃklad požadavku:
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Ãdá, že podporuje rozÅ¡ÃÅenà deflate-frame a z požadovaných subprotokolů jedinÄ SOAP.
PÅenos dat
Komunikace WebSocketem se skládá z ârámcůâ â fragmentů dat, které mohou posÃlat obÄ strany a které mohou být nÄkolika druhů:
- âtextové rámceâ â obsahujà textová data, která si strany navzájem posÃlajÃ.
- âbinárnà rámceâ â obsahujà binárnà data, která si strany navzájem posÃlajÃ.
- âping-pongové rámceâ se použÃvajà ke kontrole spojenÃ, posÃlá je server a prohlÞeÄ na nÄ automaticky odpovÃdá.
- existujà i âuzavÃracà rámceâ a nÄkolik dalÅ¡Ãch servisnÃch rámců.
V prohlÞeÄi pÅÃmo pracujeme jen s textovými a binárnÃmi rámci.
Metoda WebSocket .send() umà poslat textová i binárnà data.
Volánà socket.send(tÄlo) umožÅuje, aby tÄlo byl ÅetÄzec nebo binárnà formát, napÅ. Blob, ArrayBuffer atd. Nenà tÅeba nic nastavovat: prostÄ poÅ¡le data v jakémkoli formátu.
Když pÅijÃmáme data, textová data pÅicházejà vždy jako ÅetÄzec. U binárnÃch dat si můžeme vybrat mezi formáty Blob a ArrayBuffer.
To se nastavuje vlastnostà socket.binaryType. StandardnÄ je "blob", takže binárnà data pÅicházejà jako objekty Blob.
Blob je binárnà objekt vysoké úrovnÄ. Je pÅÃmo integrován s <a>, <img> a jinými znaÄkami, proto je to rozumný standard. Pro binárnà zpracovánà a pÅÃstup k jednotlivým bytům dat vÅ¡ak můžeme zmÄnit formát na "arraybuffer":
socket.binaryType = "arraybuffer";
socket.onmessage = (událost) => {
// událost.data je buÄ ÅetÄzec (u textových dat), nebo arraybuffer (u binárnÃch dat)
};
Omezenà rychlosti
PÅedstavme si, že naÅ¡e aplikace generuje velké množstvà dat k odesÃlánÃ, ale uživatel má pomalé sÃÅ¥ové pÅipojenÃ, tÅeba mobilnà internet mimo mÄsto.
Můžeme volat socket.send(data) znovu a znovu. Data se vÅ¡ak budou ukládat do bufferu (vyrovnávacà pamÄti) a posÃlat jen tak rychle, jak rychlost sÃtÄ dovolÃ.
Vlastnost socket.bufferedAmount sdÄluje, kolik bytů zůstává v této chvÃli v bufferu a Äeká na poslánà po sÃti.
JejÃm prozkoumánÃm můžeme zjistit, zda je socket právÄ dostupný pro pÅenos dat.
// každých 100 ms prozkoumáme socket a dalšà data pošleme
// teprve tehdy, až budou vÅ¡echna existujÃcà data odeslána
setInterval(() => {
if (socket.bufferedAmount == 0) {
socket.send(dalÅ¡ÃData());
}
}, 100);
UzavÅenà spojenÃ
BÄžnÄ, když chce nÄkterá strana uzavÅÃt spojenà (prohlÞeÄ i server majà stejná práva), poÅ¡le âuzavÃracà rámecâ s ÄÃselným kódem a textovým odůvodnÄnÃm.
K tomu sloužà metoda:
socket.close([kód], [důvod]);
kódje speciálnà WebSocketový uzavÃracà kód (nepovinný)důvodje ÅetÄzec, který popisuje důvod uzavÅenà (nepovinný)
Pak druhá strana obdržà tento kód a důvod v handleru události close, napÅÃklad:
// uzavÃracà strana:
socket.close(1000, "Práce hotova");
// druhá strana
socket.onclose = událost => {
// událost.code === 1000
// událost.reason === "Práce hotova"
// událost.wasClean === true (Äisté uzavÅenÃ)
};
NejÄastÄjšà hodnoty kódu:
1000â standardnÃ, bÄžné uzavÅenà (použije se, nenÃ-likóduveden),1006â tento kód nelze nastavit ruÄnÄ, oznamuje, že spojenà bylo ztraceno (bez uzavÃracÃho rámce).
Existujà i jiné kódy, napÅÃklad:
1001â strana odcházÃ, napÅ. server se vypÃná nebo prohlÞeÄ opouÅ¡tà stránku,1009â zpráva je pro zpracovánà pÅÃliÅ¡ velká,1011â neoÄekávaná chyba na serveru,- â¦a podobnÄ.
Celý seznam najdete v RFC6455, §7.4.1.
Kódy WebSocketu se trochu podobajà kódům HTTP, ale jejich hodnoty jsou jiné. NapÅÃklad kódy nižšà než 1000 jsou rezervovány a pokud se takový kód pokusÃme nastavit, nastane chyba.
// v pÅÃpadÄ pÅeruÅ¡enà spojenÃ
socket.onclose = událost => {
// událost.code === 1006
// událost.reason === ""
// událost.wasClean === false (nebyl uzavÃracà rámec)
};
Stav spojenÃ
K zÃskánà stavu spojenà sloužà vlastnost socket.readyState s tÄmito hodnotami:
0â âCONNECTINGâ: spojenà jeÅ¡tÄ nebylo vytvoÅeno,1â âOPENâ: komunikace probÃhá,2â âCLOSINGâ: spojenà se uzavÃrá,3â âCLOSEDâ: spojenà je uzavÅeno.
PÅÃklad chatu
PÅepracujme pÅÃklad chatu s použitÃm prohlÞeÄového WebSocketového API a WebSocketového modulu https://github.com/websockets/ws pro Node.js. Pozornost budeme vÄnovat pÅevážnÄ stranÄ klienta, ale server je také jednoduchý.
HTML: potÅebujeme <form> k posÃlánà zpráv a <div> pro pÅicházejÃcà zprávy:
<!-- formuláŠpro zprávy -->
<form name="odesÃlánÃ">
<input type="text" name="zpráva">
<input type="submit" value="Odeslat">
</form>
<!-- div se zprávami -->
<div id="zprávy"></div>
Od JavaScriptu chceme tÅi vÄci:
- OtevÅÃt spojenÃ.
- PÅi odeslánà formuláÅe zavolat
socket.send(zpráva)s odesÃlanou zprávou. - PÅi pÅÃchodu zprávy ji pÅipojit k
div#zprávy.
Zde je kód:
let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");
// poslánà zprávy z formuláÅe
document.forms.publish.onsubmit = function() {
let odesÃlanáZpráva = this.zpráva.value;
socket.send(odesÃlanáZpráva);
return false;
};
// zpráva pÅijata - zobrazÃme ji v div#zprávy
socket.onmessage = function(událost) {
let zpráva = událost.data;
let elementZprávy = document.createElement('div');
elementZprávy.textContent = zpráva;
document.getElementById('zprávy').prepend(elementZprávy);
}
Kód na stranÄ serveru je ponÄkud mimo náš rámec. Zde použÃváme Node.js, ale to nenà nutné. I jiné platformy majà své způsoby práce s WebSocketem.
Algoritmus na stranÄ serveru bude:
- VytvoÅÃme
klienti = new Set()â množina socketů. - Každý pÅijatý websocket pÅidáme do množiny
klienti.add(socket)a nastavÃme posluchaÄe událostimessage, aby pÅijÃmal jeho zprávy. - Když je zpráva pÅijata: iterujeme nad klienty a poÅ¡leme ji každému z nich.
- Když je spojenà uzavÅeno:
klienti.delete(socket).
const ws = new require('ws');
const wss = new ws.Server({noServer: true});
const klienti = new Set();
http.createServer((požadavek, odpovÄÄ) => {
// zde zpracováváme jen websocketová spojenÃ
// ve skuteÄném projektu bychom zde mÄli i jiný kód, který by zpracovával jiné než websocketové požadavky
wss.handleUpgrade(požadavek, požadavek.socket, Buffer.alloc(0), onSocketConnect);
});
function onSocketConnect(ws) {
klienti.add(ws);
ws.on('message', function(zpráva) {
zpráva = zpráva.slice(0, 50); // maximálnà délka zprávy bude 50
for(let klient of klienti) {
klient.send(zpráva);
}
});
ws.on('close', function() {
klienti.delete(ws);
});
}
Zde je funkÄnà pÅÃklad:
Můžete si jej také stáhnout (tlaÄÃtko vpravo nahoÅe ve vnitÅnÃm rámu) a spustit si jej lokálnÄ. Jen si nezapomeÅte nainstalovat Node.js a pÅed spuÅ¡tÄnÃm pÅÃkladu spustit npm install ws.
ShrnutÃ
WebSocket je modernà způsob, jak zÃskat stálé spojenà prohlÞeÄe se serverem.
- WebSockety nemajà omezenà jiného původu.
- V prohlÞeÄÃch jsou Å¡iroce podporovány.
- Mohou odesÃlat i pÅijÃmat ÅetÄzce a binárnà data.
Jejich API je jednoduché.
Metody:
socket.send(data),socket.close([kód], [důvod]).
Události:
open,message,error,close.
WebSocket sám o sobÄ neobsahuje obnovu spojenÃ, autentifikaci a mnoho dalÅ¡Ãch mechanismů vysoké úrovnÄ. K tomu existujà knihovny pro klienta i pro server a můžeme si také tyto schopnosti implementovat ruÄnÄ.
Aby lidé integrovali WebSocket do existujÃcÃch projektů, nÄkdy spouÅ¡tÄjà WebSocketový server paralelnÄ s hlavnÃm HTTP serverem a oba servery pÅistupujà do stejné databáze. Požadavky na WebSocket použÃvajà wss://ws.site.com, subdoménu vedoucà na WebSocketový server, zatÃmco https://site.com vede na hlavnà HTTP server.
SamozÅejmÄ jsou možné i jiné způsoby integrace.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)