WebRTC in the real world: STUN, TURN and signaling

HTML5 Rocks

WebRTC는 P2P 통신을 가능하게 합니다.

그러나...

WebRTC는 여전히 다음과 같은 서버를 필요로 합니다.

  • 시그널링(Signaling)이라 불리는, 클라이언트들의 통신을 조정하기 위한 메타데이터의 교환 서버
  • 네트워크 주소 변환기(NAT) 및 방화벽 대응을 위한 서버

이 글에서 우리는 여러분에게 시그널링 서비스를 어떻게 구축하는지와 STUN과 TURN 서버들을 이용하여 실제 연결에 있어 이상한 사항들에 대해 어떻게 타협해야 하는지를 보여줄 것입니다. 또한 WebRTC 앱들이 어떻게 다단위의 호출을 제어할 수 있는지와 VoIP나 PSTN(전화라고도 하는)과 같은 서비스들과 어떻게 상호작용하는지에 대해 설명할 것입니다.

만약 여러분이 WebRTC의 기초에 대해 익숙하지 않다면, 이 글을 읽기 전에 WebRTC와 함께 시작하기를 찾아서 읽어보기를 강력하게 권장합니다.

시그널링(Signaling)이란 무엇인가?

시그널링은 통신 조정의 프로세스입니다. WebRTC 어플리케이션이 'call'을 초기화하기 위해서 클라이언트는 다음과 같은 정보의 교환을 필요로 합니다.

  • 통신을 열고 닫는데 사용되는 세션 컨트롤 메세지들.
  • 에러 메세지들.
  • 코덱이나 코덱 설정, 대역폭, 미디어 타입 같은 미디어 메타데이터.
  • 보안 연결을 수립하기 위해 사용되는 키 데이터.
  • 밖에서 보이는 것처럼 호스트의 IP 주소와 포트와 같은 네트워크 데이터.

이 시그널링 프로세스는 클라이언트에서 메세지를 앞/뒤로 전달하기 위한 방법을 필요로 합니다. 그 메커니즘은 WebRTC API에 의해 구현되지 않습니다. 여러분이 직접 구축하여야 합니다. 시그널링 서비스 구축을 위한 몇가지 방법을 아래에 기술할 것입니다. 그러나 그 전에 아주 작은 것부터 설명하도록 하겠습니다...

JSEP

중복의 회피와 호환성을 최대화하기 위해 기술, 시그널링 방법 그리고 프로토콜의 설정은 WebRTC 표준 규격에 정의되어 있지 않습니다. 이 접근 방식은 다음과 같이 JavaScript Session Establishment Protocol(JSEP)에 의해 설명할 수 있습니다.

WebRTC call 설정은 완전하게 규격화되어 미디어 통로(Media Plane)을 제어하지만 어플리케이션에 가능하면 최대한 시그널링 증가를 방지해야 합니다. 기존의 SIP나 Jingle call 시그널링 프로토콜, 혹은 소설 적용 사례같은 특정 어플리케이션이 특화된 무언가 다른 프로토콜을 사용에 적합한 다른 어플리케이션들이 그 근거일 것입니다. 이러한 접근에서 교환되어야 할 핵심 정보들은 요구되는 송수신과 미디어 통로(Media Plane)을 설정하기 위한 미디어 설정 정보를 구성하는데 필요한 멀티미디어 세션 설명(Description) 정보입니다.

또한 JSEP의 구조는 브라우저가 상태를 저장하는 것을 회피합니다. (즉, Signaling State Machine인 함수에 전달하는 경우) 이 경우도 문제가 있을 수 있습니다. 시그널링 데이터가 페이지가 리로딩되면서 매번 사라지는 경우가 그 예입니다. 대신 시그널링 상태는 서버에 저장될 수 있습니다.

JSEP architecture diagram
JSEP 구조

JESP는 다음과 같이 제안응답의 종단(Peer)간의 교환이 필요합니다.위에서 언급한 미디어 메타데이터. 제안들과 응답들은 다음과 같은 '세션 기술 프로토콜(SDP)' 형식입니다.

v=0
o=- 7614219274584779017 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS
m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126
c=IN IP4 0.0.0.0
a=rtcp:1 IN IP4 0.0.0.0
a=ice-ufrag:W2TGCZw2NZHuwlnf
a=ice-pwd:xdQEccP40E+P0L5qTyzDgfmW
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=mid:audio
a=rtcp-mux
a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:9c1AHz27dZ9xPI91YNfSlI67/EMkjHHIHORiClQe
a=rtpmap:111 opus/48000/2
…

이 모든 SDP의 까다로운 표현들이 실제로 무엇을 의미하는지 알고 싶으십니까? 그렇다면 IETF 예제들을 살펴보시기 바랍니다.

WebRTC는 제안이나 응답은 로컬이나 원격 기술(Description)로 설정되기 전에 SDP 텍스트에서 값의 편집을 통해 변경될 수 있도록 디자인되었음을 명심하여야 합니다. 예를 들어 apprtc.appspot.compreferAudioCodec() 함수는 기본 코덱과 비트레이트를 설정하기 위해 사용할 수 있습니다. SDP는 자바스크립트로 관리하기에 조금은 고통스럽고 WebRTC의 향후 버전은 대신 JSON을 사용하도록 하자는 논의가 있지만 SDP와 붙어있음으로 인한 몇가지 이점들이 있습니다.

RTCPeerConnection + 시그널링 : 제안, 응답 그리고 후보

RTCPeerConnection은 WebRTC 어플리케이션이 Peer 간의 연결을 생성하고 오디오와 비디오의 통신에 사용되는 API입니다.

이 프로세스를 초기화하기 위해 RTCPeerConnection는 다음과 같은 2가지 태스크를 가지고 있습니다.

  • 해상도나 가용한 코덱 정보 등의 로컬 미디어 상태들을 알아냅니다. 이것은 제안/응답 메커니즘에서 사용되는 메타데이터입니다.
  • 후보들로 알려진 어플리케이션 호스트의 잠재적인 네트워크 주소들을 가져옵니다.

일단 로컬 데이터가 알아내지면, 시그널링 메커니즘를 통해 리모트 Peer와 반드시 교환되어야 합니다.

앨리스가 이브를 부르려고 시도하는 것을 상상해 보시기 바랍니다. 완전한 제안/응답 메커니즘에 대한 이런저런 뒷얘기들을 얘기하도록 하겠습니다.

  1. 앨리스가 RTCPeerConnection 객체를 생성합니다.
  2. 앨리스가 createOffer() 메소드를 사용하여 제안(SDP Session Description)을 생성합니다.
  3. 앨리스가 제안과 함께 setLocalDescription()를 호출합니다.
  4. 앨리스는 제안을 문자열화하고 시그널링 메커니즘을 이용하여 이브에게 보냅니다.

  5. 이브는 앨리스의 제안을 가지고 setRemoteDescription()를 호출하였으므로 그녀의 RTCPeerConnection가 앨리스의 설정을 알게됩니다.
  6. 이브는 em>createAnswer()를 호출하고 이에 대해 로컬 세션 정보(Local Session Description), 즉 이브의 응답을 인자로 전달하는 성공 콜백 함수를 호출합니다.
  7. 이브는 setLocalDescription()의 호출을 통해 그녀의 응답을 로컬 기술(Description)으로 설정합니다.
  8. 그리고나서 이브는 시그널링 메커니즘을 사용하여 그녀의 문자열화된 응답을 앨리스에게 다시 전송합니다.
  9. 앨리스는 setRemoteDescription()을 사용하여 이브의 응답을 원격 세션 기술(Description)으로 설정합니다.

맙소사!!

앨리스와 이브는 또한 네트워크 정보의 교환이 필요합니다. '후보들의 발견' 표시는 네트워크 인터페이스의 탐색 절차와 ICE framework를 사용한 포팅과 관련이 있습니다.

  1. 앨리스는 onicecandidate 핸들러를 가진 RTCPeerConnection 객체를 생성합니다.
  2. 핸들러는 네트워크가 가용 대기 상태가 되면 호출됩니다.
  3. 핸들러에서 앨리스는 이브에게 그들의 시그널링 채널을 통해 문자열화된 후보 데이터를 전송합니다.
  4. 이브가 후보 메세지를 앨리스로부터 수신할 때 원격 Peer 기술(Description)으로의 후보를 추가하는 addIceCandidate()를 호출합니다.

JSEP는 초기 제안 뒤에 피호출자에 대한 후보들을 증분 제공하는 호출자를 가능하게 하는ICE Candidate Trickling와 피호출자에게 호출시 동작을 시작하고 도착한 모든 호부들에 대한 대기없이 연결을 설정할 수 있는 기능을 지원하고 있습니다.

시그널링을 위한 WebRTC 코딩하기

아래는 완전한 시그널링 프로세스를 요약한 W3C 코드 예제입니다. 코드는 SignalingChannel 같은 어떤 시그널링 메커니즘이 존재하고 있다고 가정합니다. 시그널링은 아래에서 훨씬 더 자세하게 논의하도록 하겠습니다.

var signalingChannel = new SignalingChannel();
var configuration = {
  'iceServers': [{
    'url': 'stun:stun.example.org'
  }]
};
var pc;

// 초기화하려면 start()를 호출합니다.

function start() {
  pc = new RTCPeerConnection(configuration);

  // 다른 Peer에 대한 ICE 후보들을 전송합니다.
  pc.onicecandidate = function (evt) {
    if (evt.candidate)
      signalingChannel.send(JSON.stringify({
        'candidate': evt.candidate
      }));
  };

  // '제안'이 생성되면 'negotiationneeded' 이벤트를 발생합니다.
  pc.onnegotiationneeded = function () {
    pc.createOffer(localDescCreated, logError);
  }

  // 일단 원격 스트림이 도착하면 원격 비디오 엘리먼트 안에 그것을 보여줍니다. element
  pc.onaddstream = function (evt) {
    remoteView.src = URL.createObjectURL(evt.stream);
  };

  // 로컬 스트림을 받으면 그것을 자체 뷰에서 보여주고 전송을 위해 추가합니다.
  navigator.getUserMedia({
    'audio': true,
    'video': true
  }, function (stream) {
    selfView.src = URL.createObjectURL(stream);
    pc.addStream(stream);
  }, logError);
}

function localDescCreated(desc) {
  pc.setLocalDescription(desc, function () {
    signalingChannel.send(JSON.stringify({
      'sdp': pc.localDescription
    }));
  }, logError);
}

signalingChannel.onmessage = function (evt) {
  if (!pc)
    start();

  var message = JSON.parse(evt.data);
  if (message.sdp)
    pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
      // if we received an offer, we need to answer
      if (pc.remoteDescription.type == 'offer')
        pc.createAnswer(localDescCreated, logError);
    }, logError);
  else
    pc.addIceCandidate(new RTCIceCandidate(message.candidate));
};

function logError(error) {
  log(error.name + ': ' + error.message);
}

액션에서 제안/응답과 후보 교환 프로세스를 보기 위해서, simpl.info/pc의 '단일 페이지' 비디오 채팅 예제의 콘솔 로그를 살펴봅시다. 만약 여러분이 원한다면 크롬의 chrome://webrtc-internals 페이지나 오페라의 opera://webrtc-internals 페이지에서 WebRTC 시그널링의 완전한 덤프(Dump)나 상태를 다운로드할 수 있습니다.

Peer의 탐색

어떻게 내가 대화할 사람을 찾을 수 있을까요? —라고 말하는 것은 복잡한 방법입니다.

전화는 우리가 가진 전화번호와 디렉토리들에 호출을 합니다. 온라인 비디오 채팅과 메세지를 위해서도 우리는 신원과 참석 관리 시스템과 사용자들의 세션 초기화 방법이 필요합니다. WebRTC 앱들은 클라이언트들이 호출에 대한 시작이나 참여를 서로에게 신호할 수 있도록 방법이 필요합니다.

Peer 탐색 메커니즘은 WebRTC에 의해 정의되지 않으며 그렇게 할 수 있는 옵션도 없습니다. 절차는 다음과 같이 URL을 통해 이메일이나 메세지를 하는 것처럼 간단합니다. talky.io, tawk.com 그리고 browsermeeting.com과 같은 비디오 채팅 어플리케이션들은 커스텀 링크의 공유를 통해 호출하여 사람들을 초대할 수 있습니다. 개발자 Chris Ball은 WebRTC 호출 시 참가자들간의 인스턴트 메세지, 이메일이나 집 비둘기와 같이 그들이 원하는 그 어떤 메세지 서비스에 의해서도 메타 데이터를 교환할 수 있도록 하는 serverless-webrtc라는 아주 흥미로운 실험을 구축했습니다.

어떻게 시그널링 서비스를 구축할 수 있을까요?

반복하자면 시그널링 프로토콜들과 메커니즘들은 WebRTC 표준에서 정의하고 있지 않습니다. 어떤 것을 선택하더라도 클라이언트들 사이에 시그널링 메세지들과 어플리케이션 데이터를 교환하기 위한 중간 서버가 필요할 수 있습니다. 슬프게도 웹 앱은 단순하게 인터넷으로 '나와 접속해주게, 친구!'라고 외칠 수 없습니다.

고맙게도 시그널링 메세지들은 작고 호출의 시작 단계에서 대부분 교환됩니다. apprtc.appspot.comsamdutton-nodertc.jit.su로 시험해본 결과 비디오 챗 세션의 경우 시그널링 서비스에 의해 다뤄지는 전체 메세지가 약 30~45개 정도 모든 메세지의 총 크기는 약 10KB 정도라는 사실을 확인했습니다.

게다가 대역폭 용어와 관계되어 요청되지 않은 것으로는 WebRTC 시그널링 서비스는 더 많은 과정이나 메모리를 소모하지 않기 때문에 단지 메세지의 릴레이나 적은 량의 세션 상태 데이터(클라이언트들이 접속 여부 같은)만 획득하는 것만 필요합니다.

Note: 시그널링 메커니즘은 앱과 사용자 사이의 메세지와 같은 어플리케이션 데이터의 통신에도 사용될 수 있는 세션 메타 데이터의 교환에 사용되었습니다. 그게 바로 메세징 서비스(Messaging Service)입니다!

서버에서 클라이언트측으로의 메세지 푸시

시그널링을 위한 메세지 서비스는 클라이언트에서 서버로 그리고 서버로부터 클라이언트로의 양방향이어야 합니다. 양방향 통신은 HTTP 클라이언트/서버의 요청/응답 모델과 대비되지만 롱폴링(long polling)과 같은 다양한 핵들이 웹서버 상에서 동작하는 서비스로부터 브라우저에서 동작하는 웹앱으로 데이터를 푸시하기 위해 몇년이 넘도록 개발되어 왔습니다.

보다 최근에는 EventSource API폭넓게 구현되었습니다. 이는 HTTP를 통해 웹서버로부터 브라우저 클라이언트에 데이터를 보내는 '서버-송신 이벤트(sever-sent events)'를 가능하게 하였습니다. simpl.info/es에서 간단한 데모를 확인할 수 있습니다. EventSource는 단방향 메시징을 위해 디자인되었으나 다음과 같이 XHR과의 결합을 통해 시그널링 메세지의 교환을 위한 서비스 구축에 사용될 수 있습니다. 시그널링 서비스는 XHR 요청을 통해 호출자로부터 메세지를 넘겨받아 EventSource를 통해 그것을 피호출자에게 푸시할 수 있습니다.

웹소켓(WebSocket)은 보다 자연스러운 솔루션으로 전이중(Full Duplex) 클라이언트-서버 통신을 위해 디자인되었습니다. (메세지는 양방향으로 동시에 전달될 수 있습니다.) 순수 웹소켓이나 서버-송신 이벤트(Server-Sent Events, EventSource)와 함께 구축될 수 있다는 시그널링 서비스의 장점 하나로 이러한 API들이 PHP, 파이썬 그리고 루비같은 언어를 사용하여 대부분의 웹 호스팅 패키지로 다양한 일반 웹 프레임 상에서 구현될 수 있다는 것입니다.

브라우저의 약 75%가 웹소켓을 지원하고 있으며 이보다 더 중요한 사실은 모든 브라우저가 WebRTC뿐만이 아니라 웹소켓을 데스크탑과 모바일 양쪽에서 지원된다는 점입니다. TLS는 메세지들이 암호화되지 않은 상태로 가로채지지 않도록 하기 위한 모든 연결들을 위해 사용될 수 있으며 또한 프록시 탐색으로 인한 문제의 감소를 위해서도 사용됩니다. (웹소켓과 프록시 탐색에 대한 더 자세한 정보는 O'Reilly에서 곧 출판될 Ilya Grigorik의 High Performance Browser NetworkingWebRTC 챕터을 읽어보시기 바랍니다.) Peter Lubber의 웹소켓 치트 시트(WebSocket Cheat Sheet)는 웹소켓 클라이언트와 서버에 대한 추가적인 정보를 담고 있습니다.

고전적인 apprtc.appspot.com WebRTC 비디오 챗 어플리케이션을 위한 시그널링은 앱엔진 백엔드와 웹 클라이언트 간의 푸시 통신에 대한 시그널링을 가능하게 하기 위한 코멧(Comet) 기술(롱폴링, Long Polling)을 사용하는 구글 앱엔진 채널 API(Google App Engine Channel API)를 통해 이루어집니다. (웹소켓을 지원에 대한 앱엔진의 오래된 버그가 있습니다.) HTML5Rocks WebRTC 관련 글에서 이 앱에 대한 자세한 코드 설명을 보실 수 있습니다.)

The apprtc.appspot.com video chat application
동작 중인 apprtc

또한 Ajax를 통해 메세징 서버에 대한 반복적인 폴링을 통해 WebRTC 클라이언트의 획득을 통한 시그널링 핸들링이 가능하지만 이 방식은 모바일 디바이스에서 심각한 문제가 될 수 있는 과다한 네트워크 요청을 발생합니다. 세션이 연결될 후에도 피어(peer)들은 다른 단말에 의한 세션 종료나 변경을 확인하기위한 시그널링 메세지를 폴링하여야 합니다. WebRTC Book의 앱 예제는 폴링 빈도에 대한 몇가지 최적화 옵션을 보여줍니다.

시그널링의 확장(Scaling signaling)

시그널링 서비스가 클라이언트마다 비교적 적은 대역폭과 CPU를 필요로 하는데도 불구하고 인기 어플리케이션을 위한 시그널링 서버들은 아마 많은 양의 메세지를 높은 수준의 병렬적인 방식으로 다루어야 할 것입니다. 많은 량의 통신을 가지는 WebRTC 앱들은 심각한 부하를 다룰 수 있는 시그널링 서버들이 필요합니다. 이에 대해 논의하지는 않겠지만 대량의 메세지의 처리 시 (TokBox의 루머(Rumour) 서비스에서 사용되고 있는) ZeroMQ와 같은 시그널링을 사용하기 위한 다양한 고성능의 실시간 메세징 옵션들이 있습니다.

Socket.io를 이용한 Node.js의 시그널링 서비스의 구축

아래 코드는 Node.js 상에서 Socket.io를 이용하여 구축한 시그널링 서비스를 사용하는 간단한 웹 어플리케이션입니다. Socket.io의 디자인은 메세지들의 교환을 위한 서비스를 쉽게 구축할 수 있도록 만들어져 있으며 Socket.io는 내장된 컨셉인 'rooms'로 인해 WebRTC에 꽤 적합합니다.

Socket.io는 다음 순서로 웹소켓을 사용합니다. 어도비 플래시 소켓(Adobe Flash Socket), AJAX 롱폴링(AJAX long polling), AJAX 멀티파트 스트리밍(AJAX multipart streaming), 영원한 Iframe과 JSONP 폴링. 다양한 백엔드들로 포팅되었지만 아마 가장 유명한 버전은 Node.js 버전일 것입니다. 아래 예제에서 Socket.io를 사용합니다.

이 예제는 WebRTC가 아닙니다. 그저 시그널링을 웹앱에 어떻게 구축하는지를 보여주고자 디자인하였습니다. (위에서 시그널링을 위한 WebRTC 코딩을 살펴보시기 바랍니다.) 클라이언트들이 방(room)에 연결되고 메세지를 교환할 때 어떤 일이 일어나는지를 보여주는 콘솔 로그를 볼 수 있습니다. WebRTC 코드랩은 어떻게 이 예제를 완전한 WebRTC 비디오 챗 어플리케이션으로 통합하는지에 대한 단계별 과정을 알려줍니다. 코드랩 저장소의 5번째 단계에서 코드를 다운로드하거나 samdutton-nodertc.jit.su에서 실제로 진행해 볼 수 있습니다. 비디오 챗을 위해 URL을 2개의 브라우저에서 열어보시기 바랍니다.

다음과 같이 index.html 클라이언트가 있습니다.

<!DOCTYPE html>
<html>
  <head>
    <title>WebRTC client</title>
  </head>
  <body>
    <script src='/socket.io/socket.io.js'></script>
    <script src='js/main.js'></script>
  </body>
</html>

...그리고 클라이언트에서 참조하는 main.js 자바스크립트 파일이 있습니다.

var isInitiator;

room = prompt('Enter room name:');

var socket = io.connect();

if (room !== '') {
  console.log('Joining room ' + room);
  socket.emit('create or join', room);
}

socket.on('full', function (room){
  console.log('Room ' + room + ' is full');
});

socket.on('empty', function (room){
  isInitiator = true;
  console.log('Room ' + room + ' is empty');
});

socket.on('join', function (room){
  console.log('Making request to join room ' + room);
  console.log('You are the initiator!');
});

socket.on('log', function (array){
  console.log.apply(console, array);
});

다음은 완전한 서버 어플리케이션입니다.

var static = require('node-static');
var http = require('http');
var file = new(static.Server)();
var app = http.createServer(function (req, res) {
  file.serve(req, res);
}).listen(2013);

var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket){

  // 로그 서버와 클라이언트로 메세지를 전송하는 편의 함수
  function log(){
    var array = ['>>> Message from server: '];
    for (var i = 0; i < arguments.length; i++) {
      array.push(arguments[i]);
    }
      socket.emit('log', array);
  }

  socket.on('message', function (message) {
    log('Got message:', message);
    // 실제 앱에서는 방이 될 것입니다. (브로드캐스팅이 아니라)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function (room) {
    var numClients = io.sockets.clients(room).length;

    log('Room ' + room + ' has ' + numClients + ' client(s)');
    log('Request to create or join room ' + room);

    if (numClients === 0){
      socket.join(room);
      socket.emit('created', room);
    } else if (numClients === 1) {
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room);
    } else { // max two clients
      socket.emit('full', room);
    }
    socket.emit('emit(): client ' + socket.id + ' joined room ' + room);
    socket.broadcast.emit('broadcast(): client ' + socket.id + ' joined room ' + room);

  });

});

(이를 위해서 node-static에 대해 학습할 필요는 없습니다. 그냥 서버를 좀 더 쉽게 만든 것뿐입니다.)

localhost에서 이 앱을 실행하기 위해서 Node.js와 Socket.io 그리고 node-static의 설치가 필요합니다. Node는 nodejs.org에서 다운로드 할 수 있습니다. (설치는 직관적이고 빠릅니다.) Socket.io와 node-static을 설치하기 위해서는 Node Package Manager(NPM)을 다음과 같이 터미널의 어플리케이션 디렉토리에서 실행하시기 바랍니다.

npm install socket.io<br/> npm install node-static

서버를 시작하기 위해서 터미널에서 다음과 같은 명령을 어플리케이션 디렉토리에서 실행합니다.

node server.js

브라우저에서 localhost:2013을 열어봅시다. 다시 아무 브라우저에서나 새로운 페이지 탭이나 윈도우를 열고 localhost:2013를 열어봅시다. 무엇이 일어나는지 확인하기 위해서 크롬에서 콘솔을 확인하기 위해 크롬과 오페라의 개발자 도구에서 Command-Option-J나 Ctrl-Shift-J를 통해 이를 액세스할 수 있습니다.

시그널링을 위해 어떠한 접근 방법을 선택하던지 백엔드와 클라이언트 앱은 — 최소한 — 이 예제와 비슷한 서비스의 제공을 필요로할 것입니다.

시그널링을 위한 RTCDataChannel의 사용

시그널링 서비스는 WebRTC 세션의 초기화가 요구됩니다.

그러나 일단 양 피어(Peer) 간에 연결이 수립되고 나면, 이론적으로 RTCDataChannel은 시그널링 채널을 인계받습니다. 이것은 — 메세지가 바로 전달될 수 있어 — 시그널링에 대한 지연을 감소하고 시그널링 서버 대역폭과 프로세싱 비용을 감소하도록 합니다.

시그널링의 획득(Gotchas)

  • RTCPeerConnection은 setLocalDescription()이 호출되기 전까지 다음과 같이 후보들의 수집을 시작하지 않습니다. JSEP IETF 초안에서 관리합니다.
  • 다음과 같이 Trickle ICE(위를 참조하세요)의 이점을 취합니다. 후보가 도착하자마자 addIceCandidate() 를 호출합니다.

기존 시그널링 서버들

만약 직접 구축을 하고 싶지 않다면 위와 같이 Socket.io를 사용하고 WebRTC 클라이언트 자바스크립트 라이브러리와 통합된 다음과 같은 몇가지 WebRTC 시그널링 서버들이 있습니다.

  • webRTC.io: WebRTC를 위한 최초의 추상화 라이브러리 중의 하나.
  • easyRTC: 풀-스택 WebRTC 패키지.
  • Signalmaster: SimpleWebRTC 자바스크립트 라이브러리로 작성된 시그널링 서버.

...그리고 만약 그 어떤 코드도 작성하고 싶지 않다면 vLine, OpenTok이나 Asterisk같은 기업에서 제공하는 완전 상용 WebRTC 플랫폼들이 있습니다.

공개적으로 에릭슨(Ericsson)은 WebRTC 초기에 아파치 상에서 PHP를 사용한 시그널링 서버를 구축하였습니다. 이것은 이제 어디서도 쓸모가 없어졌지만 만약 비슷한 무언가를 고려하고 있다면 코드를 살펴볼 가치는 있습니다.

시그널링 보안

보안은 아무것도 발생하지 않게 하는 예술이다.

Salman Rushdie

암호화는 모든 WebRT 컴포넌트들에서 필수 사항입니다.

그러나 시그널링 메커니즘은 WebRTC 표준에서 정의하고 있지 않으므로 시그널링 보안을 구축하는 것은 여러분에게 달린 일입니다. 만약 공격자가 시그널링을 가로채려 하면 세션들을 중지하고 연결들을 리다이렉션하고 내용을 기록, 변경 및 추가합니다.

보안 시그널링에서 가장 중요한 점은 암호화되지 않은 메세지가 가로채지지 않도록 하는 HTTPS와 WSS(예: TLS) 보안 프로토콜들을 사용하는 것입니다. 또한 동일한 시그널링 서버를 사용하는 다른 호출자들로부터 액세스될 수 있게 시그널링 메세지들이 브로드캐스팅되지 않도록 주의해야 합니다.

TLS를 사용한 시그널링은 WebRTC 앱을 보호하기 위한 절대적인 필수 적용 사항입니다.

시그널링 후: NAT와 방화벽들에 대응하기 위한 ICE의 사용

메타데이터 시그널링을 위해 WebRTC 앱들은 중계서버를 사용하지만 실제 미디어와 데이터 스트리밍들은 피어(Peer)와 피어(Peer)에 일단 세션의 연결되고 RTCPeerConnection가 클라이언트에 직접 연결하기 위한 시도가 이루어집니다.

보다 쉬운 세상에서 모든 WebRTC의 종점은 다른 피어(Peer)들과의 교화을 위해 직접 통신이 가능한 유일한 주소를 가집니다.

단순한 피어(Peer) 대 피어(Peer) 연결
NAT와 방화벽이 없는 세상

실제로는 대부분의 디바이스들은 NAT와 안티바이러스 소프트웨어가 확실한 포트들과 프로토콜들을 막고 수많은 프록시와 방화벽이 협력하는 하나 이상의 레이어 뒤에 동작하고 있습니다. 사실 방화벽과 NAT는 홈 wifi 라우터와 같은 동일한 디바이스로 구축되어 있습니다.

NAT와 방화벽들 뒤에 있는 피어(Peer)들
실제 세상

WebRTC 어플리케이션들은 실제 세상의 네트워크 복잡성을 극복하기 위해 ICE 프레임워크를 사용할 수 있씁니다. 이를 위해 어플리케이션은 아래와 같이 반드시 ICE 서버 URL들을 RTCPeerConnection으로 전달하여야 합니다.

ICE는 피어(Peer)들과 연결하기 위한 최적의 경로를 찾으려 합니다. 동시에 가능한 모든 것을 시도하며 동작하는 가장 효율적인 선택을 골라냅니다. ICE는 첫번째로 다음과 같이 디바이스 운영체제로와 네트워크 카드로부터 획득한 호스트 주소를 사용하여 연결을 생성하려 합니다. 만약 실패한다면 (디바이스가 NAT 뒤에 있을 것입니다.) ICE는 STUN 서버를 이용하여 외부 주소를 획득하고 그것이 실패한다면 TURN 중계 서버를 통해 트래픽을 라우팅합니다.

다시 말해서

  • STUN 서버는 외부 네트워크 주소를 얻는데 사용됩니다.
  • TURN 서버들은 직접(P2P) 연결이 실패할 경우 트래픽을 중계하는데 사용됩니다.

모든 TURN 서버는 다음과 같이 STUN을 지원합니다. TURN 서버는 내장된 기능을 릴레이하는 STUN 서버입니다. 또한 ICE는 다음과 같이 복잡한 NAT 설정에 대응할 수 있습니다. 실제로 NAT 'Hole Punching'은 포트 주소 같은 공용 IP 이상의 것들을 요구합니다.

STUN, TURN 서버들을 위한 URL들은 RTCPeerConnection 생성자의 첫번째 인자로 전달되는 iceServers 설정 객체에 존재하는 WebRTC 앱에 의해 (선택적으로) 정의됩니다. apprtc.appspot.com에서 저 값은 다음과 같이 보입니다.

{
  'iceServers': [
    {
      'url': 'stun:stun.l.google.com:19302'
    },
    {
      'url': 'turn:192.158.29.39:3478?transport=udp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    },
    {
      'url': 'turn:192.158.29.39:3478?transport=tcp',
      'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
      'username': '28224511:1379330808'
    }
  ]
}

일단 RTCPeerConnection이 정보를 가지게 되면 ICE의 마법은 다음과 같이 자동으로 일어납니다. RTCPeerConnection는 필요에 따라 STUN과 TURN 서버들과 동작하는 피어(Peer)들 사이에서 최적의 경로을 산출하는 ICE 프레임워크를 사용합니다.

STUN

NAT들은 사설 로컬 네트워크에서 디바이스에 IP 주소를 제공하지만 이 주소는 외부에서 사용될 수 없습니다. 공용 주소가 없이는 WebRTC 피어(Peer)들은 통신할 수 있는 방법이 없습니다. 이 문제를 해결하기 위해서 WebRTC는 STUN을 사용합니다.

STUN 서버들은 공용 인터넷에서 동작하며 다음과 같은 단순한 한가지 작업을 수행합니다. (NAT 뒤에서 동작한느 어플리케이션으로부터) 전달된 요청의 IP:port 주소를 확인하고 그 주소를 응답으로 되돌려 보냅니다. 다시 말해서, 어플리케이션은 공용의 관점에서 그것의 IP:port를 발견하는 STUN 서버를 사용합니다. 이 절차는 WebRTC 피어(Peer)가 그 자신에 대해 공용에서 액세스 가능한 주소를 획득할 수 있도록 한 뒤 직접 연결을 설정하기 위한 시그널링 메커니즘을 통해 또다른 피어(Peer)로 전송합니다. (실제로는 각각의 NAT들은 개별적으로 동작하며 중첩된 NAT 레이어들이 있을 수 있으나 원칙은 여전히 동일합니다.)

STUN 서버들은 많은 것을 하거나 많은 것을 기억하지 말아야하므로 관련된 저사양 STUN 서버들은 아주 많은 양의 응답을 관리할 수 있습니다.

대부분의 WebRTC 호출은 STUN을 이용한 연결을 성공적으로 만들어냅니다. 방화벽과 복잡한 NAT 설정들 뒤에 존재하는 피어(Peer)들 사이의 호출은 더 작을 수 있겠지만 webrtcstats.com에 따른 (호출에 대한 연결 생성 성공률은) 86%입니다.

STUN 서버를 이용한 피어(Peer) 대 피어(Peer) 연결
IP:port 주소들을 얻기 위한 STUN 서버들의 사용

TURN

RTCPeerConnection는 UDP 상에서 피어(Peer)들 간의 직접 통신 설정을 시도합니다. 만약 이를 실패하면, RTCPeerConnection는 TCP에 의존합니다. 이것도 실패하면 종단점(Endpoint)들 사이의 데이터 릴레이를 수행하는 TURN 서버들이 대안으로 사용될 수 있습니다.

반복하지만 TURN은 시그널링 데이터가 아니라 피어(Peer)들 사이의 오디오/비디오/데이터 스트리밍 릴레이를 위해 사용됩니다!

TURN 서버들은 공용 주소들을 가지고 있으므로 설령 피어(Peer)들이 방화벽이나 프록시들 뒤에 존재하더라도 피어(Peer)들이 접속할 수 있습니다. TURN 서버들은 — 스트림을 릴레이하기 위한 — 개념적으로 단순한 태스크를 수행합니다. 그러나 STUN 서버들과는 다르게 본질적으로 많은 대역폭을 소모합니다. 뒤집어 말하면 TURN 서버들은 육중해질 수 있습니다.

STUN 서버를 이용한 피어(Peer) 대 피어(Peer) 연결
완전한 예: STUN, TURN 그리고 시그널링

이 다이어그램은 다음과 같이 동작하는 TURN을 보여줍니다. 순수 STUN이 성공하지 못했으므로 각 피어(Peer)는 TURN 서버의 사용에 의존합니다.

STUN과 TURN 서버들의 배포

시범적으로 구글은 apprtc.appspot.com에서 사용하는 공용 STUN 서버(stun.l.google.com:19302)를 운용하고 있습니다. STUN/TURN 서비스의 생성을 위해 'rfc5766 규격을 만족하는 TURN 서버(rfc5766-turn-server)'를 사용하는 것을 권장합니다. code.google.com/p/rfc5766-turn-server에서 STUN과 TURN 서버들의 소스 코드와 서버 설치에 대한 몇가지 소스 정보를 링크들을 확인할 수 있습니다. 또한 아마존 웹 서비스를 위한 VM 이미지도 사용할 수 있습니다.

대체가능한 TURN 서버는 restund가 있으며 소스 코드와 역시 AWS를 위한 리소스도 확인할 수 있습니다. 아래는 Google Compute Engine에서 어떻게 restund를 설치하는지에 대한 설명입니다.

  1. tcp=443과 udp/tcp=3478에 대해 필요하다면 방화벽을 열어둡니다.
  2. 4개의 인스턴스를 생성하고 각각에 대하여 공인 IP와 표준 우분투 12.06 이미지를 설정합니다.
  3. 로컬 방화벽을 설정합니다. (allow ANY from ANY)
  4. 설치 도구들
    sudo apt-get install make
    sudo apt-get install gcc
  5. restund의 다운로드 및 빌드
    libre: creytiv.com/re.html
    restund: creytiv.com/restund.html
  6. 빌드 에러(HAVE_INET6)를 발생하는 IPv6를 비활성화하고 restund/mk로 업데이트합니다.
  7. libre와 restund를 위해 sudo make install를 실행합니다.
  8. 다음과 같이 restund를 설정합니다.
    LD_LIBRARY_PATH를 설정합니다.
    restund.conf/etc/restund.conf에 복사합니다.
    올바른 10. IP 주소를 사용하도록 restund.conf를 설정합니다.
  9. restund를 실행합니다.
  10. 다음과 같이 원격에서 stund 클라이언트를 이용하여 테스트합니다. ./client IP:port

1대1일을 넘어서: 다자간(multi-party) WebRTC

TURN 서비스들을 액세스하기 위한 REST API(REST API for access to TURN Services)을 위해 Justin Uberti가 제안한 IETF 표준규격을 또한 살펴보기를 원할 수 있습니다.

다음과 같이 단순한 1대1 호출 뒤에 미디어 스트리밍의 동작에 대한 사용 사례들을 쉽게 상상해볼 수 있습니다. 동료 그룹 간의 비디오 컨퍼런스나 한명의 발표자와 몇백(혹은 몇십만의) 관중이 있는 일반 이벤트가 그 예입니다.

WebRTC 앱은 다중 RTCPeerConnections을 사용할 수 있으므로 모든 종단점(Endpoint)은 그물망처럼 각각 다른 종단점과 연결됩니다. 이는 talky.io같은 앱들이 취하는 접근 방식이며 피어(Peer)들의 작은 handful에도 매우 잘 동작합니다. 이를 넘어서 프로세싱과 대역폭의 소모는 excessive되어 가고 모바일 클라이언트들에는 특히 그렇습니다.

Mesh: small N-way call
완전한 그물망 형태의 위상 구조: 모든 것이 모두 연결되어 있음

그 대신에 star configuration에서 WebRTC 앱은 다른 모두에게 스트림들을 분배하기 위한 하나의 종단점(Endpoint)를 선택할 수 있으며 또한 서버에서 WebRTC 종단점(Endpoint)를 실행할 수 있으며 여러분 각자의 재분배 메커니즘을 구성할 수도 있습니다. (webrtc.org에서 제공하는 샘플 클라이언트 어플리케이션)

Multipoint Control Unit

대량의 종단점(Endpoint)들을 위한 더 나은 옵션은 Multipoint Control Unit (MCU)를 사용하는 것입니다. 이것은 많은 수의 참여자 간에 미디어를 배포하기 위한 가교(Bridge)처럼 동작하는 서버입니다. MCU들은 비디오 컨퍼런스에서 각기 다른 해상도, 코덱, 프레임율, 변환부호화(Transcoding)의 조작, 선택적 스트림 포워딩의 실행 그리고 오디오와 비디오의 혼합(mix)및 녹화/녹음에 대응할 수 있습니다. 다자간 호출에 대한 고려해야할 몇가지 문제점은 다음과 같습니다. 특히 다중 소스로부터 입력된 비디오들을 다중 출력하거나 오디오 혼합(Mix)를 어떻게 할 수 있을까요. vLine과 같은 클라우드 플랫폼들 역시 트래픽 라우팅을 최적화하려 하고 있습니다.

Cisco MCU5300의 실제 모습
Cisco MCU의 후면

(이전에는 Lynckia로 알려졌던) Licode는 WebRTC를 위한 오픈소스 MCU를 생산하고 있습니다. OpenTok은 Mantis를 가지고 있습니다.

크롬 31 버전과 오페라 18 버전부터 하나의 RTCPeerConnection에서 전달된 MediaStream은 또 다른 연결에 대한 입력으로 사용될 수 있습니다. simpl.info/multi를 확인해보시기 바랍니다. 이는 웹앱이 연결하고자 하는 클라이언트(들)을 선택할 수 있기 때문에 흥미로운 구조들을 가능하게 합니다.

브라우저를 넘어: VoIP, 전화 그리고 메세징

WebRTC의 표준적인 생태계는 브라우저에 동작하는 WebRTC 앱과 전화나 비디오 컨퍼런스 시스템 같은 다른 통신 플랫폼 상에서 동작하는 디바이스나 플랫폼 사이의 통신을 설정하는 것을 가능하게 만듭니다.

SIP는 VoIP나 비디오 컨퍼런스 시스템들에서 사용되는 시그널링 프로토콜입니다. WebRTC 웹앱과 비디오 컨퍼런스 시스템 같은 SIP 클라이언트 사이의 통신이 가능하기 위해서는 WebRTC는 시그널링을 중재할 프록시 서버가 필요합니다. 시그널링은 반드시 게이트웨이를 통해서 전달되어야 하지만 일단 통신이 설정되고 나면, SRTP 트래픽(비디오와 오디오)는 직접 피어(Peer) 대 피어(Peer)로 전달될 수 있습니다.

PSTN(Public Switched Telephone Network, 공중교환 전화망)은 '정말 오래된' 아날로그 전화 모두의 서킷 교환 네트워크입니다.WebRTC 웹앱과 전화기들 사이의 호출을 위해 트래픽은 반드시 PSTN 게이트웨이를 통해 전달되어야 합니다. 비슷한 것으로 WebRTC 웹앱은 IM 클라이언트와 같은 Jingle 종단점(Endpoint)과 통신하기 위한 XMPP 중계 서버가 필요합니다. Jingle은 과거 구글에 의해 XMPP가 메세징 서비스를 위해 보이스와 비디오를 가능하게 하는 확장(Extension)으로 개발되었습니다. 현재 WebRTC 구현들은 C++로 작성된 libjingle 라이브러리를 기반으로 하고 있으며 Jingle의 (실제) 구현은 구글톡(Google Talk)에서 처음 개발되었습니다.

다음과 같은 많은 앱과 라이브러리 그리고 플랫폼들이 바깥 세상과 통신하기 위해 WebRTC의 기능을 활용하였습니다.

  • sipML5: 오픈소스 자바스크립트 SIP 클라이언트
  • jsSIP: 자바스크립트 SIP 라이브러리
  • Phono: 플러그인 형태로 구축된 오픈소스 자바스크립트 폰(Phone) API
  • Zingaya: 임베디드가 가능한 폰(Phone) 위젯
  • Twilio: 보이스 및 메세징
  • Uberconference: 컨퍼런싱(역주: 디바이스 기반의 원거리 회의)

sipML5 개발자들은 webrtc2sip 역시도 구축했습니다. Tethr와 Tropo는 재난 통신을 위한 프레임워크를 피처폰(기존의 일반 휴대폰)과 컴퓨터들 간의 통신을 WebRTC를 통해 가능하게 하는 OpenBTS cell을 이용하여 '대략적으로' 시연하였습니다. 통신사가 없는 휴대폰 통신으로!

더 많은 방법을 찾아봅시다

WebRTC 코드랩: Node 상에서 동작하는 Socket.io 시그널링 서비스를 이용한 비디오 및 텍스트 채팅 어플리케이션을 어떻게 구축하는지에 대한 단계별 설명.

WebRTC 기술 리더, Justin Uberti의 2013 Google I/O WebRTC 프리젠테이션.

WebRTC Book은 대량의 자세한 네트워크 위상 다이어그램을 포함한 데이터와 시그널링 경로에 대한 아주 자세한 내용을 담고 있습니다.

WebRTC와 시그널링: 2년이라는 시간이 우리는 무엇을 배웠나: 왜 시그널링이 규격에서 빠진 것은 좋은 아이디어였는지에 대한 TokBox 블로그 포스팅.

Ilya Grigorik의 High Performance Browser Networking(O'Reilly 4판)의 WebRTC 챕터.

Comments

0