WebRTC・WebSocket・シグナリング・呼制御・Socket.ioクライアント、Socket.ioサーバー、TURN、CORN、画面共有の仕組みについて段階的に理解する。
ゴール Twilio(あるいはzoomからリアルタイム通信の仕組みを理解する)
構成
React │ Socket.io │ WebSocket(TCP) │ シグナリング │ WebRTC │ UDP │ 音声・映像
Q. 普段Web開発で使うHTTP通信と比べて、WebSocketは何が違うのか——特に「誰が」「いつ」データを送れるのか、という点に注目して調べてみてください。
Http通信の場合はライアントからリクエストを投げたらサーバーからレスポンスが入ってくると言う。1方向的な通信のやりとりを行うが、それに対してウェブソケットはリアルタイム通信、つまり双方向通信を行うことができる。
補足:
- HTTP: クライアントが要求 → サーバーが応答、で1往復が完結。次のやり取りをするには再度リクエストが必要(コネクションはその都度使い捨てに近い)。
- WebSocket: 最初の接続確立だけHTTPのハンドシェイク(Upgrade: websocket)を使うが、一度繋がるとコネクションを張りっぱなしにする。だから「クライアントから聞かれてないのにサーバーから送る」ということができる=双方向・非同期。
Q. では素のWebSocketがあるのに、なぜ実務では「Socket.io」というライブラリを使うのか? WebSocketだけでは足りない・面倒な点を調べてみてください。
WebSocketはリアルタイム通信の仕組みそのものですが、
Socket.ioは、 部屋(Room)管理 自動再接続 イベント管理 接続・切断通知 ブロードキャスト(全員・特定の部屋への送信) などの機能を提供してくれるライブラリのため、 WebSocketで一から自前実装するよりも開発効率が高く、保守しやすいため利用される。
補足: Socket.ioは「WebSocketの上位互換」ではなく、フォールバック機構を持つ点も大きいです。WebSocketが使えない環境(古いプロキシ等)では自動的にlong-pollingに切り替えます。つまりSocket.io自体が独自のプロトコルで、内部で「使えるならWebSocket、ダメならHTTPポーリング」を切り替えている、という位置づけです。
Q. Socket.io(サーバー経由の通信)があるのに、なぜビデオ通話や画面共有には別途「WebRTC」という技術が必要なのか? 音声・映像データがサーバーを経由する場合としない場合で、何が変わるか考えてみてください。
映像、音声データは通信遅延を避けるためにUDPプロトコルを利用している。サーバーを経由する場合には、TCPプロトコルによる通信となるが、TCPプロトコルの通信の場合は、信頼性の確立のための仕組みが整っている反面、音声・映像のデータはデータサイズが多すぎて、通信をする際に、データ破損などが起こりやすく、通信遅延、通信事故が発生してしまう。そのため、UWPプロトコルを利用するのに、WebRTCという通信技術を使って映像、音声データはやり取りをする必要がある
良い着眼点です。UDP/TCPの違いは正しいですが、もう一つ重要な観点が抜けています。
- ✅ プロトコルの違い: 音声・映像はSRTP(UDPベース)を使う。TCPは再送・順序保証の仕組みがある分、パケットロス時に「待ち」が発生し、リアルタイム性を損なう(多少コマ落ちしても待たされるよりマシ、という発想)。
- ⚠️ もう一つの本質: 仮にサーバー経由でUDPを使えたとしても、全員の映像・音声データがサーバーを通ると、参加者が増えるほどサーバーの帯域・処理コストが跳ね上がり、サーバーが1つ余分な中継点=遅延要因にもなる。WebRTCの一番の価値は「サーバーを介さずピア同士が直接繋がる(P2P)」こと。UDPを使うのは手段の一つで、目的は「サーバーを経由しない直接通信」を実現すること。
Q. ピア同士が「直接」繋がるには、事前にお互いのIPアドレスなどの接続情報を知る必要があります。でも見知らぬ相手同士がいきなり直接通信することはできません。この「最初の情報交換」はどうやって行うのでしょうか? (ここでSocket.ioが再登場します)
自分の回答
| |
- ✅ 仕組みの本質: シグナリングとは「まだP2Pで直接繋がっていない2人が、繋がるために必要な情報を、既存の別の通信路(Socket.io/WebSocketなど)経由でやり取りすること」。
- 📌 重要な補足: WebRTC自体は「シグナリングをどうやるか」を規定していません。Socket.ioでも、素のWebSocketでも、極端な話メールでコピペしてもいい(実用的ではないですが)。あくまで**サーバーを介した「仲介」**であって、通信方式は自由です。
- 📌 実際に交換するものは次の2種類です: a. SDP(Session Description Protocol): 「どんなコーデック・解像度で通信したいか」という能力・条件の記述 b. ICE candidate: 実際の接続先候補(IP:ポートの組み合わせ)のリスト
このSDPのやり取りの手順こそが「呼制御」です。
SDP, ICEがわからない。
直接説明します。
SDP(Session Description Protocol) 実際の映像・音声データではなく、「私はこういう通信をしたい/できます」というスペック表のテキストです。中身の例:
- 音声/映像どちらを使うか
- 対応コーデック(映像ならVP8, H264など、音声ならOpusなど)
- 解像度・ビットレート
- 送受信の方向(送るだけ/受けるだけ/双方向)
これを2人が交換して「じゃあお互いが対応してる形式はこれだね」とすり合わせる。
ICE(Interactive Connectivity Establishment) 「実際にどの経路(IP:ポート)でデータを送れば相手に届くか」を探す仕組みです。NATやファイアウォールがあると自分の本当のグローバルIPが分からないので、候補(ICE candidate)を複数集めます。
- 自分のローカルIP
- STUNサーバーに問い合わせて分かった「外から見た自分のIP:ポート」
- (直接繋がらない時の最終手段の)TURNサーバー経由のアドレス
これらの候補リストをお互いに交換し、実際に繋がるかテスト(疎通確認)して、一番良い経路を選びます。
例えるなら: SDPは「私は日本語と英語話せます、ビデオ通話できます」という自己紹介カード。ICEは「直通番号/転送番号/交換手経由、どの電話番号なら実際に繋がるか」を試す作業です。
では改めて。
Q. Offer/Answerのやり取りは、どちらが先に「SDP」を作って送るのでしょうか?発信する側(かける人)と、受ける側(かけられる人)、どちらが Offer を作ると思いますか?
自分の回答 発信側がSDPを作って送る。
- 発信側(Caller): createOffer() でSDP(Offer)を作る → 自分にセット(setLocalDescription) → シグナリングサーバー(Socket.io)経由で相手に送る
- 着信側(Callee): Offerを受け取る → 自分にセット(setRemoteDescription) → createAnswer() でSDP(Answer)を作る → 自分にセット(setLocalDescription) → シグナリング経由で送り返す
- 発信側: Answerを受け取り setRemoteDescription → お互いの「スペック表」が揃う
- これと並行して、両者は自分のICE candidateを見つけ次第、随時シグナリング経由で相手に送り続ける(onicecandidateイベント)
- 双方がSDP交換+十分なICE candidateの疎通確認を終えると、直接のP2P接続が確立する
つまりOffer/AnswerとICE candidateの交換は同時並行で進みます。SDPが「何を話すか」、ICEが「どの経路で話すか」の役割分担です。
localDescription と remoteDescriptionにセットするとはどういうことなのか? RTCPeerConnectionオブジェクトは内部に localDescription と remoteDescription という**2つのプロパティ(変数のようなもの)**を持っています。それぞれにSDPのテキストデータが1つずつ格納される、という意味で「スロット(入れ物)」と表現しました。
const pc = new RTCPeerConnection(); // pc.localDescription ← ここに自分のSDPが入る // pc.remoteDescription ← ここに相手のSDPが入る
await pc.setLocalDescription(offer); // pc.localDescription に offer が代入される await pc.setRemoteDescription(answer); // pc.remoteDescription に answer が代入される
つまり「オブジェクトが持つ2つのプロパティに、それぞれSDPを書き込む」というだけの操作です。特別な仕組みではなく普通のオブジェクトの状態管理だと考えてもらえれば大丈夫です。
実際のSDPはこんなテキストです(簡略化した例)。
v=0 o=- 4611731400430051336 2 IN IP4 127.0.0.1 s=- t=0 0 m=audio 9 UDP/TLS/RTP/SAVPF 111 103 a=rtpmap:111 opus/48000/2 a=fmtp:111 minptime=10;useinbandfec=1 a=sendrecv m=video 9 UDP/TLS/RTP/SAVPF 96 97 a=rtpmap:96 VP8/90000 a=rtpmap:97 H264/90000 a=sendrecv
読み方:
- m=audio … / m=video …: 「音声トラックと映像トラックがありますよ」という宣言
- 111, 103, 96, 97: 対応コーデックの候補番号(複数提示している=「これらのどれかで話せます」)
- a=rtpmap:111 opus/48000/2: 番号111は「Opusコーデック、サンプリングレート48000Hz、2チャンネル(ステレオ)」という意味
- a=rtpmap:96 VP8/90000: 番号96は「VP8コーデック」
- a=sendrecv: 「送信も受信もします」(sendonly/recvonly/inactiveもあり得る)
これが両者から届くと、ブラウザは自動で「相手も自分もVP8とOpusに対応してるから、それを使おう」とすり合わせます。人間が読むというより、ブラウザ同士が機械的に照合するためのフォーマットです。
イメージつきましたか?OKなら、STUN/TURNの問いに進みましょう。
Q. STUNサーバーとTURNサーバーは何が違うのでしょうか?STUNだけでは繋がらないケースがあるのはなぜでしょうか?
気になった点 ❯ 発信側コーラーがSD Pを作るのは理解しましたが、自分にセットすると言うのはどういうことでしょうか?また着信側もオファーを受け取った後に自分にセットしていますが、ここのセットと言うのは具体的にどういったこと?リモートとlocalのセットがありそう。
次の問い、STUN/TURNです。
Q. ICE candidateを集める際に「STUNサーバー」と「TURNサーバー」という2種類が出てきます。この2つは何が違うのでしょうか?STUNだけでは繋がらないケースがあるのはなぜでしょうか?
Q. 電話をかける時の「発信→呼び出し中→応答→通話→切断」のような一連の流れを、WebRTCでは「Offer」「Answer」という言葉を使って表現します。この Offer/Answer のやり取りはどういう順番で進むと思いますか? 調べて図解できそうなら流れを書いてみてください。
参考
socket.ioライブラリ https://socket.io/docs/v4/tutorial/step-1