홈으로 돌아가기

WebRTC DataChannel: 버퍼 오류 없이 파일

이 기사는 WebRTC DataChannel을 통한 파일 전송의 프로덕션 문제를 분석합니다: SCTP buffer 관리, relay에 청크 적응, 수신자 backpressure. 신뢰성을 위한 overlapped I/O, dynamic timeout 및 ACK 메커니즘의 코드 예제가 제공됩니다.

WebRTC의 파일: relay에서 잘림 방지 방법
Advertisement 728x90

## WebRTC DataChannel 파일 전송: 프로덕션 환경에서의 버퍼 관리와 안정성

WebRTC DataChannel에서 dc.send() 메서드는 데이터를 발신자 SCTP 버퍼에 큐잉하지만, 수신자에게 전달된다는 보장은 없습니다. 느린 릴레이 연결에서 버퍼가 메가바이트 단위로 가득 차고, 진행률이 100%에 도달해도 파일이 잘리게 됩니다. 전형적인 P2P 파일 공유 설정은 조정을 위한 시그널링 서버, 주요 경로인 직접 P2P 연결, 그리고 대체 경로인 TURN 릴레이를 포함합니다. 서버는 콘텐츠를 저장하지 않고 전송 상태를 관리합니다: 공유 생성부터 수신자 대기열까지.

주요 과제: 버퍼 백프레셔, 페이지 새로고침 시 File 객체 손실, 피어 간 준비 상태 불일치, 재연결. 이 분석은 릴레이 제한과 커스텀 TURN 지원을 가진 프로덕션 구현에서 도출됐습니다.

서버 측 파일 저장 없는 아키텍처

서버는 REST API와 WebSocket으로 조정합니다: 공유 생성, ECDSA로 연결 서명, 상태 추적. 공유 수명 주기—active → matched → transferring → active—로 발신자가 여러 수신자에게 재사용 가능. 대기열은 새 참여자에게 자동 분배.

Google AdInline article slot

종단 상태: expired, cancelled. Failed 상태는 reactivate로 복구. 콘텐츠는 DTLS로 종단 간 암호화되며, 릴레이는 데이터에 접근하지 않고 트래픽만 프록시합니다.

문제 1: TURN 대체 경로와 최적 청크 크기

P2P는 대칭 NAT, 방화벽, 모바일 네트워크로 인해 항상 가능하지 않습니다. 연결 유형은 ICE 후보에서 결정: p2p, server_relay (쿼터 있는 coturn), custom_relay (무제한 커스텀 TURN).

청크 크기는 재전송 지연 최소화로 적응:

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

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_acktransfer_done 필요.

Google AdInline article slot

문제 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

Advertisement 728x90

다음 읽기