A simple RTCDataChannel sample

Ray
16 min readJun 13, 2018

원문 (일부 오역이 있을수 있습니다)

RTCDataChannel 인터페이스는 임의의 데이터를 교환할 수 있는 채널을 만들수 있는 WebRTC API의 기능입니다. 해당 API는 WebSocket API와 의도적으로 유사하게 제작되었습니다. 동일한 프로그래밍 모델을 각각 사용할 수 있습니다.

아래 샘플에서는 같은 페이지에서 두개의 엘리먼트를 연결하는 RCTDataChannel 커넥션을 오픈합니다. 확실히 부자연스러운 부분은 있으나 두 피어를 연결하는 흐름을 보여주는데 유용합니다. 연결, 데이터를 전송 수신하는 메커니즘을 다룰것 입니다. 다른 예제에서는 원격 컴퓨터를 찾고 연결하는 방법에 대한 정보를 알아볼 것입니다.

The HTML

첫번째로 필요한 HTML을 간단히 살펴보겠습니다. 복잡하지 않습니다. 연결을 설정하고 닫는 버튼이 있습니다.

<button id="connectButton" name="connectButton" class="buttonleft">
Connect
</button>
<button id="disconnectButton" name="disconnectButton" class="buttonright" disabled>
Disconnect
</button>

이제 사용자가 입력할 텍스트를 입력할 수 있는 HTML이 있습니다(아래 코드 참조). 해당 <div>(메시지 박스)는 채널의 첫번째 피어가 됩니다.

<div class="messagebox">
<label for="message">Enter a message:
<input type="text" name="message" id="message" placeholder="Message text"
inputmode="latin" size=60 maxlength=120 disabled>
</label>
<button id="sendButton" name="sendButton" class="buttonright" disabled>
Send
</button>
</div>

이제 메시지를 입력할 HTML이 있습니다. 이 <div>는 두번째 피어가 됩니다.

<div class="messagebox" id="receivebox">
<p>Messages received:</p>
</div>

The JavaScript code

깃헙에서 코드 자체를 볼수 있지만 아래에서는 코드를 동작하게끔 하는 부분을 살펴 볼 것입니다.

WebRTC API는 Promise를 많이 사용합니다. 그것은 연결과정의 단계들을 함께 묶는 것을 매우 쉽게 만듭니다. ECMAScript 2015의 이 기능에 대한 이해가 필요합니다. 마찬가지로 이 예제에서는 화살표 함수를 사용하여 구문을 단수화합니다.

Starting up

스크립트가 동작할때, 우리는 load 이벤트 리스너를 셋업합니다. 그리고 페이지 전체가 한번 로드되면 아래 startUp함수가 호출됩니다.

function startup() {
connectButton = document.getElementById('connectButton');
disconnectButton = document.getElementById('disconnectButton');
sendButton = document.getElementById('sendButton');
messageInputBox = document.getElementById('message');
receiveBox = document.getElementById('receivebox');

// Set event listeners for user interface widgets

connectButton.addEventListener('click', connectPeers, false);
disconnectButton.addEventListener('click', disconnectPeers, false);
sendButton.addEventListener('click', sendMessage, false);
}

이것은 아주 간단합니다. 엑세스 해야할 페이지 요소에 대한 참조를 가져온 다음 세개의 버튼에 이번트 리스너를 설정합니다.

Establishing a connection

사용자가 “Connect” 버튼을 클릭하면 connectPeers() 메소드가 호출됩니다. 우리는 이것을 좀더 확실하게 살펴볼 것입니다.

참고 : 연결의 엔드포인트는 같은 페이지에 있지만 연결을 시작하는 쪽을 “로컬”, 다른 쪽 끝은 “원격”으로 참조합니다.

Set up the local peer

localConnection = new RTCPeerConnection();

sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;

첫번째 단계는 연결의 “로컬” 엔드포인트를 만드는 것입니다. 이것은 연결 요청을 보내는 피어입니다. 다음 스텝은 RTCPeerConnetion.createDataChannel을 호출해서 RTCDataChannel을 만들고 채널을 모니터링 하도록 이벤트 리스너를 설정하여, 채널이 열리고 닫힐때를 알 수 있도록 합니다.(해당 피어 연결에서 채널이 연결되거나 연결이 끊어지는 경우)

채널의 각 엔드포인트 마다 고유한 RTCDataChannel 객체가 있음을 명심해야합니다.

Set up the remote peer

remoteConnection = new RTCPeerConnection();
remoteConnection.ondatachannel = receiveChannelCallback;

유사하게 리모트 엔드포인트를 셋업합니다. 명시적으로 RTCDataChannel를 만들필요 없습니다. 위에서 설정한 채널을 통해 연결되기 때문입니다. 대신에 datachannel 이벤트 핸들러를 셋업합니다. 이것은 데이터 채널이 오픈될때 호출됩니다. 이 핸들러는 RTCDataChannel 오브젝트를 받습니다. 아래에서 볼수 있습니다.

Set up the ICE candidates

다음 단계는 ICE 후보 리스너와의 연결을 설정하는 것입니다. 이들은 상대방과 통신할 새로운 ICE 후보가 있을때 호출됩니다.

참고 : 실제 환경에서 두 피어가 동일한 컨텍스트에서 실행되지 않는 경우 프로세스가 좀 더 복잡해집니다. 각 사이드에서 RTCPeerConnection.addIceCandidate()를 호출하여 한번에 하나씩 제안된 연결 방법(예 : UDP, 릴레이, TCP등)을 제공하며 합의에 도달 할 때까지 앞뒤로 이동합니다. 그러나 이 예제에서는 실제 네트워킹이 필요 없기 때문에 각 사이드에서 첫번째 제안을 수락합니다.

localConnection.onicecandidate = e => !e.candidate
|| remoteConnection.addIceCandidate(e.candidate)
.catch(handleAddCandidateError);

remoteConnection.onicecandidate = e => !e.candidate
|| localConnection.addIceCandidate(e.candidate)
.catch(handleAddCandidateError);

icecandidate 이벤트 에 대한 이벤트 핸드러를 갖도록 각 RTCPeerConnection을 구성합니다.

Start the connection attempt

우리 피어와 연결을 시작하기 위해 해야할 마지막 일은 연결 제안을 만드는 것입니다.

localConnection.createOffer()
.then(offer => localConnection.setLocalDescription(offer))
.then(() => remoteConnection.setRemoteDescription(localConnection.localDescription))
.then(() => remoteConnection.createAnswer())
.then(answer => remoteConnection.setLocalDescription(answer))
.then(() => localConnection.setRemoteDescription(remoteConnection.localDescription))
.catch(handleCreateDescriptionError);

무슨 일을 하는지 한줄씩 보도록 합시다.

  1. 먼저 RTCPeerConnection.createOffer 메소드를 호출하여 우리가 만들고자 하는 연결을 설명하는 SDP blob를 만듭니다. 이 메소드는 연결이 오디오, 비디오 또는 둘 다를 지원해야하는지 여부와 같이 연결에 대해 충족해야하는 제약 조건이 있는 개체를 선택적으로 허용합니다. 우리는 이 예에서 어떠한 제약도 없습니다.
  2. 위의 오퍼가 성공적으로 생성되면, blob을 로컬 연결의 RTCPeerConnection.setLocalDescription() 메소드로 전달합니다. 이렇게하면 연결의 로컬 엔드포인트가 구성됩니다.
  3. 다음 단계는 리모트 피어에 대해 알려줌으로써 로컬 피어를 리모트에 연결 하는 것입니다. 이 작업은 remoteConnection.RTCPeerConnection.setRemoteDescription()을 호출하여 수행됩니다. 이제 remoteConnection은 빌드 중인 연결을 알고 있습니다.
  4. 이제 리모트피어가 응답할때가 되었습니다. createAnswer() 메소드를 호출하면됩니다. 이것은 리모트피어가 설정할수있는 연결을 설명하는 SDP의 BLOB를 생성합니다. 이 구성은 두 피어가 지원할 수 있는 옵션에 조합의 어딘가에 있습니다.
  5. 응답이 생성되면 RTCPeerConnection.setLocalDescript()을 호출하여 remoteConnection에 전달됩니다. 리모트 커넥션 엔드포인트가 설정됩니다. (리모트 피어의 로컬엔드포인트, 혼란스러울 수 있지만 익숙해지셔야 합니다)
  6. 마지막으로 로컬 연결의 리모트 설명은 localConnection의 RTCPeerConnection.setRemoteDescription()을 호출하여 리모트 피어를 참조하도록 설정됩니다.
  7. catch()는 발생하는 모든 오류를 처리하는 루틴을 호출합니다.

참고: 이 과정은 실제 구현이 아닙니다. 정상적인 사용법에서는 두 컴퓨터에서 실행되는 두 개의 조각(청크)가 있어 상호 작용하고 연결을 협상합니다.

Handling successful peer connection

각 사이드의 P2P 커넥션이 성공적으로 연결되면, 그에 대한 응답으로 RTCPeerConnection의 icecandidate 이벤트가 호출됩니다. 이 핸들러는 필요한 모든 작업을 수행 할 수 있지만, 이 예에서는 사용자 인터페이스를 업데이트 하는데 사용됩니다.

function handleLocalAddCandidateSuccess() {
connectButton.disabled = true;
}

function handleRemoteAddCandidateSuccess() {
disconnectButton.disabled = false;
}

위의 코드에서 하는 작업은 로컬 피어가 연결되었을때 “Connect”버튼을 비활성화하고 리모트 피어가 연결될때 “Disconnect”버튼을 활성화 하는 것입니다.

Connecting the data channel

RTCPeerConnection이 열리면 데이터 채널 열기 프로세스를 완료하기 위해 dataChannel 이벤트가 원격지로 전송됩니다. 그러면 다음과 같은 receiveChannelCallback()메소드가 호출됩니다.

function receiveChannelCallback(event) {
receiveChannel = event.channel;
receiveChannel.onmessage = handleReceiveMessage;
receiveChannel.onopen = handleReceiveChannelStatusChange;
receiveChannel.onclose = handleReceiveChannelStatusChange;
}

dataChannel 이벤트는 채널 속성에 리모트 피어의 채널 엔드포인트를 나타내는 RTCDataChannel에 대한 참조를 포함합니다. 이것은 저장되며, 채널에서 처리하려는 이벤트의 이벤트 리스너를 설정합니다. 이 작업이 완료되면 우리의 handleReceiveMessage() 메소드가 리모트 피어에 의에 데이터가 수신될때마다 호출되고 handleReceiveChannelStatusChange() 메소드는 채널의 연결 상태가 변경 될 때마다 호출되어 채널이 완전히 열리고, 닫힐때 대응할 수 있습니다.

Handling channel status changes

로컬 및 리모트 피어는 채널 연결 상태의 변경을 나타내는 이벤트를 처리하는 단일 메소드를 사용합니다.

로컬 피어에서 open, close 이벤트가 호출되면 handleSendChannelStatusChange() 메소드가 호출됩니다.

function handleSendChannelStatusChange(event) {
if (sendChannel) {
var state = sendChannel.readyState;

if (state === "open") {
messageInputBox.disabled = false;
messageInputBox.focus();
sendButton.disabled = false;
disconnectButton.disabled = false;
connectButton.disabled = true;
} else {
messageInputBox.disabled = true;
sendButton.disabled = true;
connectButton.disabled = false;
disconnectButton.disabled = true;
}
}
}

채널의 상태가 “Open”으로 변경되면 두 피어간 연결설정이 완료되었음을 나타냅니다. 연결이 열려있을때 필요하지 않으므로 “연결”버튼은 비활성화 하시고, 사용자 인터페이스는 연결이 열려 있을 경우 텍스트 입력상자가 활성화되고 “보내기” 및 “연결 끊기” 버튼을 사용할 수 있도록 업데이트 됩니다.

만약 상태가 “closed”상태가 된다면 반대의 상대로 설정됩니다. “보내기” 버튼,텍스트 박스가 비활성화되고, “연결”버튼이 활성화됩니다.

현재 예제의 리모트 피어는 콘솔에 이벤트를 로깅하는 것을 제외하고 상태 변경 이벤트를 무시합니다.

function handleReceiveChannelStatusChange(event) {
if (receiveChannel) {
console.log("Receive channel's status has changed to " +
receiveChannel.readyState);
}
}

handleReceiveChannelStatusChange () 메소드는 발생한 이벤트를 입력 매개 변수로받습니다. RTCDataChannelEvent가됩니다.

Sending messages

사용자가 “보내기”버튼을 누르면 버튼의 click 이벤트 핸들러로 설정한 sendMessage가 호출됩니다. 간단합니다.

function sendMessage() {
var message = messageInputBox.value;
sendChannel.send(message);

messageInputBox.value = "";
messageInputBox.focus();
}

먼저 텍스트 박스의 값을 가져옵니다. 그런다음 sendChannel.send()를 호출하여 리모트 피어로 전송합니다. 그게 전부입니다. 전송후에 텍스트 박스의 값을 초기화 하고, 포커스 맞춥니다.

Receiving messages

“message” 이벤트가 리모트 채널에서 발생될때, handleReceiveMessage() 메소드가 이벤트 핸들러로 호출됩니다.

function handleReceiveMessage(event) {
var el = document.createElement("p");
var txtNode = document.createTextNode(event.data);

el.appendChild(txtNode);
receiveBox.appendChild(el);
}

이 메소드는 단순한 DOM 인젝션을 수행합니다. p 오브젝트를 생성하고 해당 오브젝트에 text를 추가합니다. 해당 텍스트는 event의 data 프로퍼티입니다. 해당 텍스트는 새로운 엘리먼트의 자식으로 추가됩니다. 해당 텍스트가 브라우저에 표시됩니다.

Disconnecting the peers

사용자가 “disconnect” 버튼을 클릭하면 , 버튼의 이벤트핸들러로 disconnectPeers() 메소드가 호출됩니다.

function disconnectPeers() {

// Close the RTCDataChannels if they're open.

sendChannel.close();
receiveChannel.close();

// Close the RTCPeerConnections

localConnection.close();
remoteConnection.close();

sendChannel = null;
receiveChannel = null;
localConnection = null;
remoteConnection = null;

// Update user interface elements

connectButton.disabled = false;
disconnectButton.disabled = true;
sendButton.disabled = true;

messageInputBox.value = "";
messageInputBox.disabled = true;
}

각 피어의 RTCDataChannel을 닫은 다음 비슷하게 각 RTCPeerConnection을 닫음으로 시작합니다. 오브젝트에 대한 저장된 참조는 모두 우발적인 재사용을 방지하기 위해 null로 설정되며 사용자 인터페이스는 연결이 닫혔다는 것을 반영하도록 업데이트 됩니다.

--

--