## WebRTC DataChannel 파일 전송: 프로덕션 환경에서의 버퍼 관리와 안정성
WebRTC DataChannel에서 dc.send() 메서드는 데이터를 발신자 SCTP 버퍼에 큐잉하지만, 수신자에게 전달된다는 보장은 없습니다. 느린 릴레이 연결에서 버퍼가 메가바이트 단위로 가득 차고, 진행률이 100%에 도달해도 파일이 잘리게 됩니다. 전형적인 P2P 파일 공유 설정은 조정을 위한 시그널링 서버, 주요 경로인 직접 P2P 연결, 그리고 대체 경로인 TURN 릴레이를 포함합니다. 서버는 콘텐츠를 저장하지 않고 전송 상태를 관리합니다: 공유 생성부터 수신자 대기열까지.
주요 과제: 버퍼 백프레셔, 페이지 새로고침 시 File 객체 손실, 피어 간 준비 상태 불일치, 재연결. 이 분석은 릴레이 제한과 커스텀 TURN 지원을 가진 프로덕션 구현에서 도출됐습니다.
서버 측 파일 저장 없는 아키텍처
서버는 REST API와 WebSocket으로 조정합니다: 공유 생성, ECDSA로 연결 서명, 상태 추적. 공유 수명 주기—active → matched → transferring → active—로 발신자가 여러 수신자에게 재사용 가능. 대기열은 새 참여자에게 자동 분배.
종단 상태: expired, cancelled. Failed 상태는 reactivate로 복구. 콘텐츠는 DTLS로 종단 간 암호화되며, 릴레이는 데이터에 접근하지 않고 트래픽만 프록시합니다.
문제 1: TURN 대체 경로와 최적 청크 크기
P2P는 대칭 NAT, 방화벽, 모바일 네트워크로 인해 항상 가능하지 않습니다. 연결 유형은 ICE 후보에서 결정: p2p, server_relay (쿼터 있는 coturn), custom_relay (무제한 커스텀 TURN).
청크 크기는 재전송 지연 최소화로 적응:
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;
}
}
server_relay에서 작은 청크는 복구를 가속: 손실 패킷은 전체 재전송되며, 지연은 크기에 비례. iceTransportPolicy: 'relay'인 전용 RTCPeerConnection으로 TURN 사전 확인은 약 5초 소요.
문제 2: 송신 루프의 백프레셔와 중첩 I/O
단순 루프는 청크를 순차 전송하며 file.slice().arrayBuffer()에서 대기. 프로덕션 구현은 더블 버퍼링과 고/저 워터마크 사용:
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;
}
- 중첩 I/O: 현재 청크 전송 중 다음 청크 읽기 병렬 처리.
- 동적 타임아웃:
estimatedDrainMs = (bufferedAmount / speed) * 1000, 5~60초 제한. - 정확한 진행률:
offset - dc.bufferedAmount, 완전 드레인 전 99% 초과 안 함.
완료는 버퍼 드레인 + 수신자 transfer_ack 후 transfer_done 필요.
문제 3: 수신자 측과 디스크 백프레셔
수신자는 DataChannel 버퍼 재사용 피하기 위해 ArrayBuffer 복사:
const chunk = event.data.slice(0); // 복사
writeChain = writeChain.then(() => writer.write(chunk));
if (bytesReceived === fileInfo.size) {
dc.send(JSON.stringify({ type: 'transfer_ack' }));
}
writeChain은 순차 프라미스 체인. 느린 디스크(USB 등)는 스택 위로 백프레셔 전파, SCTP 데이터 유입 throttling.
추가 프로덕션 과제
- 파일 손실:
File객체 직렬화 안 됨; IndexedDB에 ArrayBuffer 또는 Blob으로 저장. - 재연결: 시그널링이 DataChannel 전에 끊어질 수 있음;
peer_left를 정상 처리. - Safari 특성: ordered/unordered 모드와 버퍼 별도 처리.
핵심 요약:
dc.send()는 SCTP 버퍼에 씀, 네트워크 아님;bufferedAmount항상 모니터링.- 청크 크기 적응: P2P 1MB, server_relay 64KB로 재전송 최적화.
- 드레인 후
transfer_ack대기 후transfer_done신호. - 중첩 I/O와 동적 타임아웃으로 최고 성능.
- 릴레이 쿼터 양방향 확인; TURN 헬스 체크 사전 실행.
— Editorial Team
아직 댓글이 없습니다.