【WebRTCのお勉強】2つのDockerコンテナ間を利用してWebRTCのシグナリングの基礎を考える
※ 当ページには【広告/PR】を含む場合があります。
2021/07/14
テスト環境の事前準備
テスト環境の確認
$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 10 (buster)
Release: 10
Codename: buster
$ docker --version
Docker version 20.10.5, build 55c4c88
検証用コンテナの準備
$ tree
.
├── Dockerfile
├── client_listener.js
├── client_emitter.js
├── server.js
└── package.json
{
"name": "wrtc-dckr",
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"websocket": "~1.0.34",
"wrtc": "~0.4.7",
"@mapbox/node-pre-gyp": "^1.0.5"
}
}
FROM ubuntu:18.04
LABEL your_name <your-name@email.com>
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y
RUN apt-get install -y sudo
RUN apt-get install curl git zip -y
RUN apt-get install python2.7 libncurses5-dev libexpat-dev -y
RUN apt-get install w3m -y
RUN apt-get install build-essential -y
#Add sudo user & group
RUN groupadd -g 1000 developer && \
useradd -g developer -G sudo -m -s /bin/bash your_name && \
echo 'your_name:developer' | chpasswd
RUN echo 'Defaults visiblepw' >> /etc/sudoers
RUN echo 'your_name ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
#Install Node12
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash
RUN apt-get install --yes nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install yarn
ENV NODE_ENV development
WORKDIR /usr/src/app
COPY package.json /usr/src/app/
RUN cd /usr/src/app && yarn install && yarn cache clean
COPY client_listener.js /usr/src/app/
COPY client_emitter.js /usr/src/app/
COPY server.js /usr/src/app/
USER your_name
ENV PS1="[\u@\h:\w]$"
CMD ["bash"]
node-webrtc
ubuntu:18.04
node-pre-gyp
$ docker build -t wrtc-dckr:ubuntu18-node12 .
Sending build context to Docker daemon 6.656kB
#...中略
Successfully tagged wrtc-dckr:ubuntu18-node12
$ docker images | grep wrtc
wrtc-dckr ubuntu18-node12 6ef36753bafe 5 hours ago 542MB
TypeScript&Node.jsのネットワーク入門 TypeScriptネットワークプログラミング―HTML5/WebSocket/WebRTCによる
同一イメージから2つのDockerコンテナを立ち上げる方法
wrtcc1
#👇コンテナ1生成 + bashでターミナル起動
$ docker run --name wrtcc1 -it wrtc-dckr:ubuntu18-node12 bash
f05393335494
bash-5.0# hostname
f05393335494
bash-5.0# hostname -i
172.17.0.2
#デタッチで外に出る: Ctrlキー + Pキー → Ctrlキー + Qキー と入力
bash-5.0# read escape sequence
-d
#👇コンテナ1生成 + bashでターミナル起動 + デタッチで外に出る
$ docker run --name wrtcc1 -d -it wrtc-client:node12 bash
wrtcc2
#コンテナ2生成 + デタッチで外に出る + bashでターミナル起動
$ docker run --name wrtcc2 -it wrtc-dckr:ubuntu18-node12 bash
b96ce0f8224e
bash-5.0# hostname
b96ce0f8224e
bash-5.0# hostname -i
172.17.0.3
#デタッチ : Ctrl + P → Ctrl + Q と入力する
bash-5.0# read escape sequence
$ docker ps | grep wrtcc
b96ce0f8224e wrtc-client:node12 "docker-entrypoint.s…" 12 minutes ago Up 11 minutes wrtcc2
f05393335494 wrtc-client:node12 "docker-entrypoint.s…" 16 minutes ago Up 16 minutes wrtcc1
docker attachコマンド
#Attach Container#1
$ docker attach wrtcc1
bash-5.0# hostname
f05393335494
bash-5.0# hostname -i
172.17.0.2
#Attach Container#2
$ docker attach wrtcc2
bash-5.0# hostname
b96ce0f8224e
bash-5.0# hostname -i
172.17.0.3
TypeScript&Node.jsのネットワーク入門 TypeScriptネットワークプログラミング―HTML5/WebSocket/WebRTCによる
WebRTCテスト環境の構成
受信側(wrtccl)
配信側(wrtcce)
call
offer
answer
TypeScript&Node.jsのネットワーク入門 TypeScriptネットワークプログラミング―HTML5/WebSocket/WebRTCによる
サーバー側(server.js)の実装
コンテナ(wrtcs)
$ docker run -d -it --name wrtcs -p 41337:41337 \
wrtc-dckr:ubuntu18-node12 bash
ws://172.17.0.1:41337
const webSocketsServerPort = 41337;
const webSocketServer = require('websocket').server;
const http = require('http');
const server = http.createServer((request, response) => {});
const clients = [];
server.listen(webSocketsServerPort, () => {
console.log(`${(new Date()).toISOString()} | Server is listening on port# ${webSocketsServerPort} .`);
});
const wsServer = new webSocketServer({httpServer: server});
//👇登録されたクライアントから接続requestを受け取った
wsServer.on('request', (request) => {
console.log(`${(new Date()).toISOString()} | Connection from Origin ${request.origin} .`);
const connection = request.accept(null, request.origin);
console.log(`${(new Date()).toISOString()} | Connection accepted.`);
//👇クライアントを番号で管理
const index = clients.push(connection) - 1;
//👇クライアントからmessageを受信した
connection.on('message', (message) => {
for (let i=0; i < clients.length; i++) {
if (connection === clients[i]) {
console.log('SDPの逆転送はキャンセルします');
} else {
console.log(message);
//👇接続中のすべてのwsクライアントへメッセージ送信
clients[i].sendUTF(message.utf8Data);
}
};
});
//👇クライアントが切断された
connection.on('close', (connection) => {
console.log(`${(new Date()).toISOString()} | Peer# ${connection} disconnected.`);
//👇切断したクライアントを削除
clients.splice(index, 1);
});
});
docker cpコマンド
$ docker cp server.js wrtcs:/usr/src/app/server.js
docker execコマンド
$ docker exec -it wrtcs node server.js
TypeScript&Node.jsのネットワーク入門 TypeScriptネットワークプログラミング―HTML5/WebSocket/WebRTCによる
配信側クライアント側(client_emitter.js)の実装
1. 他のピアから送られてくる「call」を受け取ったら、
データチャネルを作成し、サーバーへ「offer」のSDP情報を送る
2. サーバーの「answer」が送られてくるまで待ってからコネクションの確立する
3. コネクションが確立した後に自身の「ice」を送信する
const WebSocketClient = require('websocket').client;
const client = new WebSocketClient();
const {
RTCPeerConnection,
RTCSessionDescription,
RTCIceCandidate
} = require('wrtc');
const remoteIp = process.env.REMOTE_HOST_IP || '0.0.0.0';
const localIp = process.env.CLIENT_IP || '127.0.0.1';
let rtcPeerConnection = null;
let dataChannel = null;
//👇接続エラー時のイベント
client.on('connectFailed', (err) => {
console.log(`Connect Error: ${err}`);
});
//👇Websocket接続中のイベント
client.on('connect', (connection) => {
//👇初期化処理
console.log('シグナリングサーバーに接続中...');
//👇接続時のエラー
connection.on('error', (err) => {
console.log(`Connect Error: ${err}`);
});
//👇サーバーからのコマンド受信時
connection.on('message', async (message) => {
if (message.type === 'utf8') {
const command = JSON.parse(message.utf8Data);
if (command.type == 'call') {
console.log('CALL受信処理');
if (rtcPeerConnection == null) {
rtcPeerConnection = new RTCPeerConnection({ iceServers: [] });
//👇Media StreamかDataChannelが無ければICE Candicateが発行されない
await startDataChannel(rtcPeerConnection);
rtcPeerConnection.onicecandidate = sendIceCandidate;
}
await createOffer(rtcPeerConnection);
}
else if (command.type == 'offer') {
console.log('OFFER受信処理');
if (command.sdp) {
if (rtcPeerConnection == null) {
rtcPeerConnection = new RTCPeerConnection({ iceServers: [] });
rtcPeerConnection.onicecandidate = sendIceCandidate;
await rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(command.sdp));
await createAnswer(rtcPeerConnection);
}
}
}
else if (command.type == 'answer') {
console.log('ANSWER受信処理');
if (rtcPeerConnection && command.sdp) {
//👇リモートのsdpを記憶
await rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(command.sdp));
}
}
else if (command.type == 'candidate') {
console.log('ICE受信処理');
const candidate_ = new RTCIceCandidate(command.candidate);
await rtcPeerConnection.addIceCandidate(candidate_);
} else {
console.log(`Unknown ${command.type} Type Deteced!`);
}
}
});
//👇切断時のイベント
connection.on('close', () => {
peer.close();
console.log('ピアのコネクションを切断しました');
});
//👇DataChannelを始める
async function startDataChannel(peer_) {
if (peer_ == null) return false;
try {
//👇コネクション確立後にDataChannelを用意する
dataChannel = peer_.createDataChannel('msg_channel');
dataChannel.onopen = () => {
//👇接続時の処理
sendNumber();
};
dataChannel.onerror = (err) => {
//👇エラー時の処理
console.error(err);
};
dataChannel.onclose = () => {
//👇切断時の処理
dataChannel = null;
};
function sendNumber() {
if (dataChannel) {
const number = Math.round(Math.random() * 0xFFFFFF);
//👇dataChannel.send( data )でデータを送信
dataChannel.send(number.toString());
setTimeout(sendNumber, 1000);
}
};
return true;
} catch (err) {
console.error(err);
}
}
//👇WebSocketでコマンドを送信する
function wsSend(command) {
console.log('WebSocketから通信中...');
connection.sendUTF(JSON.stringify(command));
}
async function createOffer(peer_) {
let sdp_;
try {
sdp_ = await peer_.createOffer();
peer_.setLocalDescription(sdp_);
} catch (err) {
console.error(err)
}
wsSend({
type: 'offer',
sdp: sdp_
});
}
async function createAnswer(peer_) {
let sdp_;
try {
sdp_ = await peer_.createAnswer();
peer_.setLocalDescription(sdp_);
} catch (err) {
console.error(err);
}
wsSend({
type: 'answer',
sdp: sdp_
});
}
function sendIceCandidate(e) {
if (e.candidate) {
console.log('ICE情報の送信');
wsSend({
type: 'candidate',
candidate: e.candidate
});
}
}
});
//👇サーバーに接続
client.connect(`ws://${remoteIp}:41337`, null, `docker://${localIp}`, null, null);
call
answer
candidate
$ docker run -d -it --name wrtcce \
wrtc-dckr:ubuntu18-node12 bash
docker cpコマンド
$ docker cp client_emitter.js wrtcce:/usr/src/app/client_emitter.js
docker execコマンド
$ docker exec -it \
-e REMOTE_HOST_IP=172.17.0.1 \
-e CLIENT_IP=172.17.0.2 \
wrtcce node client_emitter.js
REMOTE_HOST_IP
CLIENT_IP
TypeScript&Node.jsのネットワーク入門 TypeScriptネットワークプログラミング―HTML5/WebSocket/WebRTCによる
受信側クライアント側(client_listener.js)の実装
1. コンテナが起動した直後、受信の開始を合図するために「call」を送る
2. サーバーから送られてくる配信側のオファーを受け取り、処理を行った上で「answer」を送信する
3. アンサーを送信した後にコネクションが確立するので、続けてサーバーへ「ice」が送られる
const WebSocketClient = require('websocket').client;
const client = new WebSocketClient();
const {
RTCPeerConnection,
RTCSessionDescription,
RTCIceCandidate
} = require('wrtc');
const remoteIp = process.env.REMOTE_HOST_IP || '0.0.0.0';
const localIp = process.env.CLIENT_IP || '127.0.0.1';
let rtcPeerConnection = null;
let dataChannel = null;
//👇接続エラー時のイベント
client.on('connectFailed', (err) => {
console.log(`Connect Error: ${err}`);
});
//👇Websocket接続中のイベント
client.on('connect', (connection) => {
//👇初期化処理
console.log('シグナリングサーバーに接続中...');
//👇データチャネルの開始・発信を要求
wsSend({
type: 'call',
channel_id: 'DataChannelテスト'
});
//👇接続時のエラー
connection.on('error', (err) => {
console.log(`Connect Error: ${err}`);
});
//👇サーバーからのコマンド受信時
connection.on('message', async (message) => {
if (message.type === 'utf8') {
const command = JSON.parse(message.utf8Data);
if (command.type == 'call') {
console.log('CALL受信処理');
if (rtcPeerConnection == null) {
rtcPeerConnection = new RTCPeerConnection({ iceServers: [] });
rtcPeerConnection.onicecandidate = sendIceCandidate;
}
await createOffer(rtcPeerConnection);
}
else if (command.type == 'offer') {
console.log('OFFER受信処理');
if (command.sdp) {
if (rtcPeerConnection == null) {
rtcPeerConnection = new RTCPeerConnection({ iceServers: [] });
await recieveDataChannel(rtcPeerConnection);
rtcPeerConnection.onicecandidate = sendIceCandidate;
await rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(command.sdp));
await createAnswer(rtcPeerConnection);
}
}
}
else if (command.type == 'answer') {
console.log('ANSWER受信処理');
if (rtcPeerConnection && command.sdp) {
//👇リモートのsdpを記憶
await rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(command.sdp));
}
}
else if (command.type == 'candidate') {
console.log('ICE受信処理');
const candidate_ = new RTCIceCandidate(command.candidate);
await rtcPeerConnection.addIceCandidate(candidate_);
} else {
console.log(`Unknown ${command.type} Type Deteced!`);
}
}
});
//👇切断時のイベント
connection.on('close', () => {
peer.close();
console.log('ピアのコネクションを切断しました');
});
//👇DataChannelを受信する
async function recieveDataChannel(peer_) {
if (peer_ == null) return false;
try {
if (dataChannel) dataChannel = null;
peer_.ondatachannel = (e) => {
//👇DataChannelを作るのではなく、配信側から渡されたものを使う
dataChannel = e.channel;
dataChannel.onopen = () => {
//👇接続時の処理
console.log('DataChannelによる配信を開始します!');
};
dataChannel.onmessage = (message) => {
//👇データ受信時の処理
console.log(message);
};
dataChannel.onerror = (err) => {
//👇エラー時の処理
console.error(err);
};
dataChannel.onclose = () => {
//👇切断時の処理
dataChannel = null;
};
};
return true;
} catch (err) {
console.error(err)
}
}
//👇WebSocketでコマンドを送信する
function wsSend(command) {
console.log('WebSocketから通信中...');
connection.sendUTF(JSON.stringify(command));
}
async function createOffer(peer_) {
let sdp_;
try {
sdp_ = await peer_.createOffer();
peer_.setLocalDescription(sdp_);
} catch (err) {
console.error(err)
}
wsSend({
type: 'offer',
sdp: sdp_
});
}
async function createAnswer(peer_) {
let sdp_;
try {
sdp_ = await peer_.createAnswer();
peer_.setLocalDescription(sdp_);
} catch (err) {
console.error(err);
}
wsSend({
type: 'answer',
sdp: sdp_
});
}
function sendIceCandidate(e) {
if (e.candidate) {
console.log('ICE情報の送信');
wsSend({
type: 'candidate',
candidate: e.candidate
});
}
}
});
//👇サーバーに接続
client.connect(`ws://${remoteIp}:41337`, null, `docker://${localIp}`, null, null);
offer
candidate
$ docker run -d -it --name wrtccl \
wrtc-dckr:ubuntu18-node12 bash
docker cpコマンド
$ docker cp client_listener.js wrtccl:/usr/src/app/client_listener.js
docker execコマンド
$ docker exec -it \
-e REMOTE_HOST_IP=172.17.0.1 \
-e CLIENT_IP=172.17.0.3 \
wrtccl node client_listener.js
REMOTE_HOST_IP
CLIENT_IP
ネットワークアプリケーションのためのパフォーマンス最適化 ハイパフォーマンス ブラウザネットワーキング
WebRTC通信の検証
$ docker exec -it wrtcs node server.js
$ docker exec -it \
-e REMOTE_HOST_IP=172.17.0.1 -e CLIENT_IP=172.17.0.2 \
wrtcce node client_emitter.js
$ docker exec -it \
-e REMOTE_HOST_IP=172.17.0.1 -e CLIENT_IP=172.17.0.3 \
wrtccl node client_listener.js
ネットワークアプリケーションのためのパフォーマンス最適化 ハイパフォーマンス ブラウザネットワーキング
まとめ
参考サイト
記事を書いた人
ナンデモ系エンジニア
電子工作を身近に知っていただけるように、材料調達からDIYのハウツーまで気になったところをできるだけ細かく記事にしてブログ配信してます。
カテゴリー