WebSocket, QUIC e HTTP-3
In questa pagina 6
Questa nota chiude il percorso sulle applicazioni Web con due evoluzioni che superano i limiti del modello richiesta-risposta di HTTP/1.1 (HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encodingHTTP/1.1 (oggi RFC 9110 e 9112) rende la connessione persistente di default (si chiude solo con "Connection: close"), rende obbligatorio l'header Host (virtual hosting) e introduce i nuovi metodi PUT, DELETE, OPTIONS, TRACE, Expect: 100-continue, richieste di intervalli (206) e Transfer-Encoding: chunked; con la connessione persistente il client deve sapere dove finisce ogni risposta: lunghezza del corpo nell'ordine HEAD/1xx/204/304 senza corpo, Transfer-Encoding chunked, Content-Length, altrimenti fino alla chiusura; il chunked divide il corpo in blocchi preceduti dalla lunghezza in esadecimale, termina con un chunk 0 e un trailer facoltativo, e si decodifica contando i byte dichiarati (non cercando CRLF).HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encoding →): WebSocket, che dà alle applicazioni un canale bidirezionale persistente, e QUIC con HTTP/3, che cambiano il trasporto sotto HTTP per ridurre la latenza.
WebSocket
Perché
In HTTP è sempre il client a parlare per primo: il server non può mandare dati di sua iniziativa (Modello ISO-OSI, data plane e control plane, architetture client-server e publish-subscribeIl modello ISO/OSI (7 livelli) e la pila TCP/IP (4 livelli) dividono la comunicazione in strati; ogni livello riceve una SDU dal livello superiore, aggiunge la sua intestazione e produce una PDU; il confine fra due livelli adiacenti e' il SAP, identificato da un indirizzo (EtherType per Ethernet, numero di protocollo per IP, numero di porta per TCP e UDP) e per un programma C il SAP del trasporto e' l'API delle socket; il data plane inoltra i pacchetti seguendo le tabelle, il control plane le costruisce con i protocolli di instradamento; client/server funziona come una chiamata di funzione (richiesta e risposta), peer-to-peer mette tutti i nodi alla pari, publish/subscribe funziona come un interrupt: un broker inoltra gli eventi agli iscritti, con disaccoppiamento nello spazio, nel tempo e nella sincronizzazione.Modello ISO-OSI, data plane e control plane, architetture client-server e publish-subscribe →: è client/server puro). Per applicazioni che hanno bisogno di notifiche immediate (chat, giochi, quotazioni, editor condivisi) si sono usate varie soluzioni:
| tecnica | funzionamento | limite |
|---|---|---|
| polling | il client chiede ogni T secondi |
traffico inutile se non ci sono novità; ritardo fino a T |
| long polling | il server tiene la richiesta aperta finché ha qualcosa da dire | una richiesta (con tutti gli header) per ogni evento |
| Server-Sent Events | una risposta text/event-stream che non finisce mai |
solo dal server al client |
| WebSocket | un canale bidirezionale sulla stessa connessione TCP | richiede un server dedicato |
L'overhead conta: ogni richiesta HTTP porta con sé centinaia di byte di header (con i cookie anche migliaia), mentre un frame WebSocket ha un'intestazione di 2-14 byte. Con un polling a 1 s con circa 800 byte fra richiesta e risposta (500 + 300) un client consuma 800 byte al secondo anche senza novità; con WebSocket resta un frame di ping ogni tanto.
Handshake
WebSocket (RFC 6455) comincia come HTTP: il client apre una connessione TCP (porta 80 per ws://, 443 per wss://, cioè WebSocket su TLS) e invia una richiesta GET HTTP/1.1 che chiede di cambiare protocollo (Upgrade):
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.comSe accetta, il server risponde con 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Dopo la riga vuota la connessione non è più HTTP: i byte successivi sono frame WebSocket. Gli header:
| header | significato |
|---|---|
Upgrade: websocket, Connection: Upgrade |
richiesta di passaggio di protocollo |
Sec-WebSocket-Key |
16 byte casuali codificati in Base64 (24 caratteri), scelti dal client a ogni connessione |
Sec-WebSocket-Version: 13 |
versione del protocollo |
Origin |
sito che ha originato la richiesta (il server può rifiutare origini non permesse) |
Sec-WebSocket-Protocol, Sec-WebSocket-Extensions |
sottoprotocolli ed estensioni negoziati (es. permessage-deflate per la compressione) |
Sec-WebSocket-Accept |
prova che il server ha capito WebSocket (sotto) |
Se il server non accetta risponde con un normale errore HTTP (400, 404, 426 Upgrade Required).
Calcolo di Sec-WebSocket-Accept. Il server concatena la chiave del client (come stringa, così com'è) con il GUID fisso 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, calcola il SHA-1 (20 byte) e lo codifica in Base64 (28 caratteri):
chiave = "dGhlIHNhbXBsZSBub25jZQ=="
cat = "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
sha1 = b3 7a 4f 2c c0 62 4f 16 90 f6 46 06 cf 38 59 45 b2 be c4 ea
accept = base64(sha1) = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="Il client ricalcola il valore e lo confronta: se non coincide chiude. Lo scopo non è la sicurezza (SHA-1 qui è solo un modo per mescolare i dati) ma evitare che un server HTTP o una cache non WebSocket risponda per errore con un messaggio che sembra un'accettazione.
Base64
Base64 (RFC 4648 par. 4) rappresenta byte arbitrari con 64 caratteri stampabili (A-Z a-z 0-9 + /): serve dove il canale accetta solo testo (qui l'header HTTP, ma anche Authorization: Basic e la posta MIME). Si leggono 3 byte (24 bit) alla volta e si dividono in 4 gruppi da 6 bit, ciascuno un indice nell'alfabeto (0-25 A-Z, 26-51 a-z, 52-61 0-9, 62 +, 63 /).
Esempio. Man = 4D 61 6E = 01001101 01100001 01101110 → 010011 010110 000101 101110 = 19, 22, 5, 46 → T W F u: TWFu. Se i byte finali non riempiono un gruppo si aggiunge il riempimento =: due byte (Ma) → TWE=, un byte (M) → TQ==. Quindi la lunghezza codificata è sempre multipla di 4 e vale 4 · ceil(n / 3): 16 byte casuali (la chiave) → 24 caratteri con == finale; i 20 byte di SHA-1 → 28 caratteri con un =. La codifica ingrandisce i dati di un terzo (3 byte → 4 caratteri).
Le funzioni base64_encode e sha1 in C, con test sui valori della RFC, sono in Esercizio - Client WebSocket con handshake, SHA-1 e Base64 (sul modello della prova pratica).
Frame
Dopo l'handshake i dati viaggiano in frame (RFC 6455 par. 5.2):
byte 0 byte 1
+-+-+-+-+-------+ +-+-------------+
|F|R|R|R| opcode| |M| lunghezza | [lunghezza estesa 2 o 8 byte] [chiave di mascheramento 4 byte] dati
|I|S|S|S| (4) | |A| (7) |
|N|V|V|V| | |S| |
| |1|2|3| | |K| |
+-+-+-+-+-------+ +-+-------------+| campo | significato |
|---|---|
| FIN (1 bit) | 1 = ultimo frame del messaggio |
RSV1-3 |
riservati alle estensioni (0 senza estensioni) |
| opcode (4 bit) | 0x0 continuazione, 0x1 testo (UTF-8), 0x2 binario, 0x8 close, 0x9 ping, 0xA pong |
| MASK (1 bit) | 1 = il payload è mascherato, e segue la chiave di 4 byte |
| lunghezza (7 bit) | 0-125: la lunghezza vera; 126: la lunghezza è nei 2 byte seguenti (big endian); 127: è nei 8 byte seguenti |
| chiave di mascheramento | 4 byte, presente solo se MASK = 1 |
| payload | i dati |
Quindi l'intestazione occupa 2 byte per payload fino a 125 byte, 4 fino a 65535, 10 oltre; con la maschera, altri 4 byte.
Mascheramento. Tutti i frame dal client al server devono essere mascherati con una chiave casuale di 4 byte nuova per ogni frame: trasformato[i] = originale[i] XOR chiave[i mod 4] (la stessa operazione rimaschera e smaschera). I frame dal server al client non sono mascherati. Un frame non conforme fa chiudere la connessione con errore di protocollo. Il motivo non è la riservatezza (la chiave viaggia accanto ai dati) ma la sicurezza di intermediari come i proxy: senza maschera uno script malevolo potrebbe costruire dati che sembrano una richiesta HTTP e avvelenare una cache.
Gli esempi della RFC 6455 par. 5.7 per il testo Hello:
| frame | byte |
|---|---|
| non mascherato (server → client) | 81 05 48 65 6c 6c 6f |
mascherato (client → server), chiave 37 fa 21 3d |
81 85 37 fa 21 3d 7f 9f 4d 51 58 |
in due frammenti: Hel poi lo |
01 03 48 65 6c + 80 02 6c 6f |
ping non mascherato con Hello |
89 05 48 65 6c 6c 6f |
| 256 byte binari | 82 7e 01 00 + dati |
| 65536 byte binari | 82 7f 00 00 00 00 00 01 00 00 + dati |
Per il primo frame mascherato: 0x81 = FIN 1, opcode 1 (testo); 0x85 = MASK 1, lunghezza 5; poi la chiave; poi 48^37 = 7f, 65^fa = 9f, 6c^21 = 4d, 6c^3d = 51, 6f^37 = 58... (il quarto byte si maschera con chiave[3], il quinto di nuovo con chiave[0]).
Messaggi, frammentazione e controllo
- Un messaggio può essere diviso in più frame: il primo ha l'opcode del tipo (testo o binario) e
FIN = 0, i successivi opcode0x0(continuazione), l'ultimoFIN = 1. Serve per inviare dati la cui lunghezza non è nota in anticipo, ed è il corrispondente a livello applicativo del chunked. - I frame di controllo (close, ping, pong) hanno payload al massimo di 125 byte, non si frammentano e possono comparire in mezzo ai frammenti di un messaggio.
- Ping/pong: chi riceve un ping risponde subito con un pong con lo stesso payload; si usa per verificare che il peer sia vivo (keepalive) e per tenere aperti i NAT.
- Chiusura. Chi vuole chiudere invia un frame
closecon un codice a 2 byte (e un testo facoltativo); l'altro risponde con unclose; poi si chiude la connessione TCP.
| codice | significato |
|---|---|
| 1000 | chiusura normale |
| 1001 | l'endpoint se ne va (il server si spegne, la pagina viene chiusa) |
| 1002 | errore di protocollo |
| 1003 | tipo di dati non accettabile |
| 1006 | chiusura anomala (valore riservato: non si trasmette, lo vede l'applicazione quando la TCP cade senza close) |
| 1007 | dati incoerenti (testo non UTF-8) |
| 1008 | violazione di una policy |
| 1009 | messaggio troppo grande |
| 1011 | errore interno del server |
La sicurezza si cura con wss:// (TLS), il controllo dell'Origin lato server, un limite alla dimensione dei messaggi (codice 1009) e la verifica dell'autenticazione durante l'handshake (le credenziali di un cookie o di un token nell'URL: dopo l'Upgrade non c'è più Authorization per ogni messaggio).
Un client WebSocket completo in C (handshake, frame mascherati, ping/pong, close) è in Esercizio - Client WebSocket con handshake, SHA-1 e Base64 (sul modello della prova pratica). Il Web usa WebSocket come substrato del modello publish/subscribe: il server pubblica eventi su canali a cui il client si è iscritto con un messaggio applicativo (Modello ISO-OSI, data plane e control plane, architetture client-server e publish-subscribeIl modello ISO/OSI (7 livelli) e la pila TCP/IP (4 livelli) dividono la comunicazione in strati; ogni livello riceve una SDU dal livello superiore, aggiunge la sua intestazione e produce una PDU; il confine fra due livelli adiacenti e' il SAP, identificato da un indirizzo (EtherType per Ethernet, numero di protocollo per IP, numero di porta per TCP e UDP) e per un programma C il SAP del trasporto e' l'API delle socket; il data plane inoltra i pacchetti seguendo le tabelle, il control plane le costruisce con i protocolli di instradamento; client/server funziona come una chiamata di funzione (richiesta e risposta), peer-to-peer mette tutti i nodi alla pari, publish/subscribe funziona come un interrupt: un broker inoltra gli eventi agli iscritti, con disaccoppiamento nello spazio, nel tempo e nella sincronizzazione.Modello ISO-OSI, data plane e control plane, architetture client-server e publish-subscribe →).
QUIC
I limiti di TCP per il Web
Anche con HTTP/2 (RFC 9113: un'unica connessione TCP su cui viaggiano molti flussi paralleli, con header compressi) restano problemi che stanno nel trasporto:
- Latenza di avvio: prima del primo byte di dati servono l'handshake TCP (1 RTT) e quello TLS (1 o 2 RTT: TCP - connessione, affidabilità e controllo di flussoTCP (Transmission Control Protocol) è il protocollo di trasporto con connessione e affidabile: trasforma il servizio senza connessione e inaffidabile di IP in un flusso di byte ordinato, senza errori né duplicati. La connessione si apre con l'handshake a tre vie (SYN, SYN+ACK, ACK) e si chiude con tre o quattro segmenti (FIN). I byte sono numerati: il numero di sequenza è quello del primo byte del segmento, il numero di ACK (cumulativo) è il prossimo byte atteso. Il mittente può inviare $\min(\text{rwnd},\text{cwnd})$ byte non ancora confermati; rwnd (finestra del ricevitore, in un campo di 16 bit) è il controllo di flusso. L'errore si gestisce con checksum, ACK, timeout di ritrasmissione (RTO) e ritrasmissione rapida dopo tre ACK duplicati. Per usare tutto il canale la finestra deve valere almeno il prodotto banda-ritardo (BDP); il throughput massimo è $\text{MSS}\cdot W_{\max}/\text{RTT}$.TCP - connessione, affidabilità e controllo di flusso →, Crittografia asimmetrica, RSA e TLSCrittografia asimmetrica (a chiave pubblica): chiave di cifratura pubblica v_B, chiave di decifratura privata s_B, matematicamente legate; C = E_vB(P), P = D_sB(C); dà riservatezza. I certificati digitali, emessi da una Certificate Authority e firmati con la sua chiave privata, legano un'identità a una chiave pubblica. Firma digitale: hash del messaggio cifrato con la chiave privata; garantisce autenticità, integrità e non ripudio. RSA: si scelgono due primi grandi p e q, N = pq, phi(N) = (p-1)(q-1), e coprimo con phi(N), d = e^(-1) mod phi(N); chiave pubblica (N, e), segreta (N, d); cifratura c = m^e mod N, decifratura m = c^d mod N con m < N (teorema di Eulero); sicura finché la fattorizzazione è difficile (N di almeno 2048 bit); il padding casuale (PKCS#1 v1.5: 00 02 [casuale] 00 [m]) difende da malleabilità e determinismo. La firma RSA è s = m^d mod N, verificata con s^e mod N. TLS (su TCP) autentica gli estremi con il certificato, cifra i dati con una chiave di sessione simmetrica e garantisce l'integrità con i MAC; da TLS 1.0 a 1.3 la chiave si ricava con Diffie-Hellman e l'handshake si accorcia. DTLS è la versione per UDP.Crittografia asimmetrica, RSA e TLS →).
- Head-of-line blocking di TCP: TCP consegna i byte in ordine. Se un segmento si perde, tutti i dati successivi, anche quelli appartenenti ad altri flussi HTTP/2, aspettano la ritrasmissione, perché TCP non sa che il flusso di byte contiene più risorse indipendenti.
- Connessione legata a indirizzi e porte: se il client cambia rete (dal Wi-Fi al cellulare) cambia l'IP e la connessione TCP muore.
- Ossificazione: TCP vive nel kernel e nei dispositivi intermedi (firewall, NAT) che guardano e modificano i suoi campi: cambiarlo è quasi impossibile da decenni.
Che cos'è QUIC
QUIC (RFC 9000, con TLS in RFC 9001 e recupero delle perdite in RFC 9002) è un protocollo di trasporto sopra UDP, implementato in user space (nelle librerie del browser e del server, non nel kernel), con sicurezza TLS 1.3 integrata e sempre attiva. Vuol dire che dal punto di vista della rete sono datagrammi UDP (di solito sulla porta 443), ma QUIC si assume ciò che UDP non dà: affidabilità, ordine per flusso, controllo di flusso e di congestione (Protocollo UDPUDP (User Datagram Protocol) è il protocollo di trasporto senza connessione e inaffidabile: rispetto a IP aggiunge soltanto la comunicazione processo-processo (numeri di porta) e un controllo d'errore facoltativo. L'intestazione è di soli 8 byte (porta sorgente, porta destinazione, lunghezza, checksum). Il checksum copre pseudo-intestazione (indirizzi IP, protocollo 17, lunghezza), intestazione e dati, ed è il complemento a uno della somma a 16 bit; se vale 0 significa "non calcolato", e un risultato 0 si trasmette come 0xFFFF. UDP non ha connessione, numeri di sequenza, controllo di flusso, di errore né di congestione: si sceglie per i messaggi brevi (DNS, DHCP, RIP, SNMP) e per le applicazioni in tempo reale, dove conta non aggiungere ritardo.Protocollo UDP →, TCP - controllo di congestioneLa congestione nasce quando collegamenti veloci alimentano un collegamento lento: le code dei router si riempiono, i pacchetti si perdono o ritardano e, nel caso peggiore, la rete collassa (quasi solo ritrasmissioni). TCP controlla la propria finestra di congestione cwnd con il feedback delle perdite (timeout o tre ACK duplicati): slow start (cwnd raddoppia a ogni RTT) fino alla soglia ssthresh, poi congestion avoidance (+1 MSS per RTT); a ogni perdita ssthresh = W/2. Le varianti si distinguono per come reagiscono ai tre dupACK: Tahoe riparte da cwnd = 1 dopo la ritrasmissione rapida; Reno usa il fast recovery (ssthresh = cwnd/2, cwnd = ssthresh + 3, +1 per ogni altro dupACK); NewReno gestisce gli ACK parziali e recupera più perdite nella stessa finestra; SACK riscontra i blocchi ricevuti e ritrasmette solo quello che manca.TCP - controllo di congestione →).
| caratteristica | come funziona |
|---|---|
| flussi indipendenti | una connessione contiene molti stream; una perdita ferma solo lo stream interessato, gli altri proseguono |
| handshake veloce | trasporto e crittografia nello stesso scambio: 1 RTT (e 0 RTT in ripresa) |
| sempre cifrato | quasi tutto il pacchetto è cifrato e autenticato, anche gran parte degli header: gli intermediari non possono interferire |
| Connection ID | la connessione è identificata da un ID, non dalla quaterna IP/porte: cambiare rete non interrompe la connessione (connection migration) |
| controllo di flusso | per stream e per connessione |
| perdite e congestione | numeri di pacchetto sempre crescenti (niente ambiguità fra pacchetto e ritrasmissione), ACK, algoritmi simili a quelli di TCP (NewReno, CUBIC, BBR) |
| evoluzione | essendo in user space si aggiorna con il software, senza attendere i sistemi operativi |
Flussi. Ogni stream ha un identificatore a 62 bit; i due bit meno significativi dicono il tipo: bit 0 chi l'ha aperto (0 client, 1 server), bit 1 la direzione (0 bidirezionale, 1 unidirezionale). Quindi gli stream bidirezionali aperti dal client hanno ID 0, 4, 8, 12, ....
Pacchetti. All'inizio (handshake) i pacchetti hanno un'intestazione lunga (tipo Initial, 0-RTT, Handshake, Retry, con versione e Connection ID), poi quella corta (1-RTT). Un pacchetto contiene frame (STREAM con i dati, ACK, CRYPTO per il TLS, MAX_DATA e MAX_STREAM_DATA per il flusso, NEW_CONNECTION_ID, CONNECTION_CLOSE...). Il datagramma UDP con un pacchetto Initial del client deve essere di almeno 1200 byte (con PADDING): serve a limitare gli attacchi di amplificazione (il server non risponde con più dati di quelli ricevuti prima della validazione dell'indirizzo).
Quanto si risparmia
Tempo prima del primo byte della risposta (RTT = 50 ms, trascurando elaborazione e trasmissione; si conta il numero di andata e ritorno):
| configurazione | scambi prima della richiesta | primo byte | con RTT = 50 ms |
|---|---|---|---|
| HTTP su TCP (senza TLS) | 1 (TCP) | 2 RTT | 100 ms |
| HTTPS, TCP + TLS 1.2 | 1 + 2 | 4 RTT | 200 ms |
| HTTPS, TCP + TLS 1.3 | 1 + 1 | 3 RTT | 150 ms |
| HTTP/3, QUIC 1-RTT | 1 (trasporto + TLS) | 2 RTT | 100 ms |
| HTTP/3, QUIC 0-RTT (ripresa) | 0 | 1 RTT | 50 ms |
Con 0-RTT i dati della richiesta partono nel primo volo di pacchetti, usando chiavi di una sessione precedente: a costo di un rischio di replay (un attaccante può ripetere la richiesta), quindi si ammette solo per richieste idempotenti.
HTTP/3
HTTP/3 (RFC 9114) è HTTP trasportato da QUIC. La semantica (metodi, header, codici, cache, Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →) non cambia: cambia come i messaggi viaggiano.
- Una richiesta, uno stream. Ogni coppia richiesta/risposta usa uno stream bidirezionale aperto dal client. Richieste diverse sono indipendenti: una perdita blocca solo la sua. Il head-of-line blocking del trasporto sparisce.
- Frame. I messaggi sono composti da frame HTTP/3:
HEADERS(tipo0x01, gli header),DATA(tipo0x00, il corpo),SETTINGS(0x04, parametri, sul flusso di controllo),GOAWAY(0x07, chiusura ordinata). I messaggi non usanoContent-Lengtho chunked per la delimitazione del corpo, perché lo stream ha già un inizio e una fine. - Flusso di controllo. Un flusso unidirezionale per direzione (stream type
0x00) porta iSETTINGS. - QPACK (RFC 9204): la compressione degli header, ripensata rispetto all'HPACK di HTTP/2 per funzionare con consegna fuori ordine: la tabella dinamica si aggiorna su flussi dedicati e le richieste dichiarano da quale stato della tabella dipendono.
- Scoperta. Un client non sa in anticipo che un server parla HTTP/3: lo scopre con l'header di risposta
Alt-Svc: h3=":443"; ma=86400(anche via record DNS HTTPS) ricevuto su una connessione HTTP/1.1 o HTTP/2, o lo prova direttamente. La negoziazione dell'applicazione TLS usa l'identificatore ALPNh3. Se UDP è bloccato dalla rete, il client ripiega su TCP con HTTP/2 o HTTP/1.1.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| trasporto | TCP | TCP | QUIC su UDP |
| formato | testo | binario, frame | binario, frame su stream QUIC |
| richieste in parallelo | più connessioni (6 per host) | multiplexing su una connessione | multiplexing su una connessione QUIC |
| head-of-line blocking | per connessione (pipelining) | in TCP, fra tutti i flussi | solo nello stesso stream |
| header | non compressi | HPACK | QPACK |
| cifratura | opzionale (HTTPS) | di fatto TLS (i browser) | sempre (TLS 1.3 integrato) |
| avvio connessione | TCP + TLS (3-4 RTT) | TCP + TLS | 1 RTT, 0 RTT in ripresa |
| cambio di rete | connessione persa | connessione persa | migrazione con Connection ID |
Programmare QUIC. Una applicazione non usa connect, read e write su un socket TCP: gli stream sono forniti da una libreria (quiche, ngtcp2, msquic, picoquic) che usa socket UDP con sendto e recvfrom (System call POSIX, file descriptor e API delle socketLe system call sono le funzioni con cui un programma in user space chiede servizi al kernel (open, read, write, close, fork, pipe, dup2, socket, bind, listen, accept, connect); restituiscono -1 e impostano errno in caso di errore; un file descriptor e' un intero che indicizza la tabella dei file aperti del processo (0 stdin, 1 stdout, 2 stderr) e vale per file, pipe e socket; read e write possono trasferire MENO byte del richiesto (e sui socket TCP non c'e' nessun confine fra i messaggi), quindi servono cicli write_all e read_exact; l'API delle socket crea un punto finale di comunicazione (socket), lo lega a indirizzo e porta (bind), lo rende passivo (listen), accetta connessioni (accept) o si connette (connect); getaddrinfo traduce nomi e porte in indirizzi (IPv4 e IPv6); UDP usa sendto e recvfrom e conserva i confini dei datagrammi, TCP e' uno stream affidabile.System call POSIX, file descriptor e API delle socket →) e implementa in user space ritrasmissioni, controllo di flusso, TLS e frame. Il modello di programmazione resta ad eventi: i dati di uno stream arrivano come eventi e si scrivono con chiamate della libreria.
Esercizi collegati
- Esercizio - Client WebSocket con handshake, SHA-1 e Base64 (sul modello della prova pratica): handshake,
Sec-WebSocket-Accept, frame mascherati, ping/pong, chiusura.
Errori tipici
- Dimenticare che i frame dal client vanno mascherati, o mascherare quelli del server.
- Confondere la lunghezza
126/127con la lunghezza vera: sono indicatori che dicono quanti byte di lunghezza estesa seguono (e sono in big endian). - Calcolare
Sec-WebSocket-Acceptcon la chiave decodificata dal Base64 invece che con la stringa, o dimenticare il GUID, o codificare in Base64 l'esadecimale del SHA-1 invece dei 20 byte. - Credere che la maschera protegga i dati (non cifra; serve
wss://). - Trattare un messaggio come un solo frame: può essere frammentato; e dimenticare di rispondere a un ping.
- Scrivere
HTTP/1.1 101senza riga vuota finale: il client non vede la fine degli header. - Credere che QUIC sia "TCP più veloce": è un altro protocollo, sopra UDP, con TLS integrato e flussi indipendenti.
- Dire che HTTP/3 cambia metodi e codici di stato: cambia solo il trasporto e la rappresentazione dei messaggi.
- Contare male i tempi di avvio: TCP 1 RTT, TLS 1.2 2 RTT, TLS 1.3 1 RTT, QUIC 1 RTT totale.
Domande d'esame
1. Descrivere l'handshake WebSocket e come si calcola Sec-WebSocket-Accept.
Traccia. Il client invia una GET HTTP/1.1 con Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Key (16 byte casuali in Base64) e Sec-WebSocket-Version: 13. Il server risponde 101 Switching Protocols con Upgrade, Connection e Sec-WebSocket-Accept = Base64(SHA-1(chiave + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")). Per la chiave dGhlIHNhbXBsZSBub25jZQ== si ottiene s3pPLMBiTxaQ9kYGzzhZRbK+xOo=. Il client verifica il valore; poi la connessione trasporta frame WebSocket.
2. Decodificare il frame 81 85 37 fa 21 3d 7f 9f 4d 51 58.
Traccia. 81: FIN = 1, opcode 1 (testo). 85: MASK = 1, lunghezza 5. Chiave 37 fa 21 3d. Dati mascherati 7f 9f 4d 51 58; in XOR con la chiave 37 fa 21 3d 37: 48 65 6c 6c 6f = Hello.
3. Perché HTTP/3 usa QUIC e non TCP? Elencare tre vantaggi e il tempo prima del primo byte con RTT = 80 ms per HTTPS su TCP+TLS 1.3 e per QUIC 1-RTT. Traccia. TCP consegna in ordine, quindi una perdita blocca tutti i flussi (head-of-line blocking); QUIC ha flussi indipendenti. Vantaggi: handshake con TLS integrato in 1 RTT (0 RTT in ripresa), nessun blocco fra richieste, migrazione fra reti con Connection ID, evoluzione in user space, cifratura sempre attiva. Primo byte: TCP + TLS 1.3 = (1 + 1 + 1) RTT = 3 × 80 = 240 ms; QUIC 1-RTT = 2 RTT = 160 ms.
Versione ripasso
- Perché WebSocket. HTTP è solo richiesta e risposta. Polling (traffico inutile), long polling (una richiesta per evento), SSE (solo server -> client). WebSocket: canale bidirezionale persistente sulla stessa connessione TCP, header di 2-14 byte per frame.
ws://porta 80,wss://porta 443 (TLS). - Handshake.
GET /chat HTTP/1.1conUpgrade: websocket,Connection: Upgrade,Sec-WebSocket-Key(16 byte casuali in Base64, 24 caratteri),Sec-WebSocket-Version: 13(Origin,Sec-WebSocket-Protocol/Extensionsfacoltativi). Risposta101 Switching Protocols+Upgrade+Connection+Sec-WebSocket-Accept= Base64(SHA-1(chiave +258EAFA5-E914-47DA-95CA-C5AB0DC85B11)); chiavedGhlIHNhbXBsZSBub25jZQ==->s3pPLMBiTxaQ9kYGzzhZRbK+xOo=. Dopo la riga vuota solo frame. - Base64 (RFC 4648). 3 byte -> 4 caratteri da 6 bit (
A-Z a-z 0-9 + /), riempimento=;Man=4D 61 6E-> 19, 22, 5, 46 ->TWFu;Ma->TWE=;M->TQ==. Lunghezza4·ceil(n/3): 16 byte -> 24 caratteri (==), SHA-1 da 20 byte -> 28 (=). - Frame. Byte 0: FIN, RSV1-3, opcode (
0x0continuazione,0x1testo,0x2binario,0x8close,0x9ping,0xApong). Byte 1: MASK + lunghezza a 7 bit (<126vera;126+ 2 byte;127+ 8 byte, big endian). Poi chiave di 4 byte se MASK. Intestazione 2, 4 o 10 byte (+4 con maschera). - Maschera. Client -> server obbligatoria, chiave casuale per frame:
out[i] = in[i] XOR chiave[i mod 4]; server -> client mai. Non cifra: protegge gli intermediari (cache poisoning). Esempi:Hello=81 05 48 65 6c 6c 6f; mascherato con chiave37 fa 21 3d=81 85 37 fa 21 3d 7f 9f 4d 51 58; frammentato01 03 48 65 6c+80 02 6c 6f; ping89 05 .... - Messaggi. Frammentazione: primo frame opcode del tipo con
FIN=0, poi0x0, ultimoFIN=1. Controllo (close, ping, pong): payload al più 125 byte, non frammentati. Ping -> pong con lo stesso payload. Close: codice a 2 byte (1000 normale, 1001 va via, 1002 protocollo, 1003 dati non accettabili, 1006 anomala, 1007 dati incoerenti, 1008 policy, 1009 troppo grande, 1011 errore interno), l'altro risponde con close, poi si chiude TCP. - Sicurezza WebSocket.
wss://, controllo diOrigin, limite ai messaggi, autenticazione nell'handshake. - Limiti di TCP. Avvio lento (TCP 1 RTT + TLS 1-2 RTT), head-of-line blocking (consegna in ordine: una perdita blocca tutti i flussi HTTP/2), connessione legata a IP e porte, ossificazione (TCP - connessione, affidabilità e controllo di flussoTCP (Transmission Control Protocol) è il protocollo di trasporto con connessione e affidabile: trasforma il servizio senza connessione e inaffidabile di IP in un flusso di byte ordinato, senza errori né duplicati. La connessione si apre con l'handshake a tre vie (SYN, SYN+ACK, ACK) e si chiude con tre o quattro segmenti (FIN). I byte sono numerati: il numero di sequenza è quello del primo byte del segmento, il numero di ACK (cumulativo) è il prossimo byte atteso. Il mittente può inviare $\min(\text{rwnd},\text{cwnd})$ byte non ancora confermati; rwnd (finestra del ricevitore, in un campo di 16 bit) è il controllo di flusso. L'errore si gestisce con checksum, ACK, timeout di ritrasmissione (RTO) e ritrasmissione rapida dopo tre ACK duplicati. Per usare tutto il canale la finestra deve valere almeno il prodotto banda-ritardo (BDP); il throughput massimo è $\text{MSS}\cdot W_{\max}/\text{RTT}$.TCP - connessione, affidabilità e controllo di flusso →).
- QUIC (RFC 9000/9001/9002). Trasporto su UDP (porta 443) in user space, TLS 1.3 integrato, sempre cifrato; stream indipendenti (ID: bit 0 = iniziatore, bit 1 = direzione; client bidirezionali 0, 4, 8...); Connection ID (migrazione fra reti); handshake 1 RTT, ripresa 0 RTT (rischio replay: solo richieste idempotenti); pacchetti con frame (
STREAM,ACK,CRYPTO,MAX_DATA...); numeri di pacchetto crescenti; controllo di flusso per stream e connessione; congestione come TCP;Initialdi almeno 1200 byte (Protocollo UDPUDP (User Datagram Protocol) è il protocollo di trasporto senza connessione e inaffidabile: rispetto a IP aggiunge soltanto la comunicazione processo-processo (numeri di porta) e un controllo d'errore facoltativo. L'intestazione è di soli 8 byte (porta sorgente, porta destinazione, lunghezza, checksum). Il checksum copre pseudo-intestazione (indirizzi IP, protocollo 17, lunghezza), intestazione e dati, ed è il complemento a uno della somma a 16 bit; se vale 0 significa "non calcolato", e un risultato 0 si trasmette come 0xFFFF. UDP non ha connessione, numeri di sequenza, controllo di flusso, di errore né di congestione: si sceglie per i messaggi brevi (DNS, DHCP, RIP, SNMP) e per le applicazioni in tempo reale, dove conta non aggiungere ritardo.Protocollo UDP →).
| primo byte (RTT = 50 ms) | RTT | tempo |
|---|---|---|
| HTTP su TCP | 2 | 100 ms |
| TCP + TLS 1.2 | 4 | 200 ms |
| TCP + TLS 1.3 | 3 | 150 ms |
| QUIC 1-RTT | 2 | 100 ms |
| QUIC 0-RTT | 1 | 50 ms |
- HTTP/3 (RFC 9114). Stessa semantica di HTTP, trasporto QUIC. Una richiesta = uno stream bidirezionale del client; frame
HEADERS(0x01),DATA(0x00),SETTINGS(0x04, su flusso di controllo),GOAWAY(0x07); nienteContent-Length/chunked per delimitare; QPACK (RFC 9204) per gli header con consegna fuori ordine. Scoperta conAlt-Svc: h3=":443"; ma=86400, ALPNh3; ripiego su TCP se UDP è bloccato. - Confronto. HTTP/1.1: testo, più connessioni, HOL per connessione. HTTP/2: binario, multiplexing su TCP, HPACK, HOL in TCP. HTTP/3: QUIC su UDP, HOL solo nello stesso stream, QPACK, sempre cifrato, 1 RTT, migrazione. QUIC si programma con librerie su socket UDP (
sendto/recvfrom). - Errori tipici: frame del client non mascherati; lunghezza
126/127presa per la lunghezza vera;Sec-WebSocket-Acceptcalcolato con la chiave decodificata, senza GUID o con l'esadecimale; maschera creduta cifratura; ping ignorato; riga vuota mancante dopo il101; QUIC creduto "TCP più veloce"; HTTP/3 creduto un cambio di semantica; tempi di avvio contati male.
Esercizi su questo argomento
Teoria collegata
- Caching, autenticazione, tipi MIME e URI in HTTP
- DNS, proxy web, HTTP CONNECT e gateway applicativi
- HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encoding
- Modello ISO-OSI, data plane e control plane, architetture client-server e publish-subscribe
- Protocolli per i servizi multimediali
- Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endianness
- Streaming adattativo e DASH