← Tutti gli episodi

Stagione 94  /  Episodio 7

La passerella

I canali dati di WebRTC: SCTP dentro DTLS, canali affidabili e inaffidabili, ordinati e non ordinati, la dimensione dei messaggi, il controllo del flusso con bufferedAmount e i casi d'uso, dal trasferimento di file ai giochi.

Note dell'episodio

Bentornati sulle due rive. Finora sul ponte abbiamo fatto passare carri pesanti: audio e video. Oggi guardiamo la passerella che corre accanto alla carreggiata: i canali dati.

Un canale dati permette di mandare qualunque cosa tra due browser: testo, file, comandi, lo stato di un gioco. Direttamente, con la stessa cifratura, lungo lo stesso guado trovato da ICE. Ed è, secondo molti, la parte più sottovalutata di WebRTC.

Partiamo dai materiali.

I canali dati usano un protocollo che forse non avete mai incontrato: SCTP. È un protocollo di trasporto nato per la telefonia, alla pari di TCP e UDP, con alcune caratteristiche preziose. Ma ha un problema: quasi nessun router o firewall su internet lo lascia passare così com'è.

La soluzione adottata da WebRTC è un incastro di scatole. SCTP viaggia dentro DTLS, quindi cifrato con la stessa parola d'ordine del quinto episodio. DTLS viaggia dentro UDP. E UDP passa per il guado trovato da ICE, o per il traghetto. Dall'esterno, i guardiani vedono solo pacchetti UDP cifrati. È una matrioska, descritta nella RFC 8261, e funziona molto bene.

Nella lettera del messaggero, i canali dati occupano una sola sezione, quella con la riga m uguale application. Una volta aperta quella sezione, potete creare tutti i canali che volete senza rinegoziare.

La caratteristica più utile di SCTP sono i flussi multipli.

Su una sola connessione possono convivere fino a circa sessantacinquemila canali, e ognuno è indipendente dagli altri. Se un messaggio grande sta passando su un canale, un messaggio piccolo su un altro canale non deve aspettare in fila. È una differenza importante rispetto a un WebSocket, dove tutto passa in un'unica coda: se state mandando un file da cento megabyte, anche il messaggio di chat aspetta che il file sia finito.

Quando create un canale con createDataChannel, il browser manda all'altra riva un piccolo messaggio di apertura, con un protocollo chiamato DCEP, e l'altra riva riceve l'evento datachannel. In alternativa, potete negoziare i canali a mano: entrambe le rive creano il canale con lo stesso numero identificativo, e il canale è pronto subito, senza messaggio di apertura. È comodo per i canali di controllo che esistono sempre.

Qui sta la vera forza della passerella: potete scegliere, canale per canale, quali garanzie volete.

Per impostazione predefinita un canale è affidabile e ordinato: ogni messaggio arriva, e arriva nell'ordine in cui è partito. Esattamente come TCP. È quello che serve per una chat o per un file.

Ma potete togliere l'ordine: i messaggi arrivano tutti, ma ognuno appena arriva, senza aspettare quelli precedenti. E potete togliere l'affidabilità, in due modi. Con un numero massimo di ritrasmissioni, per esempio zero: il messaggio parte una volta e se si perde, pazienza. Oppure con una durata massima: il canale ritrasmette il messaggio, ma solo entro un certo numero di millisecondi. Dopo, rinuncia. Le due opzioni si escludono a vicenda.

Perché rinunciare all'affidabilità? Pensate a un gioco in cui ogni giocatore manda la propria posizione venti volte al secondo. Se un aggiornamento si perde, ritrasmetterlo è inutile: tra cinquanta millisecondi ne arriva uno nuovo, più recente. Peggio: aspettare la ritrasmissione bloccherebbe gli aggiornamenti successivi. Per questi dati conta solo l'ultimo valore. È la stessa logica che abbiamo visto per il video, applicata ai dati.

Un canale inaffidabile e non ordinato si comporta di fatto come UDP, ma cifrato, con controllo della congestione e capace di attraversare i NAT. È qualcosa che nel browser, prima di WebRTC, semplicemente non esisteva.

Arriviamo a un dettaglio che ha fatto perdere ore a molti sviluppatori: la dimensione massima di un messaggio.

Ogni riva dichiara nella lettera quanto grande può essere un messaggio che è disposta a ricevere, con un attributo chiamato max message size. I browser moderni accettano messaggi di centinaia di kilobyte, ma i valori sono diversi tra un browser e l'altro, e tra versioni diverse. Se mandate un messaggio troppo grande, il canale può chiudersi di colpo.

La regola pratica è semplice: per la massima compatibilità, spezzate i dati in pezzi da sedici kilobyte. È il valore che funziona ovunque. Per un file, significa leggerlo a fette, mandare le fette una alla volta, e ricomporle dall'altra parte.

E qui c'è la seconda trappola, ancora più comune.

La chiamata send di un canale dati non blocca. Mette il messaggio in coda e ritorna subito. Se scrivete un ciclo che legge un file da un gigabyte e chiama send per ogni fetta, il ciclo finisce in un istante, e tutto il gigabyte si accumula nella memoria del browser, in attesa di partire. Nei casi peggiori la scheda si blocca, o il canale si chiude per una coda troppo piena.

Il canale vi dice quanti byte ha in coda, con la proprietà bufferedAmount. La tecnica corretta è il controllo del flusso: mandate fette finché la coda è sotto una certa soglia, poi vi fermate. Impostate una soglia bassa con bufferedAmountLowThreshold, e quando la coda scende sotto quella soglia il canale vi avvisa con l'evento bufferedamountlow. A quel punto riprendete. Nelle note trovate una funzione completa per inviare e ricevere un file con questa tecnica.

Quando conviene un canale dati rispetto a un WebSocket?

Conviene quando i dati vanno da un utente all'altro e il server non ha bisogno di vederli: un file condiviso durante una chiamata, il puntatore del mouse su una lavagna condivisa, i comandi di un controllo remoto, lo stato di un gioco tra pochi giocatori. Risparmiate banda sul server, riducete la latenza, e i dati non passano dal vostro sistema.

Non conviene quando il server deve comunque elaborare o conservare i dati, quando i destinatari sono molti, o quando volete semplicità. Un canale dati richiede segnalazione, ICE, magari un traghetto. Un WebSocket richiede un indirizzo. Per comunicare con il server, la scelta giusta resta WebSocket, o la nuova WebTransport, di cui parleremo nell'ultimo episodio.

Quindi, ricapitolando: i canali dati sono una passerella costruita con SCTP dentro DTLS dentro UDP. Possono essere molti e indipendenti, affidabili o veloci, ordinati o no. Vanno usati con due accortezze: messaggi in pezzi piccoli e controllo del flusso con bufferedAmount.

Finora abbiamo costruito ponti tra due sole rive. Ma cosa succede quando le rive sono cinque, dieci, cento? Nel prossimo episodio arriviamo all'isola in mezzo al fiume: le architetture per le chiamate di gruppo. Ci sentiamo lì.