P2P file transfer (with WebRTC, RxJS)

Jurung Park
Pagecall Engineering
7 min readNov 20, 2018

왜 P2P로 파일을 전송하는가?

HTTP라는 아주 훌륭한 프로토콜이 있음에도 불구하고 우리는 왜 WebRTC DataChannel을 이용하여 사용자의 브라우저끼리 직접 파일을 전송할 수 있도록 했을까? 이유는 아래와 같은 P2P 통신이 갖는 이점을 취하고 싶어서였다. 더군다나 PageCall은 이미 사용자간의 모든 통신을 WebSocket이나 HTTP가 아닌 WebRTC를 통해 처리하고 있었으므로 파일 또한 P2P로 주고 받을 수 있어야 완전한 커뮤니케이션의 분산화가 가능했다.

  • 보안
    파일정보가 서버에 잠깐 동안이라도 저장되지 않는다. 법률, 금융 업계에서 PageCall이 활용될 때 고객의 민감한 정보가 담긴 파일이 당사자들을 제외한 어떤 물리적 장치에도 업로드되지 않게 할 수 있다.
  • 속도
    파일서버를 사용한다면 A에서 B로 파일을 전송할 때, A가 먼저 파일 서버에 파일을 업로드한 뒤, B가 그 파일을 다운로드 받게 된다. 하지만 P2P로 직접 전송할 수 있게 되면 A의 업로드가 곧 B의 다운로드가 된다. 여기 더해서 만약 두 당사자가 논리적, 물리적으로 가까운 네트워크(ex. 동일한 건물)에 위치한다면 데이터가 더 효율적인 경로로 이동할 수 있게 된다.
  • 비용
    별도의 파일 서버를 관리하는데 들어가는 물리적, 인적 비용을 고려하지 않아도 된다.

사전 지식 1 — WebRTC

WebRTC(Web Real-Time Communication)는 browser, native mobile application에서 API를 통해 P2P로 실시간 통신을 가능하게 하는 오픈소스 프로젝트이다. 이 글에서는 브라우저에서의 WebRTC를 다루고, 그 중에서도 P2P로 데이터 통신을 가능하게 하는 DataChannel에 중점을 두고 설명한다.
Chrome, Firefox 등의 브라우저에는 RTCPeerConnection이라는 클래스가 글로벌하게 노출되어있는데 RTCPeerConnection의 인스턴스를 통해 P2P로 영상/음성 미디어 스트림과 임의의 데이터를 브라우저에서 브라우저로 직접 통신할 수 있다. 이 때 데이터 통신은 RTCDataChannel이라는 인터페이스를 이용하여 이뤄지며 이 글에서는 RTCDataChannel의 특성을 설명하고 이것을 이용한 파일 전송을 다룬다.

RTCDataChannel의 사용법은 매우 간단하다. 송신자측에서 RTCDataChannel.send()로 패킷을 보내면 수신자측에서 RTCDataChannel.onmessage이벤트 핸들러를 통해 패킷을 받을 수 있도록 쉽게 설계되어있다.

이 글에서 생략된 부분은 P2P 연결 초반에 필요한 Signaling단계와 미디어 스트림의 통신이다. WebRTC와 관련된 더 자세한 내용은 아래 홈페이지에서 학습할 수 있다. https://webrtc.org/

사전 지식 2 — RxJS

RxJS는 Reactive Extensions 의 JS 버전으로 Observable 패턴을 활용해 비동기처리를 쉽게 구현할 수 있게 도와주는 라이브러리이다. 이 글에서는 valve$를 구현하기 위해 BehaviorSubject를 활용했다. RxJS를 이용하여 파이프로 흘러가는 비동기적인 데이터들을 직관적으로 처리할 수 있었다.

RxJS에 대한 더 자세한 내용은 아래 공식 홈페이지에서 확인할 수 있다.
https://rxjs-dev.firebaseapp.com/

Image result for rxjs

RTCDataChannel의 특성

  • packet 크기 제한
    RTCDataChannel.send()로 보낼 수 있는 데이터의 크기에 제한이 있다. 브라우저 마다 다를 수 있겠지만 64kb 이상의 패킷을 보낼 경우 데이터가 손실될 위험이 있다.
  • buffer 크기 제한
    RTCDataChannel.send()로 시간당 보낼 수 있는 데이터의 크기에 제한이 있다. 너무 많은 데이터를 한꺼번에 보내면 buffer가 가득차서 데이터가 유실될 수 있다.
    RTCDataChannel.bufferedAmount를 통해 buffer 사용량을 알 수 있으며, RTCDataChannel.onbufferedamountlow를 통해 buffer 수위가 특정값 이하로 낮아졌음을 알 수 있다.

DrainablePacket 구현

파일과 그 메타데이터를 넘겨주면 적절한 크기의 Chunk로 쪼개어 적당한 속도로 방출하는 DrainablePacket을 구현하였다. DrainablePacketvalve$DrainablePacket을 사용하는 부분에서 buffer의 상태에 따라 조절하게 된다.

아래 DataChannel.ts에서 DrainablePacket을 어떤 식으로 생성하고 valve$를 조절하는지 알 수 있다. packet을 보낼 수 있을 때 밸브를 열고, 보낼 수 없을 때는 밸브를 닫아 packet이 더 이상 나오지 않게 통제한다.

RTCDataChannel의 buffer가 항상 일정한 수준으로 유지되도록 밸브를 조절하는 방식이 꽤나 재밌다.

아래는 실제로 우리제품에 적용한 상수들이다. DrainSpeed는 buffer의 수위가 적정수준으로 유지될 수 있게 하는 Chunk 생성 속도이며 한번에 15개의 Chunk들이 만들어지도록 세팅되어있다.

DrainablePacket의 작동 방식

DrainablePacket의 가장 큰 특징은 외부에서 조절되는 밸브로 흐름이 통제된다는 것이다. 밸브는 주로 RTCDataChannel의 buffer 수위에 따라 조절되는데 buffer의 수위가 BufferMax 보다 높아지면 밸브가 잠기고 BufferMin 보다 낮아지면 다시 열리게 되어있다.

또한 File의 일부분을 Chunk로 만드는 작업을 DrainablePacket이 초기화 될 때 한꺼번에 다 처리하는 것이 아니라 필요할 때마다 즉시 Chunk를 만들어서 내보내준다. 만약 초기에 모든 Chunk들을 한번에 준비해두면 Javascript의 heap용량을 많이 차지해서 1GB 이상의 대용량 파일을 처리할 때 heap용량이 부족해지는 사태가 벌어질 수 있다.

P2P 파일 전송 사용 후기

현재 구동되고 있는 PageCall 제품에서 이러한 P2P 파일 전송 기능이 사용되고 있다. 기대했던 대로 월등한 속도의 향상이 있었다. 기존에는 사용자가 상대방에게 파일을 보내주고 싶을 때 다음과 같은 차례대로 파일이 전송되었다.

  1. 원하는 파일 AWS S3에 업로드
  2. 업로드 완료되면 RTCDataChannel 을 통해서 상대방에게 파일 URL이 담긴 시그널을 전송
  3. 시그널을 받은 상대방은 S3에서 해당 파일 다운로드

P2P로 파일전송을 할때는 이 모든 단계가 하나로 줄었다.

  1. 원하는 파일 상대방에게 전송. 끝.

사실 실제 제품에 이 아이디어를 적용하기 위해서 글에 나와있는 것보다 훨씬 귀찮고 까다로운 작업들이 필요했다. 예를 들어 파일로부터 적절한 Chunk들을 생성하기, 수신자측에서 Chunk들을 바르게 조립하기, 그리고 이러한 P2P 파일 전송 프로토콜을 한 단계감싸서 마치 서버에 HTTP요청을 보내듯 상대방에게 파일을 요청할 수 있는 상위 레벨 프로토콜을 구현하는 것 까지 굉장히 많은 작업이 필요했다.

하지만 우리의 이러한 작업들이 다 쓸모없는 일이 되어도 좋으니 언젠가 WebRTC의 표준으로 대용량 파일전송 API가 들어가면 좋겠다. 그렇게 된다면 많은 웹개발자들이 행복해질 것이다.

--

--