WebRTC DataChannel: Dateiübertragung, Puffer-Management und Zuverlässigkeit im Produktiveinsatz
Bei WebRTC DataChannel stellt die Methode dc.send() die Daten in den SCTP-Puffer des Senders in die Warteschlange, garantiert aber keine Lieferung beim Empfänger. Bei langsamen Relay-Verbindungen füllt sich der Puffer mit Megabytes an Daten, der Fortschrittsbalken erreicht 100 %, und die Datei wird abgeschnitten. Ein typisches P2P-Dateifreigabe-Setup umfasst einen Signaling-Server zur Koordination, direkte Peer-to-Peer-Verbindungen als primären Pfad und TURN-Relays als Fallback. Der Server speichert keinen Inhalt, sondern verwaltet Übertragungszustände: von der Freigabeerstellung bis zu den Empfängerwarteschlangen.
Wichtige Herausforderungen: Rückdruck durch den Puffer, Verlust des File-Objekts bei Seitenrefresh, unterschiedliche Bereitschaft der Peers und Rekonnektionen. Diese Analyse basiert auf einer produktiven Umsetzung mit Relay-Limits und eigener TURN-Unterstützung.
Architektur ohne serverseitige Dateispeicherung
Der Server koordiniert über REST-API und WebSocket: Er erstellt Freigaben, signiert Verbindungen mit ECDSA und verfolgt Zustände. Der Lebenszyklus einer Freigabe – active → matched → transferring → active – erlaubt dem Sender, sie für mehrere Empfänger wiederzuverwenden. Warteschlangen verteilen automatisch an neue Teilnehmer.
Endzustände: expired, cancelled. Failed-Zustände erholen sich über reactivate. Der Inhalt ist Ende-zu-Ende mit DTLS verschlüsselt, und Relays leiten Traffic weiter, ohne auf die Daten zuzugreifen.
Problem 1: TURN-Fallback und optimaler Chunk-Größe
P2P ist nicht immer möglich wegen symmetrischem NAT, Firewalls oder Mobilfunknetzen. Verbindungstypen werden aus ICE-Candidates bestimmt: p2p, server_relay (coturn mit Quoten), custom_relay (eigener TURN ohne Limits).
Die Chunk-Größe passt sich an, um Retransmissionsverzögerungen zu minimieren:
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;
}
}
Kleinere Chunks bei server_relay beschleunigen die Erholung: Ein verlorenes Paket wird komplett retransmittiert, mit Verzögerungen proportional zur Größe. Eine Vorab-Prüfung von TURN über eine dedizierte RTCPeerConnection mit iceTransportPolicy: 'relay' dauert ca. 5 Sekunden.
Problem 2: Rückdruck und überlappende I/O in der Sendeschleife
Eine naive Schleife sendet Chunks sequentiell und wartet bei file.slice().arrayBuffer(). Die produktive Umsetzung nutzt Double-Buffering und High/Low-Wassermarken:
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;
}
- Überlappende I/O: Liest den nächsten Chunk parallel zum Senden des aktuellen.
- Dynamischer Timeout:
estimatedDrainMs = (bufferedAmount / speed) * 1000, begrenzt auf 5–60 Sekunden. - Ehrlicher Fortschritt:
offset - dc.bufferedAmount, nie über 99 % bis vollständiger Abfluss.
Abschluss erfordert Pufferabfluss + transfer_ack vom Empfänger vor transfer_done.
Problem 3: Empfängerseitiger und Festplatten-Rückdruck
Der Empfänger kopiert ArrayBuffer, um DataChannel-Puffer-Wiederverwendung zu vermeiden:
const chunk = event.data.slice(0); // Kopie
writeChain = writeChain.then(() => writer.write(chunk));
if (bytesReceived === fileInfo.size) {
dc.send(JSON.stringify({ type: 'transfer_ack' }));
}
writeChain ist eine sequentielle Promise-Kette. Langsame Festplatten (z. B. USB-Sticks) erzeugen Rückdruck die gesamte Kette hoch und drosseln die SCTP-Datenaufnahme.
Weitere Herausforderungen im Produktiveinsatz
- Dateiverlust:
File-Objekte serialisieren nicht; speichern als ArrayBuffer oder Blob in IndexedDB. - Rekonnektionen: Signaling kann vor DataChannel abbrechen;
peer_leftals normal behandeln. - Safari-Eigenheiten: Ordered/unordered-Modi und Puffer separat handhaben.
Wichtige Erkenntnisse:
dc.send()schreibt in den SCTP-Puffer, nicht direkt ins Netz; immerbufferedAmountüberwachen.- Chunk-Größen anpassen: 1 MB für P2P, 64 KB für
server_relayzur Optimierung von Retransmits. - Auf
transfer_acknach Abfluss warten, bevortransfer_donesignalisiert wird. - Überlappende I/O und dynamische Timeouts für Höchstleistung nutzen.
- Relay-Quoten bilateral prüfen; TURN-Health-Checks vorab durchführen.
— Editorial Team
Noch keine Kommentare.