Hlavní navigace

Komunikace webové aplikace se serverem pomocí Ajax a SSE

3. 11. 2022
Doba čtení: 18 minut

Sdílet

 Autor: Depositphotos
Web už dlouho nejsou jen statické stránky odkazující na další stránky. Webová stránka dnes může být dynamická, může se měnit se podle toho, kde kdo na ni klikne a může komunikovat s okolním světem v reálném čase.
Co se dozvíte v článku
  1. Na začátek trocha historie
  2. Jak funguje protokol HTTP
  3. Stručné seznámení s Ajaxem
  4. Přichází SSE – Server-Sent Events
  5. Pokračování příště

Na začátek trocha historie

První www stránka spatřila světlo světa ve švýcarském CERNu v roce 1990. Tehdy obsahovala pouze text a odkazy na další stránky. Poměrně brzy přibyly obrázky a JavaScript a tím inovace na dlouhou dobu skončily. Komunikace se serverem probíhala stále podle stejného schématu: otevřít spojení, poslat dotaz, dostat odpověď a zavřít spojení. Po celá devadesátá léta byly webové prohlížeče doslova prolezlé různými vzájemně nekompatibilními pluginy, které doplňovaly chybějící funkce. Nejslavnější byl dodnes zatracovaný Adobe Flash Player. Umožňoval animovat stránky a komunikovat s okolním světem bez nutnosti nového načtení stránky.

Ani JavaScript nebyl v té době žádná sláva. Každý tvůrce prohlížeče měl trochu jiný standard více či méně kompatibilní s oficiálním standardem. Program napsaný pro jeden prohlížeč neběžel v jiném prohlížeči a manuál JavaScriptu měl u každé funkce poznámku typu „tato funkce funguje pouze v Internet Exploreru 4“, „tahle jenom v Netscape 5“ a podobně. Celkový výsledek byl takový, že půlka webových stránek fungovala správně pouze v tom jednom správném prohlížeči, který shodou okolností neexistoval ve verzi pro Linux.

V roce 1999, kdy už se situace s JavaScriptem začala trochu stabilizovat, se objevila technologie AJAX – Asynchronous Javascript and XML (Asynchronní Javascript a XML). Tohle byla naprosto přelomová technologie webových prohlížečů. Tato technologie umožňuje JavaScriptu, běžícímu ve webové stránce, komunikovat s okolním světem bez toho, aby se stránka opustila nebo znovu načetla a vykreslila. Způsob komunikace se serverem je přesně stejný jako při stahování samotné stránky, takže na straně serveru není potřeba žádná úprava. To umožnilo realizaci úplně nového typu webové stránky – tzv. „Single Page App“, kdy je celá aplikace v jediné webové stránce. Právě od této doby tu máme online mapy, online e-mailové klienty a podobně.

Ovšem ani technologie AJAX není všespasitelná. Umožňuje velmi jednoduše poslat na server data z prohlížeče pomocí požadavku POST nebo načíst již existující data pomocí GET. Ovšem pokud má webová stránka čekat na nějaká data od serveru, která budou k dispozici v nepředvídatelném okamžiku, musí se to dělat komplikovaně a s velkou zátěží pro linku i pro server. Na pomoc se ovšem muselo čekat dalších přibližně sedm let.

Až v roce se 2006 objevila technologie SSE – „Server-sent events“. Ta je velmi podobná standardním požadavkům typu GET, ovšem na rozdíl od něj se spojení nezavře okamžitě po poslání odpovědi, ale zůstane otevřeno po celou dobu, dokud je otevřená stránka nebo dokud ho program nezavře. Server může tímto kanálem kdykoliv poslat další data v okamžiku, kdy budou k dispozici. Ovšem tato linka je pouze jednosměrná – ve směru ze serveru do prohlížeče. Pokud potřebujeme komunikovat obousměrně, musíme kombinovat vysílání pomocí POST a příjem pomocí SSE.

Přibližně o čtyři roky později, v roce 2011, byla ve standardu RFC 6455 definována technologie WebSocket. Od roku 2015 je podporovaná všemi hlavními prohlížeči, takže dnes je již univerzálně použitelná. Umožňuje otevřít socket na libovolný server (nejenom na server, ze kterého byla stažená stránka) a držet jej otevřený, dokud se nezavře stránka. Socket je obousměrný, komunikace je full-duplex, to znamená, že data mohou být posílána současně oběma směry s minimální režií.

Tato technologie umožnila vznik webových aplikací běžících v prohlížeči, které komunikují v reálném čase, jako například chat. WebSocket je postaven na TCP, takže je zaručeno doručení paketů ve správném pořadí a bez výpadků. Ze stejného důvodu se ale příliš nehodí pro přenos dat v reálném čase –  rádia, telefonní hovory nebo video hovory po webu.

Proto se přibližně ve stejné době, t.j. od roku 2010 začala vyvíjet technologie WebRTC. Použitelná je přibližně od roku 2018, ovšem dodnes existují rozdíly v implementacích napříč prohlížeči. Je založena na protokolu UDP a současně obsahuje kodeky pro různé formáty audia a videa. Teprve s ní mohly přijít videokonference v prohlížeči. Ovšem trochu jsme se tady nostalgicky vrátili do devadesátých let minulého století, protože mnoho těchto webových aplikací funguje třeba pouze v prohlížeči Chrome, ale už ne ve Firefoxu. V jednom se ale situace změnila – žádná z těchto aplikací už nevyžaduje Internet Explorer.

Jak funguje protokol HTTP

Protokol HTTP je základem celého World Wide Webu. Načítají se jím stránky, obrázky, javascriptové programy, CSS. Prostě jakýkoliv soubor, který je třeba dopravit ze serveru do prohlížeče ve fázi načítání stránky, je dopraven právě tímto protokolem. Existuje i protokol HTTPS, ale tím se tady a teď nebudeme zabývat. Je to v zásadě HTTP zabezpečený proti odposlouchávání a změně odesílatele. HTTP je původní varianta a pro potřeby tohoto článku bude dostatečný a podstatně srozumitelnější.

Komunikace protokolem HTTP je vždy spuštěna klientem – prohlížečem. Klient otevře TCP spojení na server a pošle požadavek – nejčastěji GET. Součástí požadavku je několik parametrů, které definují například formát dat, které klient akceptuje, adresa odesílatele, ale především URL adresa stránky, kterou klient požaduje. Tato adresa obsahuje jméno souboru, který má server dodat, případné parametry a jméno virtuálního serveru. Hlavička obsahuje i ty nešťastné sušenky (cookies), kvůli kterým teď musíme na každé stránce odklepávat, že souhlasíme se vším. Ovšem ony mohou obsahovat i velmi důležité informace pro chod stránky nebo aplikace. Celá hlavička končí prázdným řádkem. Řádky jsou (zcela nepochopitelně) oddělovány dvojicí znaků CR a LF (\r\n).

Server najde příslušný soubor. Pokud je to skript, předá mu parametry a převezme jeho výstup, jinak vezme přímo obsah souboru a výsledek pošle jako odpověď klientovi. Odpověď začíná hlavičkou, která obsahuje především status operace – například klasické „200 OK“ nebo kód chyby, například klasické „404 Not Found“. Dále je v ní informace o formátu dat a případné kompresi. Hlavička končí prázdným řádkem, za nímž následují data. Po vyslání odpovědi server ukončí spojení.

Příklad komunikace HTTP. Nejprve si ukážeme klientův dotaz, poté následuje odpověď serveru:

GET /ciao.html HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: jwt=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJqZXRAc3BpbnRlYy5jb20iLCJuYW1lIjoiSmV0IiwiYXYiOm51bGwsInR6IjoiRXVyb3BlL1JvbWUiLCJsYyI6ImVuIiwiZGYiOiJERC9NTS9ZWVlZIiwiYXAiOiIiLCJwZXJtaXNzaW9ucyI6WyJtYW5hZ2U6c3lzdGVtIl0sImdyb3VwcyI6WzFdLCJpYXQiOjE2Mzk1NTkyNDIsImV4cCI6MTYzOTU2MTA0MiwiYXVkIjoidXJuOndpa2kuanMiLCJpc3MiOiJ1cm46d2lraS5qcyJ9.lFFa04izhdHt9FvY9LWGpEceEEH1kjjTw4NWteF7rrAK8kEnduyDOJjhDVKiXGHv-XRz9bteR3gRVMHeUOeTvkpnw4oKHLBDIj3Pmy30wkxdzi08G3gE7ZavG6h9BpLoGmgQ0rtjqWwtZV11DzUTek7W_tsUXX2a--23_EGidgmHE31IVcyhOjIsN8jeQu1Hzsf9gPm9K0I6ZB9FE1-bVhF8-n90KZ35LaU9pBwHrIbK9x-LUQswHFYq3ab32gRb5BxdfcfAU_Sv67RPZ6chs2H3QVr8SOh2pTsA-mBoNKrl7qxtHmIo55YFPUMs8KO02UIzq2Wyfe40HHZCD0Ps_w
Upgrade-Insecure-Requests: 1
DNT: 1
Sec-GPC: 1
If-Modified-Since: Tue, 05 Jul 2022 09:58:21 GMT
If-None-Match: "21-5e30be484a8fd"
HTTP/1.1 200 OK
Date: Tue, 05 Jul 2022 09:59:24 GMT
Server: Apache/2.4.29 (Ubuntu)
Last-Modified: Tue, 05 Jul 2022 09:59:15 GMT
ETag: "20-5e30be7bfb996"
Accept-Ranges: bytes
Content-Length: 32
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

<html><body>ciao</body></html>

Na to, že jsme potřebovali vyžádat stránku, která má dohromady 32 znaků, je to šílená režie. Samozřejmě, v případě mnohakilobajtové stránky se tato režie ztratí, ovšem pro typy komunikací o kterých budeme mluvit v dalších kapitolách je tato režie obrovská. A to jsme ještě neviděli všechno. Toto byla jen a pouze data – jeden řetězec tam, druhý zpátky. Ale na úrovni TCP se přitom vyměnilo dokonce 10 paketů.

Autor: Josef Pavlík

První tři pakety jsou sestavení TCP spojení. Klient kontaktuje server, server potvrdí, klient potvrdí, že dostal potvrzení. Následující paket je již text dotazu. Na jeho začátku je 66 bajtů hlavičky TCP, pak je samotný text dotazu, který jsme již viděli v předchozím bloku. Další paket je potvrzení příjmu od serveru a hned poté následuje jeho odpověď. I tato odpověď má na začátku 66 bajtů hlavičky TCP, pak teprve následuje text odpovědi, kterou jsme viděli na předchozím výpisu. Ovšem text samotné odpovědi = obsah stránky je pouze poslední řádek, pouhých 32 bajtů. Všechno ostatní byly jen informace o formátu, o serveru a samozřejmě i status (OK, chyba, přesměrování a podobně). Poslední čtyři pakety už jsou jenom potvrzení o přijetí odpovědi a uzavření spojení.

Abychom dostali 32 bajtů odpovědi, po lince prošlo dohromady 2135 bajtů v 10 paketech. Účinnost je přibližně 1,5%. Abych byl upřímný, dost mě to překvapilo. Nikdy dřív jsem to nepočítal a nepředstavoval jsem si, že dojdu k takovému výsledku. Je pravda, že je to hlavně kvůli té cookie, která se tam vzala kdoví kdy a kdoví odkud a je pěkně dlouhá. To jenom dokazuje, že prohlížeče jsou skutečně neuvěřitelně zaneřáděné a nikdy nevíte, co a kam od vás odchází.

Stručné seznámení s Ajaxem

Jak jsme si již řekli v úvodu, dokud nepřišel Ajax, webová stránka mohla být dynamická v tom smyslu, že JavaScript běžící ve stránce měl omezené možnosti, jak ji modifikovat za běhu. Ovšem nebyl žádný rozumný způsob, jak dostat nová data zvenku bez nového načtení stránky. Pravděpodobně to bylo možné nějak obejít pomocí jiné stránky schované někde v iframe, ale to byly spíš eskamotáže. Ajax toto omezení navždy poslal na smetiště dějin.

Už jsme si řekli, že Ajax je zkratka z Asynchronous Javascript and XML. Slovo asynchronní je velmi důležité. V praxi znamená, že po celou dobu komunikace stránka „nevytuhne“, je stále aktivní, dá se klikat na tlačítka a vidět příslušné reakce, přitom v pozadí prohlížeč komunikuje. Naopak slovo XML v této zkratce ani ve jméně třídy JavaScriptu nemá v podstatě žádný význam. Komunikace není vázaná na tento formát dat, v praxi se v drtivé většině případů používá JSON.

Celá komunikace začne vyvoláním metody send ve třídě XMLHttpRequest. Prohlížeč pošle standardní HTTP dotaz typu GET nebo POST. V okamžiku, kdy celá komunikace skončí, se zavolá funkce, která byla zaregistrovaná jako callback. Mezitím je JavaScript volný, může obsluhovat další události, které vznikly ve stránce nebo může současně komunikovat s okolním světem přes další požadavky  XMLHttpRequest.

Na server přijde standardní požadavek HTTP, takže jakýkoliv server, který je schopen dodávat běžné stránky, může stejným způsobem dodávat i stránky nebo data vyžádaná přes Ajax.

Příklad použití třídy XMLHttpRequest:

// Initialize the HTTP request.
let xhr = new XMLHttpRequest();
// define the request
xhr.open('GET', 'send-ajax-data.php');

// Track the state changes of the request.
xhr.onreadystatechange = function () {
        const DONE = 4; // readyState 4 means the request is done.
        const OK = 200; // status 200 is a successful return.
        if (xhr.readyState === DONE) {
                if (xhr.status === OK) {
                        console.log(xhr.responseText); // 'This is the output.'
                } else {
                        console.log('Error: ' + xhr.status); // An error occurred during the request.
                }
        }
};

// Send the request to send-ajax-data.php
xhr.send(null);

Třída XMLHttpRequest je mocný nástroj, ale řekněme si to upřímně, je dost neohrabaný. Proto se v prohlížečích mezi lety 2015 a 2017 objevila funkce fetch, která velmi zjednodušuje použití Ajaxu. Vnitřně stále používá XMLHttpRequest, funkčnost je přesně stejná, ale program, který měl předtím kolem patnácti řádků, má teď pouze tři.

Příklad stejného programu s použitím funkce fetch:

fetch('send-ajax-data.php')
    .then(data => console.log(data))
    .catch (error => console.log('Error:' + error));

Tento konkrétní program pošle na server požadavek na načtení stránky send-ajax-data.php. Odpověď, kterou dostane, potom vypíše na konzoli prohlížeče. Pro provoz na lince platí stejný protokol, který jsme viděli v minulé kapitole. Ovšem v tomto případě bývá režie velmi významná, protože se Ajaxem většinou stahují poměrně malá množství dat.

Ajax má jedno velké a nepříjemné omezení, uměle přidané kvůli bezpečnosti. Ajax se může spojit pouze se serverem, ze kterého byla stránka původně načtena. Při pokusu o spojení na jiný server prohlížeč zablokuje tento požadavek a spojení skončí s chybou. Tento mechanismus se jmenuje CORS = Cross-Origin Resource Sharing. Volně přeloženo „křížení míst sdílení zdrojů“. V určitých případech je možné tento mechanismus zablokovat a povolit přistup i na jiný server, než ze kterého pochází stránka, ale mně osobně se to nikdy nepodařilo.

Nakonec se ještě zmíním o způsobu, jak se dá Ajaxem velmi neefektivně realizovat příjem dat, která přicházejí asynchronně (neočekávaně). Představme si modelový případ. Na dveřích od místnosti je spínač, kterým sever detekuje otevření dveří. Hlídač má otevřenou webovou stránku, která indikuje stav dveří. JavaScript běžící ve stránce periodicky posílá na server požadavky HTTP typu GET. Samozřejmě nemůže chrlit požadavky jeden za druhým jako kulomet a dostávat jednu negativní odpověď za druhou, aby odchytil událost, která se stane jednou za hodinu. Tím by zahltil linku a neúměrně by zatěžoval server. Abychom se této situaci vyhnuli, server neodpoví ihned, ale pozdrží odpověď do doby, kdy se stav dveří změní. Ovšem nemůže odpověď zdržovat donekonečna, spojení by se rozpadlo. Takže server po určitém čase (řádově desítky sekund) odpoví, že se nic nezměnilo a tím současně uzavře spojení. Klient ihned otevře nové spojení a pošle další požadavek a bude se znovu čekat na událost nebo na timeout. Tímto způsobem se sníží zatížení linky i severu, ale i přesto se prohlížeč dozví o události v reálném čase.

Ovšem dělal jsem v praxi čekání na událost přesně tímto způsobem a stávalo se naprosto pravidelně, že po 30 minutách a několika sekundách (vždy po přesně stejném čase) Firefox jednoduše přestal přijímat odpovědi. Na lince byly, v konzoli Firefoxu také, ale JavaScript je nedostával. Nepodařilo se přijít na to, proč se to stávalo. Musel jsem tam čas od času nechat znovu načíst stránku.

Přichází SSE – Server-Sent Events

Jak jsme si řekli v minulé kapitole, není jednoduché čekat na neočekávané události pomocí Ajaxu. Proto se přibližně sedm let po Ajaxu objevila technologie SSE. Od standardní komunikace po HTTP se liší v jednom důležitém detailu: Po odeslání odpovědi se spojení neuzavře a server má možnost posílat klientovi další a další data, kdykoliv potřebuje, po již otevřeném kanále.

Zahájení komunikace ze strany JavaScriptu je překvapivě jednoduché. Jednoduše otevřeme spojení na určitou adresu URL a nadefinujeme callback, který se zavolá, jakmile dojde k události. Událost v tomto případě může být doručení nového paketu nebo rozpad spojení.

var source = new EventSource('updates.cgi');
source.onmessage = function (event) {
  alert(event.data);
};

Na server se pošle standardní požadavek GET. Jediná nová věc je formát dat, který prohlížeč akceptuje. V případě Ajaxu to býval text/html,application/xhtml+xml,application/xml. Tady je to pouze text/event-stream. Server odpoví klasickým 200 OK a HTTP hlavičkami, ale nezavře socket. Ten zůstane otevřený a server na něj bude postupně posílat data, kdy bude chtít nebo kdy je bude mít k dispozici. Nesmí ovšem po každé události zapomenout na flush, jinak data zůstanou v bufferu operačního systému a nic se nepošle.

Data se posílají jednoduchým protokolem podobným hlavičce HTTP. Každý paket obsahuje několik řádků ukončených CRLF ve formátu proměnná: hodnota. Rozeznávají se pouze:

  • id – může obsahovat libovolný identifikátor, který je pak v JavaScriptu dostupný v  event.lastEventId,
  • data – jeden řádek kódovaný v UTF-8. Pokud je v jednom paketu více řádků typu data, řádky budou v event.data  odděleny znakem LF (\n),
  • event – typ události. Podle typu se mohou volat různé funkce. Pokud není specifikován, bude to typ „message“ a pro jeho obsluhu se vyvolá funkce zaregistrovaná metodou  onmessage,
  • retry – obsahuje číselnou hodnotu a nastaví reconnect na klientovi na tuto hodnotu v milisekundách.

Řádky, které nemají tento formát nebo nastavují neznámou proměnnou, jsou ignorovány. Paket se ukončí prázdným řádkem, pak již nezbývá, než zavolat flush, aby se paket poslal klientovi.

Při rozpadu spojení se ho prohlížeč pokusí znovu navázat. Ovšem ne vždy. Když jsem zabil cgi skript, který jednou za pět sekund poslal paket, spojení se přerušilo a prohlížeč ho během 10 sekund znovu otevřel a jelo se dál. Když jsem ale restartl Apache, který spojení se skriptem zprostředkovával, spojení se samozřejmě také přerušilo, ale už se neobnovilo. V konzoli prohlížeče se objevila hláška:

The connection to event_generator.php was interrupted while the page was loading.

Spojení se již neobnovilo. Při zabití skriptu se tato hláška neobjevila. Je vidět, že se na automatické znovunavázání spojení nedá spoléhat. Naštěstí je k dispozici callback onerror, který se zavolá v případě chyby. V něm je možné explicitně zavřít toto spojení metodou close, aby se automaticky neobnovilo a ihned otevřít nové spojení. Tím by se obešel tento nepříjemný problém, pravděpodobně bug ve Firefoxu.

Příklad velmi jednoduché webové stránky, která otevře spojení a čeká na události:

<html>
<body>
cekam na event
<script>
 var source = new EventSource("/event_generator.php");
 source.onmessage = function (event) {
      document.getElementById("events").innerHTML +=
           `<div>[${event.lastEventId}] ${event.data}</div>`;
};
</script>
<div id="events"></div>
</body>
</html>

K ní generátor událostí v PHP:

<?php
header('Content-type: text/event-stream');

$i=1;
while(!connection_aborted())
{
  sleep(5);
  echo "id:$i\r\n";
  $i++;
  $date=date('Y-m-d H:i:s');
  echo "data:$date\r\n\r\n";
  ob_end_flush(); # bez těchto dvou flush to nefunguje
  flush();
}

Poznámka: Je nutno zajistit, aby PHP skript mohl běžet libovolně dlouho. V php.ini  je nutno nastavit max_execution_time na nulu. Navíc, pokud PHP běží přes FPM (FastCGI Process Manager), mohl by mít problémy s vyprazdňováním bufferu.

Alternativní generátor událostí jako cgi skript v Pythonu:

#!/usr/bin/python3
import sys
import datetime
import time

sys.stdout.write('Content-type: text/event-stream\r\n\r\n')
sys.stdout.flush()
counter=1
while True:
   time.sleep(5)
   now = datetime.datetime.now()
   sys.stdout.write('id: %d\r\n' % counter)
   counter=counter+1
   sys.stdout.write('data: %s\r\n\r\n' % now)
   sys.stdout.flush()

Příklad výsledku:

cekam na event
[1] 2022-07-05 19:28:50.975988
[2] 2022-07-05 19:28:55.978203
[3] 2022-07-05 19:29:00.978813
[4] 2022-07-05 19:29:05.980137
[1] 2022-07-05 19:29:20.190203
[2] 2022-07-05 19:29:25.194223
[3] 2022-07-05 19:29:30.199300

V tomto příkladu jsem po třetím paketu zabil skript, který pakety na serveru generoval. Spojení se tím rozpadlo, klient ho automaticky znovu navázal. Číslování identifikátoru začíná znovu od jedničky, protože se skript na serveru spustil znovu.

Teď se ovšem podíváme, co se děje na lince. Napřed samotný text komunikace:

GET /cgi-bin/event_generator.py.cgi HTTP/1.1
Host: localhost
User-Agent:
Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101
Firefox/89.0
Accept: text/event-stream
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://localhost/sse.html
Cookie:jwt=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJqZXRAc3BpbnRlYy5jb20iLCJuYW1lIjoiSmV0IiwiYXYiOm51bGwsInR6IjoiRXVyb3BlL1JvbWUiLCJsYyI6ImVuIiwiZGYiOiJERC9NTS9ZWVlZIiwiYXAiOiIiLCJwZXJtaXNzaW9ucyI6WyJtYW5hZ2U6c3lzdGVtIl0sImdyb3VwcyI6WzFdLCJpYXQiOjE2Mzk1NTkyNDIsImV4cCI6MTYzOTU2MTA0MiwiYXVkIjoidXJuOndpa2kuanMiLCJpc3MiOiJ1cm46d2lraS5qcyJ9.lFFa04izhdHt9FvY9LWGpEceEEH1kjjTw4NWteF7rrAK8kEnduyDOJjhDVKiXGHv-XRz9bteR3gRVMHeUOeTvkpnw4oKHLBDIj3Pmy30wkxdzi08G3gE7ZavG6h9BpLoGmgQ0rtjqWwtZV11DzUTek7W_tsUXX2a--23_EGidgmHE31IVcyhOjIsN8jeQu1Hzsf9gPm9K0I6ZB9FE1-bVhF8-n90KZ35LaU9pBwHrIbK9x-LUQswHFYq3ab32gRb5BxdfcfAU_Sv67RPZ6chs2H3QVr8SOh2pTsA-mBoNKrl7qxtHmIo55YFPUMs8KO02UIzq2Wyfe40HHZCD0Ps_w
DNT: 1
Sec-GPC: 1
Pragma: no-cache
Cache-Control: no-cache
HTTP/1.1 200 OK
Date: Wed, 06 Jul 2022 06:59:22 GMT
Server: Apache/2.4.29 (Ubuntu)
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/event-stream

2d
id: 1
data: 2022-07-06 08:59:27.580531


2d
id: 2
data: 2022-07-06 08:59:32.585606

Jak vidíme, jednotlivé události jsou přeneseny ve formě pěti řádků na již otevřeném spojení. Před vlastním blokem události je řádek s délkou bloku a za blokem jsou dva prázdné řádky. Je to tím, že vlastní blok SSE, v tomto případě řádky id, data a prázdný řádek, je zabalen do bloku protokolu „chunked“. Viz hlavička Transfer-Encoding: chunked. Tento poslední protokol umožňuje přenášet data HTTP postupně, bez nutnosti znát předem celkovou délku přenosu. Používá se běžně i pro přenos normálních stránek generovaných skriptem.

Tento protokol posílá v prvním řádku hexadecimální číslo, které oznamuje protistraně délku tohoto dílčího bloku. Řádek s délkou je ukončen dvojicí znaků CR a LF. Následují vlastní data bloku, ta jsou znovu ukončena dvojicí znaků CR a LF. Tyto poslední dva znaky nejsou zahrnuty v délce bloku, protože jsou součástí protokolu chunked, nejsou součástí dat. Tato ukončovací sekvence zaručuje, že délka příštího bloku bude začínat na novém řádku i v případě, že předchozí blok nekončil na konci řádku. Celkový přenos HTTP je pak ukončen vysláním bloku s nulovou délkou.

Příklad přenosu dvou bloků chunked:

04<CR><LF>Exam<CR><LF>06<CR><LF>ple:<CR><LF><CR><LF>

V tomto příkladu se řádek Example:<CR><LF> přenáší dvěma bloky. První blok Exam má čtyři bajty. CR a LF za slovem Exam jsou součástí protokolu, takže se nepočítají. Další blok ple:<CR><LF>  má šest znaků, tyto CR a LF jsou součástí dat, jsou započítány v délce. Poslední CR a LF jsou součástí protokolu, nepočítají se do délky.

Vrátíme se zpátky k protokolu SSE. Vlastní protokol obsahuje jeden nebo více datových řádků ukončených CRLF. Poslední řádek samotného protokolu SSE je prázdný. Následuje ještě jeden CRLF, který je tentokrát součástí protokolu chunked, tedy není započítán do délky bloku. Za každým posledním řádkem SSE tedy najdeme tři CRLF za sebou, ale do délky bloku se počítají jenom dva z nich.

Na úrovni paketů to vypadá následovně:

Autor: Josef Pavlík

Vše začíná klasickou výměnou tří paketů pro navázání TCP spojení. Pak tam máme výzvu GET, potvrzení a hned potom odpověď 200 OK s hlavičkou odpovědi o délce 271 bajtů. Potom už jenom každých pět sekund přijde paket 115 bajtů s textem události a dostane potvrzení 66 bajtů. V těchto necelých 200 bajtech je celý přenos informace o události. To už je velký pokrok vůči dotazům přes Ajax. Jak vidíme, formát odpovědi je v podstatě stále stejný – je to HTTP komunikace typu GET, ale odpověď může přicházet po kouskách v libovolném čase.

Vrátíme-li se k našemu modelovému příkladu s hlídáním dveří, stačí v tomto případě pouze ve skriptu na straně serveru hlídat spínač a v okamžiku, kdy se stav změní, poslat řádek data: nový stav a flushnout výstup. Klient na druhé straně tuto událost dostane okamžitě.

Poslední věc, o které bych se chtěl zmínit, je ta, že je vhodné posílat nějaké zprávy periodicky, aby se nerozpadlo spojení. Po cestě mohou být různé proxy nebo NATy a ty by mohly shazovat neaktivní spojení. Proto je vhodné čas od času, třeba jednou za půl minuty, poslat nějakou nevýznamnou zprávu jenom proto, aby se udrželo spojení.

Pro bližší a kompletnější informace o třídě EventSource doporučuji dokumentaci u Mozilly.

Pokračování příště

Ukázali jsme si, jak webová aplikace komunikuje pomocí technologií Ajax a SSE. Nevypadalo to tak složitě. Teď se dostáváme k technologii WebSocket, která už je o kus komplikovanější. Je to téma, které si zaslouží samostatný článek.

Byl pro vás článek přínosný?

Autor článku

Josef Pavlík vystudoval FE VUT v Brně. Od roku 1991 žije v Itálii a v současné době pracuje ve společnosti SpinTec s.r.l., kde vyvíjí hardware, firmware a software nízké úrovně.