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.
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í:
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.
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_leftjako 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 monitorujtebufferedAmount.- Přizpůsobte velikost chunku: 1MB P2P, 64KB server_relay pro retransmise.
- Čekejte na
transfer_ackpo drainu předtransfer_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
Zatím žádné komentáře.