본문 바로가기
프로그래밍/WebRTC

[WebRTC] Real time communication with WebRTC 3

by YuminK 2022. 6. 10.

https://codelabs.developers.google.com/codelabs/webrtc-web/#5

6. Use RTCDataChannel to exchange data

대부분의 내용이 이전 섹션과 동일합니다. 결국에는 RTCPeerConnection을 만드는 과정에서 sendChannel과 receiveChannel에 대한 생성과 메시지를 받아서 뷰에 출력해주는 부분, 버튼 막는 처리 등의 전부입니다.

 

start 버튼을 누르면 connection생성을 시작합니다.

로컬 커넥션에서 sendDataChannel을 만드는 부분이 추가가 되어 있습니다. 

 

각 localConnection과 채널에 callback 처리를 해주고 있는데 채널에는 각 뷰에 대한 활성화처리, 로깅이 되어 있고

onicecandidate에서 각 peer가 연결 처리를 할 때 iceCandidate에 대한 콜백 처리를 각각 연결해주고 있습니다.

(서로가 서로에게 candidate 정보를 전달하는 내용)

function iceCallback1(event) {
  trace('local ice callback');
  if (event.candidate) {
    remoteConnection.addIceCandidate(
      event.candidate
    ).then(
      onAddIceCandidateSuccess,
      onAddIceCandidateError
    );
    trace('Local ICE candidate: \n' + event.candidate.candidate);
  }
}

function iceCallback2(event) {
  trace('remote ice callback');
  if (event.candidate) {
    localConnection.addIceCandidate(
      event.candidate
    ).then(
      onAddIceCandidateSuccess,
      onAddIceCandidateError
    );
    trace('Remote ICE candidate: \n ' + event.candidate.candidate);
  }
}

 

remoteConnection에서 receiveChannel을 처리하는 부분도 추가가 되었는데요.

function receiveChannelCallback(event) {
  trace('Receive Channel Callback');
  receiveChannel = event.channel;
  receiveChannel.onmessage = onReceiveMessageCallback;
  receiveChannel.onopen = onReceiveChannelStateChange;
  receiveChannel.onclose = onReceiveChannelStateChange;
}

function onReceiveMessageCallback(event) {
  trace('Received Message');
  dataChannelReceive.value = event.data;
}

function onSendChannelStateChange() {
  var readyState = sendChannel.readyState;
  trace('Send channel state is: ' + readyState);
  if (readyState === 'open') {
    dataChannelSend.disabled = false;
    dataChannelSend.focus();
    sendButton.disabled = false;
    closeButton.disabled = false;
  } else {
    dataChannelSend.disabled = true;
    sendButton.disabled = true;
    closeButton.disabled = true;
  }
}

function onReceiveChannelStateChange() {
  var readyState = receiveChannel.readyState;
  trace('Receive channel state is: ' + readyState);
}

event가 오는 시점에 채널을 연결하고 callback 처리를 해주고 있습니다. 메시지가 오면 view에 출력하는 부분과 open, close 시점에서 로깅 하는 부분이 전부입니다. 

 

나머지는 localConnection이 createOffer를 생성하여 자신의 localDescription정보를 설정하고 remoteConnection의 remoteDescription정보에 설정. 이후 createAnswer를 호출하여 localConnection과 연결되는 세션을 생성하며 remoteConnection의 로컬 description 설정, localConnection의 remoteConnection 정보를 설정합니다.(remote's description)

 

close 버튼을 누르면, channel, connection 닫고 view에 대한 처리를 합니다.

function closeDataChannels() {
  trace('Closing data channels');
  sendChannel.close();
  trace('Closed data channel with label: ' + sendChannel.label);
  receiveChannel.close();
  trace('Closed data channel with label: ' + receiveChannel.label);
  localConnection.close();
  remoteConnection.close();
  localConnection = null;
  remoteConnection = null;
  trace('Closed peer connections');
  startButton.disabled = false;
  sendButton.disabled = true;
  closeButton.disabled = true;
  dataChannelSend.value = '';
  dataChannelReceive.value = '';
  dataChannelSend.disabled = true;
  disableSendButton();
  enableStartButton();
}

>> 로그 내역

main.js:185 2814.035: local ice callback
main.js:185 2814.035: Local ICE candidate: 
candidate:4033732497 1 udp 2122260223 192.168.0.104 58102 typ host generation 0 ufrag 5nuh network-id 1 network-cost 10
main.js:185 2814.036: remote ice callback
main.js:185 2814.036: Remote ICE candidate: 
 candidate:4033732497 1 udp 2122260223 192.168.0.104 58103 typ host generation 0 ufrag BjUh network-id 1 network-cost 10
main.js:185 2814.036: AddIceCandidate success.
main.js:185 2814.037: AddIceCandidate success.
main.js:185 2814.038: remote ice callback
main.js:185 2814.038: local ice callback
main.js:185 2814.044: Send channel state is: open
main.js:185 2814.045: Receive Channel Callback
main.js:185 2814.046: Receive channel state is: open
main.js:185 3217.416: Sent Data: tetetstsdsadasdsa
main.js:185 3217.417: Received Message
main.js:185 3333.796: Closing data channels
main.js:185 3333.797: Closed data channel with label: sendDataChannel
main.js:185 3333.797: Closed data channel with label: sendDataChannel
main.js:185 3333.799: Closed peer connections
main.js:185 3333.800: Send channel state is: closed
main.js:185 3333.800: Receive channel state is: closed

 

Ice에 대한 연결 처리 로직 및 각 채널에 대한 callback에서 호출되는 로그가 출력됩니다. 메시지를 뷰에 넣어서 send함수를 호출하면 sendChannel을 통해 메시지를 전달하고 receiveChannel에서는 해당 데이터를 받아서 뷰에 출력합니다. 

 

닫기 버튼을 누른 경우에는 channel, connection을 닫아주는 처리를 하고 있습니다. 

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

function onReceiveMessageCallback(event) {
  trace('Received Message');
  dataChannelReceive.value = event.data;
}

 

전체 코드

'use strict';

var localConnection;
var remoteConnection;
var sendChannel;
var receiveChannel;
var pcConstraint;
var dataConstraint;
var dataChannelSend = document.querySelector('textarea#dataChannelSend');
var dataChannelReceive = document.querySelector('textarea#dataChannelReceive');
var startButton = document.querySelector('button#startButton');
var sendButton = document.querySelector('button#sendButton');
var closeButton = document.querySelector('button#closeButton');

startButton.onclick = createConnection;
sendButton.onclick = sendData;
closeButton.onclick = closeDataChannels;

function enableStartButton() {
  startButton.disabled = false;
}

function disableSendButton() {
  sendButton.disabled = true;
}

function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function onCreateSessionDescriptionError(error) {
  trace('Failed to create session description: ' + error.toString());
}

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

function closeDataChannels() {
  trace('Closing data channels');
  sendChannel.close();
  trace('Closed data channel with label: ' + sendChannel.label);
  receiveChannel.close();
  trace('Closed data channel with label: ' + receiveChannel.label);
  localConnection.close();
  remoteConnection.close();
  localConnection = null;
  remoteConnection = null;
  trace('Closed peer connections');
  startButton.disabled = false;
  sendButton.disabled = true;
  closeButton.disabled = true;
  dataChannelSend.value = '';
  dataChannelReceive.value = '';
  dataChannelSend.disabled = true;
  disableSendButton();
  enableStartButton();
}

function gotDescription1(desc) {
  localConnection.setLocalDescription(desc);
  trace('Offer from localConnection \n' + desc.sdp);
  remoteConnection.setRemoteDescription(desc);
  remoteConnection.createAnswer().then(
    gotDescription2,
    onCreateSessionDescriptionError
  );
}

function gotDescription2(desc) {
  remoteConnection.setLocalDescription(desc);
  trace('Answer from remoteConnection \n' + desc.sdp);
  localConnection.setRemoteDescription(desc);
}

function iceCallback1(event) {
  trace('local ice callback');
  if (event.candidate) {
    remoteConnection.addIceCandidate(
      event.candidate
    ).then(
      onAddIceCandidateSuccess,
      onAddIceCandidateError
    );
    trace('Local ICE candidate: \n' + event.candidate.candidate);
  }
}

function iceCallback2(event) {
  trace('remote ice callback');
  if (event.candidate) {
    localConnection.addIceCandidate(
      event.candidate
    ).then(
      onAddIceCandidateSuccess,
      onAddIceCandidateError
    );
    trace('Remote ICE candidate: \n ' + event.candidate.candidate);
  }
}

function onAddIceCandidateSuccess() {
  trace('AddIceCandidate success.');
}

function onAddIceCandidateError(error) {
  trace('Failed to add Ice Candidate: ' + error.toString());
}

function receiveChannelCallback(event) {
  trace('Receive Channel Callback');
  receiveChannel = event.channel;
  receiveChannel.onmessage = onReceiveMessageCallback;
  receiveChannel.onopen = onReceiveChannelStateChange;
  receiveChannel.onclose = onReceiveChannelStateChange;
}

function onReceiveMessageCallback(event) {
  trace('Received Message');
  dataChannelReceive.value = event.data;
}

function onSendChannelStateChange() {
  var readyState = sendChannel.readyState;
  trace('Send channel state is: ' + readyState);
  if (readyState === 'open') {
    dataChannelSend.disabled = false;
    dataChannelSend.focus();
    sendButton.disabled = false;
    closeButton.disabled = false;
  } else {
    dataChannelSend.disabled = true;
    sendButton.disabled = true;
    closeButton.disabled = true;
  }
}

function onReceiveChannelStateChange() {
  var readyState = receiveChannel.readyState;
  trace('Receive channel state is: ' + readyState);
}

function trace(text) {
  if (text[text.length - 1] === '\n') {
    text = text.substring(0, text.length - 1);
  }
  if (window.performance) {
    var now = (window.performance.now() / 1000).toFixed(3);
    console.log(now + ': ' + text);
  } else {
    console.log(text);
  }
}

 

 

How it works

RTCPeerConnection과 RTCDataChannel을 사용하여 메시지를 전달하고 있습니다.

RTCDataChannel은 WebSocket과 유사합니다. dataConstraint의 사용을 통해 데이터 채널은 구성될 수 있으며 다양한 공유 속성에 대한 처리를 할 수 있습니다. (예를 들면 성능 대비 신뢰성이 높은 정보 제공) 

 

Bonus points

  1. With SCTP, the protocol used by WebRTC data channels, reliable and ordered data delivery is on by default. When might RTCDataChannel need to provide reliable delivery of data, and when might performance be more important — even if that means losing some data?
  2. Use CSS to improve page layout, and add a placeholder attribute to the "dataChannelReceive" textarea.
  3. Test the page on a mobile device.

WebRTC DataChannel에서는 SCTP 프로토콜을 사용하여 통신을 진행합니다. 신뢰 가능한 순서 보장이 기본적인 설정입니다. 신뢰성 있는 데이터 순서를 제공해야 하는 순간, 퍼포먼스 적으로 더 중요한 순간은 언제일까요? 심지어 일부 데이터가 손실되는 경우라도?

 

In this step you learned how to:

  • Establish a connection between two WebRTC peers.
  • Exchange text data between the peers.

A complete version of this step is in the step-03 folder.

Find out more

https://www.html5rocks.com/en/tutorials/webrtc/datachannels/

우리는 WebSocket, Ajax, Server Sent Event 등이 있습니다. 왜 또다른 통신 채널이 필요합니까? WebSocket은 양방향 통신이지만 이러한 기술은 서버로 부터 통신을 하기 위해 설계되었습니다.

 

RTCDataChannel은 SCTP 프로토콜(Stream Control Transmission Protocol)을 사용합니다. 구성할 수 있는 전송 시맨틱을 허용합니다. (잘못된 전달과 재전송에 대한 구성을 포함하여)

 

WebRTC는 P2P 통신을 가능하게 합니다. 하지만 여전히 미디어나 네트워크 메타데이터를를 교환하기 위한 시그널링 서버가 필요합니다. webRTC는 다음을 통해 NAT과 방화벽을 제어합니다.

 

ICE Framework: 피어 사이에서 가능한 최적의 네트워크 경로를 설계하기 위함.

STUN: 각 피어 사이에서 접근 가능한 IP주소와 포트를 알아내기 위함

TURN: 연결이 실패했을 경우 데이터 relaying

For more information about how WebRTC works with servers for signaling and networking, see WebRTC in the real world: STUN, TURN, and signaling.

The capabilities

RTCDataChannel은 다양한 타입을 지원합니다. WebSocket을 모방하여 설계가 되었습니다. javascript에서 바이너리 타입인 Blob, ArrayBuffer, ArrayBufferView 등을 지원합니다. 이 때문에 멀티플레이 게임이나 파일 전송에 효과적일 수 있습니다. 

 

RTCDataChannel은 신뢰성이 보장되지 않는 모드로 가동될 수 있고(UDP 처럼) 신뢰성 있는 순서를 보장할 수 있습니다. (TCP) 또한 일부분만 신뢰성을 보장하는 경우도 가능합니다. 

 

socket losses가 없는 경우에 앞에 두 모드는 거의 동일합니다. 하지만 신뢰할 수 있는 순서를 사용하는 경우 패킷 손실이 일어났을 경우 패킷들이 막히게 되고, 과거의 정보로 변하며 재선송하고 도착하는 과정을 거치게 됩니다. 

https://bloggeek.me/sctp-data-channel/

SCTP는 TCP나 UDP 어디에나 초접을 맞출 수 있으며 두 경우보다 더 나은 향상을 보여줍니다. 

DataChannel은 어떤 타입을 필요로 하는지 알 수 없는 다양한 사례를 위한 것입니다. 

 

Reliability: 이 정보가 다른 쪽에서 온 정보인지 신뢰할 수 있는가?

Delivery: 2개의 패킷을 받은 경우, 제대로 된 순서로 받은 것인가?

Transmission: 패킷을 보냈는가? 아니면 bytes stream데이터를 보냈는가?

Flow Control, congestion control: 혼잡한 네트워크를 스스로 관리할 수 있는 프로토콜이 존재하는가?

 

SCTP에서 Reliability와 Delivery는 Configurable한 성질을 가집니다. 이러한 특성이 SCTP를 고른 이유이며 프로그래머가 자유롭게 다룰 수 있도록 했습니다. 

 

Similarly, here’s a great post Justin Uberti linked to a few weeks back – it is on reasons some game developers place their signaling over UDP and not TCP. Read it to get some understanding on why SCTP makes sense for WebRTC.

 

https://bloggeek.me/webrtc-data-channel-uses/

WebRTC 데이터 채널의 활용

댓글