Hlavní navigace

Komunikace webové aplikace se serverem pomocí WebSocket

10. 11. 2022
Doba čtení: 17 minut

Sdílet

 Autor: Depositphotos
V minulém článku jsme si ukázali, jak může webová stránka komunikovat se severem pomocí Ajax a Server-Sent Event. Dozvěděli jsme se, jaké omezení mají tyto dvě technologie. Dnes si ukážeme, jak funguje WebSocket.

Se SSE jsme už dostali k dispozici dobrý nástroj pro komunikaci, ale stále to ještě nebylo ono. SSE je totiž jednosměrný – můžeme pouze dostávat data ze serveru. Pro opačný směr sice máme Ajax, ale jak jsme viděli, na poslání řádově desítek bajtů má režii zhruba kilobajt v deseti paketech. Navíc jsme omezeni tím, že kvůli bezpečnosti můžeme komunikovat pouze se serverem, ze kterého byla stránka stažena. Chce to něco lepšího. Řešení této situace se jmenuje WebSocket.

Co se dozvíte v článku
  1. WebSocket
  2. Server pro WebSocket
  3. Shrnutí
  4. Reference

WebSocket

Předchozí technologie jsou postaveny na lehce upraveném protokolu HTTP. WebSocket na to jde úplně jinak, má protokol zcela nový. Můžeme se spojit na libovolný port a to nejen na server, ze kterého byla stránka načtena, ale na rozdíl od Ajax a SSE můžeme bez komplikací otevřít spojení i na jiný server, než ze kterého jsme stáhli stránku, protože tu není CORS (Cross-Origin Resource Sharing).

Spojení začíná komunikací protokolem HTTP. Proč taková komplikace? Nebylo by lepší jednoduše otevřít socket a hned posílat naše data? Určitě ano, ale HTTP má dvě obrovské výhody. První spočívá v tom, že můžeme využít stejný port, ze kterého jsme stáhli svou stránku. Tam sice poslouchá webový server, třeba Apache, ale právě díky tomu, že začínáme protokolem HTTP, nám bude server rozumět a přesměruje nás na WebSocket.

Poznámka: Vyvíjel jsem embeded zařízení a pro zjednodušení jsem měl WebSocket na jiném portu. Proč ne, všechno fungovalo. Ale pak jsem si uvědomil, že když budu chtít mít toto zařízení někde za NATem, musel bych přesměrovávat dva porty. Tak jsem to rychle předělával tak, aby všechno šlo přes jediný port 80 . Člověk si to musí vyzkoušet na vlastní kůži, aby to pochopil. 

Druhá výhoda je v tom, že jeden HTTP server může obsluhovat třeba i několik stovek domén. Kdyby každá stránka na každé doméně potřebovala jeden extra port pro socket, brzy by nebylo odkud brát, protože portů je omezené množství.

Otevření WebSocketu tedy začíná klasicky jako komunikace po HTTP. Ovšem už v prvním dotazu klient specifikuje, že bude chtít přejít na jiný protokol. Server mu to potvrdí ještě jako HTTP, potom oba přejdou na protokol ws.

Z JavaScriptu otevřeme WebSocket velmi jednoduše, podobně jako v případě SSE:

    socket= new WebSocket(`ws://${window.location.host}/console.ws`);
    socket.onopen = function(e) {
// vyvolá se po otevření socketu
    };
    socket.onmessage = function(event) {
        console.log(`[message] Data received from server: ${event.data}`);
// vyvolá se při příjmu datového paketu
    };
    socket.onerror = function(error) {
        alert(`[error] ${error.message}`);
// asi nemusím vysvětlovat...
    };

Data na server pošleme také naprosto jednoduše:

socket.send(data);

Při otvírání WebSocketu musíme specifikovat protokol. Ten může být ws:// nebo šifrovaný wss://. Na straně klienta se nic nezmění, ovšem komunikace bude zabezpečená, podobně jako u  https://.

Příklad jednoduché stránky, která pošle zprávu po WebSocketu na server a ten jí odpoví:

<html>
<body>
cekam na event
<script>
    socket= new WebSocket(`ws://${window.location.host}/ws/test.ws`);
    socket.onmessage = function(event) {
         document.getElementById("events").innerHTML +=
             `<div>${event.data}</div>`;
    };
// každých 5 sekund pošleme něco na server
   setInterval(function() { const now=new Date();
   socket.send(now.toISOString().substr(11,12)); }, 5000);
</script>
<div id="events"></div>
</body>
</html>

Podívejme se teď, co se děje při otevření WebSocketu na úrovni TCP:

GET /ws/test.ws HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: */*
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://localhost
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: VZNJyucbPjv81iRANZMbtA==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: U9ifcm+kYP7H6V9E2qTk/FSrFl4=
Sec-WebSocket-Protocol: chat
..{.@.J.z.N.s.U.y...reply: 15:05:31.196
.....Z...j...l...l..reply: 15:05:36.196
..%{K.?A{.0Oz.;B}..reply: 15:05:41.196
....K.8.{.7.}.<.}..reply: 15:05:46.196
...K...~...q...z....reply: 15:05:51.196
..1.W...m...b...n...reply: 15:05:56.196
........"1..(0..!7..reply: 15:06:01.196
..-._G..ew..oq..fp..reply: 15:06:06.197
..1.....=...6...>...reply: 15:06:11.197
...?.`.;P..0V..8W..reply: 15:06:16.197

Jako obvykle, nejprve vidíme to, co poslal klient serveru, poté vidíme komunikaci ze serveru na klienta. Začátek je stejný jako to, co už známe – obyčejný GET. Ale není tak obyčejný, v hlavičce je Upgrade: websocket.Tento příkaz způsobí, že server neodpoví klasické 200 OK, ale 101 Switching protocols. Ještě pár řádků HTTP hlavičky, jeden prázdný řádek, který oznamuje konec hlavičky a nastupuje úplně nový protokol.

Přechod na nový protokol ale není úplně jednoduchý. V hlavičce požadavku jsme viděli řádek Sec-WebSocket-Key: VZNJyucbPjv81iRANZMbtA==. Tato hodnota je náhodně vygenerované 128bitové číslo převedené do base64. Z tohoto čísla je nutno vygenerovat protikód, který server vrátí klientovi v řádku Sec-WebSocket-Accept: U9ifcm+kYP7H6V9E2qTk/FSrFl4=. Algoritmus výpočtu odpovědi je samozřejmě známý, musí ho znát každý server, který chce používat WebSocket. K hodnotě WebSocket-Key (tak jak je, bez dekódování) je třeba připojit řetězec 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, vypočítat SHA1, vzít tuto hodnotu 40 bajtů a zakódovat je do base64. Zní to složitě, ale například v Bashi se to dá udělat jediným řádkem

$ echo -n 'VZNJyucbPjv81iRANZMbtA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11' | sha1sum | awk '{print$1}' | xxd -r -p | base64

Po výměně paketů pro navázání komunikace a přechodu na nový protokol vidíme výměnu paketů po socketu. Klient po natažení stránky každých pět sekund pošle řádek s textem, který obsahuje aktuální čas. Server, který jsem pro tento demonstrační příklad použil, jednoduše čeká na jakýkoliv UTF-8 paket od klienta a vrátí ho zpátky se slovem „reply“ na začátku. Odpovědi vypisuje do stránky.

Rychým pohledem na komunikaci vidíme, že to není obyčejný přenos bajtů jako v případě normálního socketu. Každý blok má totiž svoji hlavičku ve které jsou informace o délce, typu přenosu a případném maskování. Bloky od serveru ke klientovi v tomto případě nejsou maskovány, ty od klienta k serveru ano. Jeden blok může mít délku až 4 GB, může být rozdělen do několika TCP paketů, celý přenos může být složen z několika bloků.

Toto je hex dump websocketové části předchozí komunikace:

00000483 81 8c 7b 98 40 ef 4a ad 7a df 4e a2 73 de 55 a9 ..{.@.J.z.N.s.U.
00000493 79 d9                                           y.              
0000009F 81 13 72 65 70 6c 79 3a 20 31 35 3a 30 35 3a 33 ..reply: 15:05:3
000000AF 31 2e 31 39 36                                  1.196           
00000495 81 8c 91 f9 b7 5a a0 cc 8d 6a a4 c3 84 6c bf c8 .....Z...j...l..
000004A5 8e 6c                                           .l              
000000B4 81 13 72 65 70 6c 79 3a 20 31 35 3a 30 35 3a 33 ..reply: 15:05:3
000000C4 36 2e 31 39 36                                  6.196           
000004A7 81 8c 25 0a 7b 4b 14 3f 41 7b 10 30 4f 7a 0b 3b ..%.{K.?A{.0Oz.;
000004B7 42 7d                                           B}              
000000C9 81 13 72 65 70 6c 79 3a 20 31 35 3a 30 35 3a 34 ..reply: 15:05:4
000000D9 31 2e 31 39 36                                  1.196           
000004B9 81 8c 85 0d cb 4b b4 38 f1 7b b0 37 ff 7d ab 3c .....K.8.{.7.}.<
000004C9 f2 7d                                           .}              
000000DE 81 13 72 65 70 6c 79 3a 20 31 35 3a 30 35 3a 34 ..reply: 15:05:4
000000EE 36 2e 31 39 36                                  6.196           

struktura hlavičky bloku je tato:

offset délka formát význam
0 1 bit F....... (nejvyšší bit) FIN = příznak, že je to poslední blok přenosu
0 4 bity …OOOO Opcode = typ bloku
1 1 bit M....... MASK = příznak, že data jsou maskována
1 7 bitů .LLLLLLL LEN = délka bloku
2 0,2 nebo 8 bajtů nejvýznamnější byte je první (big endian) XLEN rozšířená délka. Pokud je LEN 126, jsou tady 2 bajty délky. Pokud je LEN 127, je tady 8 bajtů délky
2+len(XLEN) 0 nebo 4 bajty Pokud je MASK 1, jsou tady 4 bajty masky
2+len(XLEN)+len(MASK) 0 až 4giga-1 data

Pokud je délka dat v bloku menší než 126 bajtů, vejdou se přímo do druhého bajtu hlavičky. Přenosy s větší délkou mají tuto délku v přídavných dvou nebo dokonce osmi bajtech.

Pokud je nastavený bit MASK, znamená to, že jsou data xorována maskou, která zabírá čtyři bajty v hlavičce. První bajt dat je xorován prvním bajtem masky, druhý bajt druhým, pátý bajt dat je pak zase XORován prvním bajtem masky a tak dále. Jinými slovy, každá čtveřice bajtů dat je XORována stejnou maskou.

Příznak FIN znamená, že tento blok je poslední blok přenosu.

Poslední hodnota o které jsem se ještě nezmínil je OPCODE. Ten specifikuje typ dat. Může to být:

kód význam
0×00 pokračovací blok – opcode byl specifikován v prvním bloku
0×01 text v UTF8
0×02 binární data
0×08 oznamuje, že strana, která ho poslala chce uzavřít spojení
0×09 ping, slouží na udržení spojení, aby nespadlo kvůli neaktivitě
0×0a pong, odpověď na ping
ostatní rezervováno

První dva pakety websocketové části komunikace měly tento obsah (vydekódováno Wiresharkem):

Autor: Josef Pavlík
Autor: Josef Pavlík

Jak vidíme, v případě kratších a nemaskovaných bloků je hlavička dlouhá pouhé dva bajty. Při maskovaném přenosu je to šest bajtů. Komunikace vypadá na úrovni paketů takto:

Autor: Josef Pavlík

První tři pakety jsou klasicky otevření TCP spojení. Pak klient pošle GET, dostane potvrzení a odpověď 101 Switching protocols. Ještě potvrzení odpovědi a pak už následuje komunikace po protokolu WebSocket. Každých pět sekund odejde jeden websocketový paket od klienta na server, ten okamžitě pošle odpověď a klient mu ji potvrdí. Je zajímavé, že server nepotvrzuje klientovi příjem paketu. Pravděpodobně je potvrzená paketem, kterým se přenáší odpověď. Celkově klient poslal dvanáct datových bajtů, server mu odpověděl 19bajtovou zprávou. Po drátě se vyměnily tři pakety o celkové délce 237 bajtů. Účinnost je v tomto případě 13%. Je to velký pokrok proti Ajax nebo SSE a to v tomto případě máme kratší zprávy, než byly v příkladech s Ajaxem nebo SSE.

Server pro WebSocket

Jak jsme viděli v předchozí kapitole, otevřít WebSocket na straně klienta (prohlížeče) je velmi jednoduché. Jak to ovšem vypadá na straně serveru? Bohužel server pro WebSocket už zdaleka není tak jednoduchý jako server pro SSE. V případě SSE stačilo jen nezavírat spojení a nechat PHP nebo cgi skript běžet a postupně posílat pakety. S WebSocketem to není tak jednoduché. Jak jsme si již řekli, pro navázání komunikace se používá standardní protokol HTTP nebo HTTPS, kterému web server rozumí. Díky tomu se můžeme spojit se serverem na portu 80 nebo 443. Ale server nedokáže sám zpracovávat pakety WebSocketu. Dokáže je pouze přesměrovávat na jiný port stejného nebo jiného serveru (reverse proxy). Na tomto novém portu běží démon, který dostane spojení a vyřizuje požadavky, které na něj přesměrovává web server. Problém je ovšem v tom, že tento démon musí poslouchat na nějakém konkrétním portu a portů je pouze 64 tisíc.

V případě fyzického serveru, který obsluhuje stovky virtuálních serverů by to nemuselo stačit. Naštěstí existuje velmi jednoduché řešení tohoto problému. Web server totiž bude komunikovat s tímto démonem přes rozhraní loopback. To ovšem není pouze adresa 127.0.0.1, loopback je celá síť 127.0.0.0/8, takže i adresy 127.0.0.2, 127.0.0.3 až do 127.255.255.254. Každý virtuální webový server tedy může přesměrovávat svoje web sockety na jinou IP adresu loopbacku a bude mít k dispozici všech 64 tisíc portů pro 64 tisíc různých socketů.

Nejprve je tedy nutno zkonfigurovat Apache, aby přesměrovával některou konkrétní stránku konkrétního virtuálního webového serveru na příslušného démona, který obsluhuje WebSocket.

Konfigurace přesměrování z web serveru

Nejdříve musíme povolit moduly Apache. Na Ubuntu to uděláme jednoduše:

# a2enmod proxy
# a2enmod proxy_http
# a2enmod proxy_wstunnel
# systemctl restart apache2

Do konfigurace příslušného virtuálního serveru přidáme přesměrování na port, na kterém bude poslouchat náš démon:

ProxyPass "/ws/" "ws://127.0.0.2:12345"
ProxyPassReverse "/ws/" "ws://127.0.0.2:12345"

Přístup na jakýkoliv soubor v adresáři ws virtuálního serveru bude přesměrován na démona, který poslouchá na adrese 127.0.0.2 na portu 12345. Tam poběží náš pokusný daemon, na který se spojíme přes WebSocket.

Démon pro WebSocket

Démona můžeme napsat v jakémkoliv jazyce, který se nám líbí. Musí poslouchat na TCP portu. Až dostane spojení, musí odpovědět na počáteční komunikaci protokolem HTTP a interpretovat bloky WebSocketu. Samozřejmě existuje nespočet knihoven pro nejrůznější jazyky, které tuto práci udělají za nás. Například v Pythonu s použitím knihovny pywebsocket by tento server mohl vypadat přibližně takto:

#!/usr/bin/python3

# https://pypi.org/project/pywebsocket/
# pip install pywebsocket


from pywebsocket.server import WebsocketServer, WebsocketClient

def on_client_connect(server : WebsocketServer,
                      client : WebsocketClient) -> None:
    print("got connection")

def on_client_disconnect(server : WebsocketServer,
                          client : WebsocketClient) -> None:
    print("disconnected")

def on_client_data(server : WebsocketServer,
                   client : WebsocketClient,
                   data) -> None:
    # Echo client's message.
    print("got:"+data)
    server.send_string(client.get_id(), "reply: "+data)

server = WebsocketServer("127.0.0.2", 12345,
                         client_buffer_size       = 1024,
                         pass_data_as_string      = True,
                         daemon_handshake_handler = False,
                         debug                    = True)

server.set_special_handler("client_connect",    on_client_connect)
server.set_special_handler("client_disconnect", on_client_disconnect)
server.set_special_handler("client_data",       on_client_data)

server.start()

Vyvolání metody server.start() nikdy neskončí, bude se čekat na spojení na adresu 127.0.0.2, port 12345. Příklad je s mírnými úpravami převzat z domovské stránky projektu pywebsocket.

Jak je bohužel u Pythonu zvykem, knihovna pywebsocket na mém počítači nefunguje, přestože mám poměrně nový systém (Ubuntu 18.04). To mi ovšem nezabránilo napsat pokusný program v Pythonu bez pomoci této knihovny. Vystačil jsem si pouze s base64 a hashlib kvůli výpočtu protikódu pro navázání komunikace.

Démon pro WebSocket v Pythonu bez speciální knihovny

Kvůli zjednodušení tento program pouze počká na první spojení, obslouží ho a skončí. S mírnými úpravami by se dal vyvolat z xinetd nebo ze systemd. Pro pokus to stačí a díky tomu, že všechno děláme bez použití knihoven, máme možnost vidět, jak přesně komunikace po WebSocketu funguje.

#!/usr/bin/python3

import base64
import hashlib
import socket
import sys

Nejprve otevřeme socket a počkáme na první spojení

# open TCP socket
sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#set socket option reused to true
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.2',12345))
sock.listen(1)

#accept a connection
conn, addr = sock.accept()
print('Connected by', addr)

Přečteme celou HTTP hlavičku, která končí prázdným řádkem. Zapamatujeme si URL dotazu, pokud bychom jej chtěli použít. Nebude to celé URL, chybí protokol a host, bude to jen path/file?případné_parametry. Do slovníku vars dekódujeme hodnoty jednotlivých parametrů definovaných v hlavičce.

def read_line(conn):
    line=b'';
    while True:
        c=conn.recv(1)
        if len(c)==0: return False
        if c==b'\r': continue
        if c==b'\n': break
        line+=c
    return line.decode('utf-8')

#read the header
data=[]
while True:
    line=read_line(conn)
    if line==False: sys.exit(0)
    if line=='': break
    data.append(line)
#print(repr(data))
url=data[0].split(' ')[1]  #get the path/file and parameters

#create dictionary of variables from header
vars = {}
for line in data:
    line = line.split(': ')
    if len(line)>1: vars[line[0]] = line[1]

##print the map
#print(vars)

#print("\n");
#print("File name: " + url)
#print("key="+vars['Sec-WebSocket-Key'])

Z parametru Sec-Websocket-Key vypočítáme protikód, sestavíme odpověď a pošleme ji klientovi.

#calc websocket handshake
key = vars['Sec-WebSocket-Key']
key = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
key = key.encode('utf-8')
key = hashlib.sha1(key).digest()
key = base64.b64encode(key)
key = key.decode('utf-8')

handshake =  "HTTP/1.1 101 Switching Protocols\r\n"
handshake += "Upgrade: websocket\r\n"
handshake += "Connection: Upgrade\r\n"
handshake += "Sec-WebSocket-Accept: " + key + "\r\n"
handshake += "Sec-WebSocket-Protocol: chat\r\n"
handshake += "\r\n"

print(handshake)
conn.send(handshake.encode('utf-8'))

Připravíme si třídu websocket, která bude číst bloky přicházející z WebSocketu. Pro zjednodušení nebudeme předpokládat, že by byl jeden přenos realizován více bloky. V případě přenosů kratších než 4 GB by k tomu nemělo docházet.

#class to handle the websocket
class websocket:
    def __init__(self, conn):
        self.conn = conn

    def unmask(self, data):
        self.retval=b''
        for i in range(len(data)):
            self.retval+=bytes([data[i] ^ self.masking_key[i%4]])
        return self.retval

    def read_frame(self):
        #read the fist two bytes of header
        self.data = self.conn.recv(2)
        print("self.data: " + repr(self.data))
        if len(self.data)==0: return #connection lost
        #parse the first byte
        self.fin = (self.data[0] & 0b10000000) >> 7
        self.opcode = self.data[0] & 0b00001111
        #parse the second byte
        self.mask = (self.data[1] & 0b10000000) >> 7
        self.length = self.data[1] & 0b01111111

        #read the extended length
        if self.length==126:
            self.data = self.conn.recv(2)
        elif self.length==127:
            self.data = self.conn.recv(8)
        #decode the extended length
        if self.length>=126:
            if len(self.data)==0: return
            self.length = int.from_bytes(self.data, byteorder='big')

        #read the masking key if mask is set
        if self.mask==1:
            self.masking_key = self.conn.recv(4)
            print("self.masking_key: " + repr(self.masking_key))
            if len(self.masking_key)==0:
                return

        print("self.length: " + repr(self.length))
        #read the payload
        self.payload=b''
        if self.length>0:
            self.l=self.length
            while self.l>0:
                self.data = self.conn.recv(self.l)
                print("read data: " + repr(self.data))
                if len(self.data)==0:
                    return #broken connection
                self.payload += self.data
                self.l -= len(self.data)
            #unmask the payload
            if self.mask==1:
                self.payload = self.unmask(self.payload)
        return self.payload

Stejná třída pak posílá i odpovědi. Opět nepočítáme s přenosy delšími než 4 GB. Každý blok má nastavený příznak FIN. Odpovědi posíláme vždy bez maskování (není povinné).

   def send_bin_frame(self, data):
        self.header=[0b10000000 | 0x02] # FIN, binary
        self.send_frame(data)

    def send_utf8_frame(self, string):
        self.header=[0b10000000 | 0x01] # FIN, UTF8 text
        self.send_frame(string.encode('utf-8'))

    def send_pong(self, data):
        self.header=[0b10000000 | 0x0A] # FIN, PONG
        self.send_frame(data)

    def send_frame(self, data):
        if len(data)<=125:
            self.header+=[len(data)]
        elif len(data)<=65535:
            self.header+=[126, len(data)>>8, len(data)]
        else:
            self.header+=[127]
            for i in range(7,-1,-1): # from 7 to 0 inclusive
                self.header+=[len(data)>>(i*8)]
        self.conn.send(bytes(self.header)+data)
        print("Sent: " + repr(bytes(self.header)+data))

Nakonec už nezbývá, než stále dokola číst přicházející bloky a odpovídat na ně. Tento pokusný program pouze pošle zpět to, co přijme. Po přerušení spojení program skončí

#read data from the socket and reply it back
ws=websocket(conn)
while True:
    data = ws.read_frame()
    print("got: " + repr(data))
    if not data: break;
    print("Received: " + repr(data))
    if ws.opcode==0x01: # got utf8 frame
        ws.send_utf8_frame("reply: "+data.decode('utf-8'))
    elif ws.opcode==0x02: # got binary frame
        ws.send_bin_frame("reply: ".decode('utf-8')+data)
    elif ws.opcode==0x08: # got close frame
        break
    elif ws.opcode==0x09: # got ping frame
        ws.send_pong(data)

Server pro WebSocket v ESP8266 nebo ESP32

Pokud píšeme webovou aplikaci provozovanou v mikrořadiči, je nám málo platné vědět, jak zkonfigurovat Apache nebo napsat démon v Pythonu. Tady si musíme vystačit s málem. Ale není to problém, naštěstí už tuto práci udělal někdo za nás a připravil nám knihovny, které to zvládají. Pro framework Arduino existuje pohodlná knihovna ESPAsyncWebServer, která je schopna dělat server i pro WebSocket. Program je poměrně přímočarý.

root_podpora

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>

// jednoducha html stranka, ktera otevre websocket

const char testws_html[] PROGMEM = R"=====(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebSocket</title>
<script>
var ws;
function connect() {
  ws = new WebSocket("ws://" + location.host + "/ws");
  ws.onopen = function() {
    console.log("connected");
  };
  ws.onmessage = function(e) {
    console.log(e.data);
    let log=document.getElementById("log");
    log.innerHTML+=e.data+"<br>";
  };
  ws.onclose = function() {
    console.log("disconnected");
  };
}
function send() {
  var msg = document.getElementById("msg").value;
  ws.send(msg);
}
</script>
</head>
<body onload="connect()">
<input type="text" id="msg" value="Hello world!">
<button onclick="send()">Send</button>
<div id="log"></div>
</body>
</html>
)=====";


// main
void init_serial()
  {
    // doplnit inicializaci serioveho portu
  }

void init_wifi()
  {
    // doplnit inicializaci wifi
  }

// main

void setup()
  {
  init_serial();
  init_wifi();
  AsyncWebServer *test_ws=new AsyncWebServer(80);
  test_ws->on("/", HTTP_GET, [](AsyncWebServerRequest *request)
  {
    request->send(200, "text/html", testws_html);
  });
  AsyncWebSocket *awebsocket=new AsyncWebSocket("/ws");
  awebsocket->onEvent([](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len)
  {
    switch(type)
    {
      case WS_EVT_CONNECT:
        Serial1.println("WS_EVT_CONNECT");
        break;
      case WS_EVT_DISCONNECT:
        Serial1.println("WS_EVT_DISCONNECT");
        break;
      case WS_EVT_ERROR:
        Serial1.println("WS_EVT_ERROR");
        break;
      case WS_EVT_PONG:
        Serial1.println("WS_EVT_PONG");
        break;
      case WS_EVT_DATA:
        Serial1.println("WS_EVT_DATA");
#define MSG "Hello from server, you sent: "
        char reply[len+sizeof(MSG)]=MSG;
        memcpy(reply+sizeof(MSG)-1, (char*)data, len);
        client->text(reply, sizeof(MSG)-1+len);
        break;
    }
  });
  test_ws->addHandler(awebsocket);
  test_ws->begin();
}

Shrnutí

Jak jsme si ukázali, máme několik způsobů, jak řešit komunikaci mezi webovou stránkou, tedy spíše webovou aplikací, a okolním světem. Některé způsoby jsou jednoduché, ale třeba zbytečně zatěžují linku, jiné jsou složitější, ovšem pak máme k dispozici plynulejší komunikaci. Nezbývá si, než vybrat ten správný kompromis pro řešení konkrétní situace.

Zbývá doplnit ještě popis komunikace UDP pomocí technologie WebRTC, ale s touto technologií zatím nemám příliš velké zkušenosti, takže prozatím skončím tady. Snad někdy příště.

Reference

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ě.