Powrót do strony głównej

WebRTC DataChannel: pliki bez błędów bufora

Artykuł analizuje problemy produkcyjne przesyłania plików przez WebRTC DataChannel: zarządzanie buforem SCTP, adaptacja chunków pod relay, backpressure na odbiorniku. Zawiera przykłady kodu overlapped I/O, dynamic timeouts i mechanizmów ACK dla niezawodności.

Pliki w WebRTC: jak uniknąć obcinania na relay
Advertisement 728x90

Przesyłanie plików przez WebRTC DataChannel: zarządzanie buforem i niezawodność w produkcji

W WebRTC DataChannel metoda dc.send() umieszcza dane w buforze SCTP nadawcy, ale nie gwarantuje dostarczenia odbiorcy. Na wolnych połączeniach przez relay bufor wypełnia się megabajtami, pasek postępu osiąga 100%, a plik okazuje się obcięty. Architektura typowego P2P-menedżera wymiany plików obejmuje serwer sygnalizacyjny do koordynacji, bezpośrednie połączenie jako główną ścieżkę i TURN-relay jako fallback. Serwer nie przechowuje treści, ale zarządza stanem transmisji: od utworzenia udziału po kolejkę odbiorców.

Kluczowe wyzwania: backpressure z bufora, utrata obiektu File przy odświeżeniu strony, rozbieżność gotowości stron i ponowne połączenia. Analiza oparta na implementacji produkcyjnej z limitami na relay i wsparciem dla custom TURN.

Architektura bez przechowywania plików na serwerze

Serwer koordynuje przez REST API i WebSocket: tworzy udziały, podpisuje połączenia ECDSA, śledzi stany. Cykl życia udziału — active → matched → transferring → active — pozwala reutilizować nadawcę dla wielu odbiorców. Kolejka automatycznie rozdziela nowych uczestników.

Google AdInline article slot

Stany terminalne: expired, cancelled. Failed przywracany przez reactivate. Treść szyfrowana DTLS end-to-end, relay proxy'uje ruch bez dostępu do danych.

Problem 1: Fallback na TURN i optymalny rozmiar chunka

P2P nie zawsze możliwe z powodu symetrycznego NAT, firewalli czy sieci mobilnych. Typ połączenia określa się po kandydatach ICE: p2p, server_relay (coturn z limitami), custom_relay (własny TURN bez limitów).

Rozmiar chunka dostosowywany dla minimalizacji opóźnień retransmisji:

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

Małe chunki na server_relay przyspieszają regenerację: zagubiony pakiet retransmitowany w całości, opóźnienie rośnie z rozmiarem. Wstępna weryfikacja TURN przez osobne RTCPeerConnection z iceTransportPolicy: 'relay' trwa 5 sekund.

Problem 2: Backpressure i nakładające się I/O w pętli wysyłania

Naiwna pętla wysyła chunki sekwencyjnie, marnując czas na file.slice().arrayBuffer(). Implementacja używa podwójnego buforowania i progów 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;
}
  • Nakładające się I/O: Odczyt następnego chunka równolegle do wysyłania bieżącego.
  • Dynamiczny timeout: estimatedDrainMs = (bufferedAmount / speed) * 1000, ograniczony do 5–60 s.
  • Rzetelny postęp: offset - dc.bufferedAmount, nie wyżej niż 99% do opróżnienia.

Zakończenie: opróżnienie bufora + transfer_ack od odbiorcy przed transfer_done.

Google AdInline article slot

Problem 3: Strona odbiorcy i backpressure dysku

Odbiorca kopiuje ArrayBuffer, by uniknąć reużycia bufora DataChannel:

const chunk = event.data.slice(0);  // Kopiowanie
writeChain = writeChain.then(() => writer.write(chunk));

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

writeChain — sekwencyjny łańcuch promise'ów. Wolny dysk (USB) generuje backpressure w górę stosu: spowalnia odbiór danych z SCTP.

Dodatkowe problemy produkcji

  • Utrata File: Obiekt nie serializuje się; zapisywać jako ArrayBuffer lub Blob w IndexedDB.
  • Ponowne połączenie: Signaling może paść wcześniej niż DataChannel; śledzić peer_left jako normalny przypadek.
  • Safari-sursy: Osobne traktowanie trybów ordered/unordered i buforów.

Co ważne:

  • dc.send() — zapis do bufora SCTP, nie wysyłka sieciowa; zawsze monitoruj bufferedAmount.
  • Dostosuj rozmiar chunka: 1MB P2P, 64KB server_relay dla retransmisji.
  • Czekaj na transfer_ack po opróżnieniu przed transfer_done.
  • Używaj nakładających się I/O i dynamicznych timeoutów dla wydajności.
  • Limity relay sprawdzaj dwustronnie; wstępny health-check TURN.

— Editorial Team

Advertisement 728x90

Czytaj dalej