WebRTCって何?
「Web Real-Time Communication」
Webブラウザ同士でリアルタイムにデータのやり取りをする仕組みのこと。
大容量の映像や音声データをリアルタイムに送受信できる。
WebRTC は P2P を利用したブラウザ同士を前提とした技術として作られている。
現在は、デスクトップとモバイルの「Google Chrome」「Safari」「Firefox」「Opera」等ほぼすべてのブラウザで利用できるようになっており、余計なプラグインのインストールは不要。ブラウザさえあれば使用可能。
WebRTCは通信をP2Pで行うため、サーバーを介さずリアルタイムのやり取りが可能。P2Pが使われるサービスは、たとえば「LINE」「Spotify」「BitCoin」「Ethereum」など。
※上記のサービス例は、WebRTCは使用されていないが、通信はP2P
WebRTCでできることの例
・リアルタイムなコミニュケーションの為のデバイス(マイク・カメラ)管理・スクリーンキャプチャ
・デバイス認識・選択
・デバイスから取得した大量の映像や音声のデータの圧縮と送信(コーデック)
・P2Pで相手と通信
など・・・
WebRTC は沢山の技術の集合体と考えてもらって問題ない。 それらをブラウザから簡単に使えるようにした仕組みが WebRTC API である。
WebRTC にはメディアチャネルとデータチャネルの二つがある。
- 映像と音声のデータ通信方式がメディアチャネル
- 好きなデータをやりとりできるのがデータチャネル
シグナリングサーバーとは
「通信相手に関する情報を得る」のが役割のサーバー
STUNサーバーとは
外部ネットワークから見た際の自身のIPアドレスを教えてくれます。そのアドレスと自身のPCのアドレスを比較してNAT越えが必要かを判断します。
NATとは
ネットワークアドレスを変換する機能のこと。
例えば、自宅でWi-Fiをつないだ際、端末にグローバルIPアドレスが付与されていないケースがあります。これは、プロバイダーから割り振られるIPアドレスが一つのため、複数の端末がインターネットに接続できるよう、ルーターがIPをLANの端末へ振り分けているためです。
そのため、端末で表示されるIPアドレスがグローバルIPアドレスと異なることがあります。しかし、これでは情報を送りたい側は困ってしまいます。なぜなら、通信したい先の本当のIPアドレスが変換されているため、わからないからです。
そこで、NATによって変換されたアドレスをセキュアに関連づける「NAT超え」が必要となります。その際に必要となるのが、STUNサーバーとTURNサーバーです。
TURNサーバーとは
STUNサーバーを使えば、NAT超えが必要かどうかがわかります。実際に企業で導入しているネットワークにはNAT越えが必要な場合が多く、且つ、Firewallを超える必要もでてきます。
Firewallを超える
Firewallは、ほとんどのパソコンに組み込まれた基本的なセキュリティ対策の1つです。特に、企業においては、サイバー攻撃などのリスクを防ぐため、ポート制御やウイルス感染を防ぐ役割を担います。
しかし、Firewallをセキュアに超えられる仕組みがなければ、ビジネスシーンでP2Pのような通信ができません。そこで、考えられたのが「TURNサーバー」です。
下記はデータチャネルのデモソース
深掘れば深掘るほど、奥が深く、理解がしづらいですが・・・
P2P通信でリアルタイムチャットを実際に動かしてみることで、P2Pについての理解が深まった気がします・・・
なにより、HTMLとJavascriptだけでP2Pのリアルタイム通信ができるってすごい!
HTML
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>WebRTC Demo</title>
</head>
<body>
<h1>WebRTC Data Channel Demo</h1>
<h2>シグナリング</h2>
<p>状態: <input type="text" id="status" value="" readonly="readonly" /></p>
<h3>SDPの生成</h3>
<p>(手順1) ブラウザ1で Start を押し,SDP (offer) を生成する。</p>
<button type="button" onclick="startPeerConnection()">Start</button>
<h3>自端末のSDP (Read-only)</h3>
<p>(手順2) ブラウザ1からこのSDP (offer) をコピーする。</p>
<p>(手順4) ブラウザ2で生成したこのSDP (answer) をコピーする。</p>
<textarea id="localSDP" cols="80" rows="5" readonly="readonly"></textarea>
<h3>他端末のSDP (手動でセットする)</h3>
<p>(手順3) ブラウザ2で,コピーしたブラウザ1のSDP (offer) を貼り付け Set を押すと,自端末のSDPに返答用 SDP (answer) が生成される。</p>
<p>(手順5) ブラウザ1で,コピーしたブラウザ2のSDP (answer) を貼り付け Set を押す。</p>
<textarea id="remoteSDP" cols="80" rows="5"></textarea>
<button type="button" onclick="setRemoteSdp();">Set</button>
<h2>データチャネルでの通信</h2>
<form action="javascript:sendMessage()">
<input type="text" id="message" size="30" value="" />
<input type="submit" value="Send" />
</form>
<textarea id="history" cols="80" rows="10" readonly="readonly"></textarea>
</body>
</html>
javascript
// ICE server URLs
let peerConnectionConfig = {'iceServers': [{"urls": "stun:stun.l.google.com:19302"}]};
// Data channel オプション
let dataChannelOptions = {
ordered: false,
}
// Peer Connection
let peerConnection;
// Data Channel
let dataChannel;
// ページ読み込み時に呼び出す関数
window.onload = function() {
document.getElementById('status').value = 'closed';
}
// 新しい RTCPeerConnection を作成する
function createPeerConnection() {
let pc = new RTCPeerConnection(peerConnectionConfig);
// ICE candidate 取得時のイベントハンドラを登録
pc.onicecandidate = function(evt) {
if ( evt.candidate ) {
// 一部の ICE candidate を取得
// Trickle ICE では ICE candidate を相手に通知する
console.log(evt.candidate);
document.getElementById('status').value = 'Collecting ICE candidates';
} else {
// 全ての ICE candidate の取得完了(空の ICE candidate イベント)
// Vanilla ICE では,全てのICE candidate を含んだ SDP を相手に通知する
// (SDP は pc.localDescription.sdp で取得できる)
// 今回は手動でシグナリングするため textarea に SDP を表示する
document.getElementById('localSDP').value = pc.localDescription.sdp;
document.getElementById('status').value = 'Vanilla ICE ready';
}
};
pc.onconnectionstatechange = function(evt) {
switch(pc.connectionState) {
case "connected":
document.getElementById('status').value = 'connected';
break;
case "disconnected":
case "failed":
document.getElementById('status').value = 'disconnected';
break;
case "closed":
document.getElementById('status').value = 'closed';
break;
}
};
pc.ondatachannel = function(evt) {
console.log('Data channel created:', evt);
setupDataChannel(evt.channel);
dataChannel = evt.channel;
};
return pc;
}
// ピアの接続を開始する
function startPeerConnection() {
// 新しい RTCPeerConnection を作成する
peerConnection = createPeerConnection();
// Data channel を生成
dataChannel = peerConnection.createDataChannel('test-data-channel', dataChannelOptions);
setupDataChannel(dataChannel);
// Offer を生成する
peerConnection.createOffer().then(function(sessionDescription) {
console.log('createOffer() succeeded.');
return peerConnection.setLocalDescription(sessionDescription);
}).then(function() {
// setLocalDescription() が成功した場合
// Trickle ICE ではここで SDP を相手に通知する
// Vanilla ICE では ICE candidate が揃うのを待つ
console.log('setLocalDescription() succeeded.');
}).catch(function(err) {
console.error('setLocalDescription() failed.', err);
});
document.getElementById('status').value = 'offer created';
}
// Data channel のイベントハンドラを定義する
function setupDataChannel(dc) {
dc.onerror = function(error) {
console.log('Data channel error:', error);
};
dc.onmessage = function(evt) {
console.log('Data channel message:', evt.data);
let msg = evt.data;
document.getElementById('history').value = 'other> ' + msg + '\n' + document.getElementById('history').value;
};
dc.onopen = function(evt) {
console.log('Data channel opened:', evt);
};
dc.onclose = function() {
console.log('Data channel closed.');
};
}
// 相手の SDP 通知を受ける
function setRemoteSdp() {
let sdptext = document.getElementById('remoteSDP').value;
if ( peerConnection ) {
// Peer Connection が生成済みの場合,SDP を Answer と見なす
let answer = new RTCSessionDescription({
type: 'answer',
sdp: sdptext,
});
peerConnection.setRemoteDescription(answer).then(function() {
console.log('setRemoteDescription() succeeded.');
}).catch(function(err) {
console.error('setRemoteDescription() failed.', err);
});
} else {
// Peer Connection が未生成の場合,SDP を Offer と見なす
let offer = new RTCSessionDescription({
type: 'offer',
sdp: sdptext,
});
// Peer Connection を生成
peerConnection = createPeerConnection();
peerConnection.setRemoteDescription(offer).then(function() {
console.log('setRemoteDescription() succeeded.');
}).catch(function(err) {
console.error('setRemoteDescription() failed.', err);
});
// Answer を生成
peerConnection.createAnswer().then(function(sessionDescription) {
console.log('createAnswer() succeeded.');
return peerConnection.setLocalDescription(sessionDescription);
}).then(function() {
// setLocalDescription() が成功した場合
// Trickle ICE ではここで SDP を相手に通知する
// Vanilla ICE では ICE candidate が揃うのを待つ
console.log('setLocalDescription() succeeded.');
}).catch(function(err) {
console.error('setLocalDescription() failed.', err);
});
document.getElementById('status').value = 'answer created';
}
}
// チャットメッセージの送信
function sendMessage() {
if ( !peerConnection || peerConnection.connectionState != 'connected' ) {
alert('PeerConnection is not established.');
return false;
}
let msg = document.getElementById('message').value;
document.getElementById('message').value = '';
document.getElementById('history').value = 'me> ' + msg + '\n' + document.getElementById('history').value;
dataChannel.send(msg);
return true;
}
下記がサンプルデモも有り分かりやすかったです。
https://webrtc.github.io/samples/