Zpět na domů

WebRTC DataChannel: soubory bez chyb bufferu

Článek rozebírá produkční problémy přenosu souborů přes WebRTC DataChannel: řízení SCTP bufferu, adaptace chunků pod relay, backpressure na receiveru. Uvádějí se kódové příklady overlapped I/O, dynamic timeouts a ACK mechanismů pro spolehlivost.

Soubory v WebRTC: jak se vyhnout ořezání na relay
Advertisement 728x90

Přenos souborů přes WebRTC DataChannel: Správa bufferu a spolehlivost v produkci

V WebRTC DataChannel metoda dc.send() umístí data do SCTP bufferu odesílatele, ale nedokáže zaručit doručení příjemci. Na pomalých relay spojích se buffer naplní megabajty dat, progress bar dosáhne 100 % a soubor je nakonec oříznutý. Architektura typického P2P sdílení souborů zahrnuje signaling server pro koordinaci, přímé spojení jako primární cestu a TURN relay jako záložní variantu. Server neukládá obsah, ale řídí stav přenosu: od vytvoření sdílení až po frontu příjemců.

Klíčové výzvy: zpětný tlak z bufferu, ztráta objektu File při obnovení stránky, nesoulad připravenosti stran a opětovná připojení. Analýza vychází z produkční implementace s limity na relay a podporou vlastního TURN.

Architektura bez ukládání souborů na serveru

Server koordinuje přes REST API a WebSocket: vytváří sdílení, podpisuje připojení ECDSA, sleduje stavy. Životní cyklus sdílení – active → matched → transferring → active – umožňuje znovu použít odesílatele pro více příjemců. Fronta automaticky rozptyluje nové účastníky.

Google AdInline article slot

Konečné stavy: expired, cancelled. Stav Failed se obnovuje přes reactivate. Obsah je šifrován DTLS end-to-end, relay proxyuje provoz bez přístupu k datům.

Problém 1: Fallback na TURN a optimální velikost chunku

P2P není vždy možné kvůli symetrickému NAT, firewallům nebo mobilním sítím. Typ spojení se určuje podle ICE kandidátů: p2p, server_relay (coturn s kvótami), custom_relay (vlastní TURN bez limitů).

Velikost chunku se přizpůsobuje pro minimalizaci zpoždění retransmisí:

Google AdInline article slot
function getOptimalChunkSize(classification: string): number {
  switch (classification) {
    case 'p2p':          return 1024 * 1024;  // 1 MB
    case 'custom_relay': return 512 * 1024;   // 512 KB
    case 'server_relay': return 64 * 1024;    // 64 KB
    default:             return 512 * 1024;
  }
}

Malé chunky na server_relay urychlují obnovu: ztracený paket se retransmituje celý, zpoždění roste s velikostí. Předběžná kontrola TURN přes samostatné RTCPeerConnection s iceTransportPolicy: 'relay' trvá 5 sekund.

Problém 2: Zpětný tlak a překrývající se I/O v cyklu odesílání

Naivní cyklus odesílá chunky sekvenčně a čeká na file.slice().arrayBuffer(). Implementace využívá double buffering a high/low water marks:

const BUFFER_THRESHOLD = 8 * 1024 * 1024; // 8 MB
const BUFFER_LOW       = 6 * 1024 * 1024; // 6 MB

dc.bufferedAmountLowThreshold = BUFFER_LOW;
let nextBuffer = await file.slice(0, chunkSize).arrayBuffer();

while (offset < file.size) {
  const buffer = nextBuffer;
  const readPromise = offset + buffer.byteLength < file.size
    ? file.slice(offset + buffer.byteLength, nextEnd).arrayBuffer()
    : null;

  if (dc.bufferedAmount >= BUFFER_THRESHOLD) {
    await waitForBufferDrain(dc, computeDrainTimeout(dc.bufferedAmount, speed));
  }

  dc.send(buffer);
  offset += buffer.byteLength;

  const effectiveSent = Math.max(0, offset - dc.bufferedAmount);
  const pct = Math.min((effectiveSent / file.size) * 100, isLast ? 100 : 99);

  nextBuffer = readPromise ? await readPromise : null;
}
  • Překrývající se I/O: Čtení dalšího chunku paralelně s odesíláním aktuálního.
  • Dynamický timeout: estimatedDrainMs = (bufferedAmount / speed) * 1000, omezen 5–60 s.
  • Čestný progress: offset - dc.bufferedAmount, ne více než 99 % do drainu.

Dokončení: drain bufferu + transfer_ack od příjemce před transfer_done.

Google AdInline article slot

Problém 3: Strana příjemce a diskový zpětný tlak

Příjemce kopíruje ArrayBuffer, aby se vyhnul opětovnému použití bufferu DataChannel:

const chunk = event.data.slice(0);  // Kopírování
writeChain = writeChain.then(() => writer.write(chunk));

if (bytesReceived === fileInfo.size) {
  dc.send(JSON.stringify({ type: 'transfer_ack' }));
}

writeChain je sekvenční řetězec promise. Pomalý disk (USB) vytváří zpětný tlak nahoru po stacku: zpomaluje příjem dat ze SCTP.

Další problémy v produkci

  • Ztráta File: Objekt není serializovatelný; ukládat jako ArrayBuffer nebo Blob v IndexedDB.
  • Opětovné připojení: Signaling může selhat dříve než DataChannel; sledovat peer_left jako normální případ.
  • Safari překvapení: Samostatné zpracování ordered/unordered režimů a bufferů.

Co je důležité:

  • dc.send() – zápis do SCTP bufferu, ne síťový send; vždy monitorujte bufferedAmount.
  • Přizpůsobte velikost chunku: 1MB P2P, 64KB server_relay pro retransmise.
  • Čekejte na transfer_ack po drainu před transfer_done.
  • Používejte překrývající se I/O a dynamické timeouty pro výkon.
  • Kvóty na relay kontrolujte oběstranně; předběžný health-check TURN.

— Editorial Team

Advertisement 728x90

Číst dál