Zurück zur Startseite

WebRTC DataChannel: Dateien ohne Pufferfehler

Der Artikel analysiert Produktionsprobleme des Dateitransfers über WebRTC DataChannel: Verwalten des SCTP-Puffers, Anpassen der Chunks ans Relay, Backpressure am Empfänger. Code-Beispiele für überlappte I/O, dynamische Timeouts und ACK-Mechanismen zur Zuverlässigkeit werden bereitgestellt.

Dateien in WebRTC: Wie vermeidet man Abschneidungen am Relay
Advertisement 728x90

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.

Google AdInline article slot

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:

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;
  }
}

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.

Google AdInline article slot

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_left als normal behandeln.
  • Safari-Eigenheiten: Ordered/unordered-Modi und Puffer separat handhaben.

Wichtige Erkenntnisse:

  • dc.send() schreibt in den SCTP-Puffer, nicht direkt ins Netz; immer bufferedAmount überwachen.
  • Chunk-Größen anpassen: 1 MB für P2P, 64 KB für server_relay zur Optimierung von Retransmits.
  • Auf transfer_ack nach Abfluss warten, bevor transfer_done signalisiert 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

Advertisement 728x90

Weiterlesen