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.
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:
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.
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_leftjako 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 monitorujbufferedAmount.- Dostosuj rozmiar chunka: 1MB P2P, 64KB server_relay dla retransmisji.
- Czekaj na
transfer_ackpo opróżnieniu przedtransfer_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
Brak komentarzy.