上級 api

Wasm + WebRTC

WebRTCとは?

WebRTC(Web Real-Time Communication)は、ブラウザ間で直接ピアツーピアの音声、動画、データ転送を可能にします — サーバーリレーは不要です。Rust/Wasmは、エンコーディング、プロトコル処理、アプリケーション状態など、クライアント側のロジック全体を駆動できます。

  従来のクライアント-サーバー       WebRTCピアツーピア

  ┌──────┐     ┌──────┐     ┌──────┐   ┌──────┐          ┌──────┐
  │Peer A│────>│Server│<────│Peer B│   │Peer A│<========>│Peer B│
  └──────┘     └──────┘     └──────┘   └──────┘  直接    └──────┘
                  ▲                                 │
             すべてのデータが                  低レイテンシ
             サーバーを経由                    サーバーコスト不要
                                              E2E暗号化

WebRTCアーキテクチャの概要

WebRTC接続にはいくつかのコンポーネントが関与します:

  ┌─────────────────────────────────────────────────────────┐
  │                    ブラウザ (Peer A)                      │
  │                                                         │
  │  ┌─────────────┐   ┌──────────────┐   ┌─────────────┐  │
  │  │  Rust        │──>│RTCPeerConn.  │──>│  ICEエージェント│  │
  │  │  Wasmコード  │   │  (ブラウザ)   │   │ (STUN/TURN) │  │
  │  └─────────────┘   └──────────────┘   └──────┬──────┘  │
  │         │                  │                   │         │
  │         ▼                  ▼                   │         │
  │  ┌─────────────┐   ┌──────────────┐           │         │
  │  │データチャネル│   │ メディア     │           │         │
  │  │(テキスト/バイナリ)│ │ ストリーム  │           │         │
  │  └─────────────┘   │(音声/動画)   │           │         │
  │                    └──────────────┘           │         │
  └───────────────────────────────────────────────┼─────────┘
                                                   │
                          ┌────────────────────────┘
                          │  STUN / TURN / 直接接続
                          ▼
  ┌─────────────────────────────────────────────────────────┐
  │                    ブラウザ (Peer B)                      │
  │              (上記と同じ構成)                           │
  └─────────────────────────────────────────────────────────┘

シグナリングプロセス

ピア同士が直接接続する前に、シグナリングサーバー(通常はWebSocketサーバー)を通じてオファーアンサーICE候補を交換する必要があります:

  Alice                   シグナリングサーバー             Bob
    │                            │                        │
    │── 1. オファー作成 ────────> │                        │
    │                            │── 2. オファー転送 ────> │
    │                            │                        │
    │                            │ <── 3. アンサー作成 ───│
    │ <── 4. アンサー転送 ────── │                        │
    │                            │                        │
    │── 5. ICE候補 ────────────> │                        │
    │                            │── 6. ICE転送 ────────> │
    │                            │                        │
    │                            │ <── 7. ICE候補 ────────│
    │ <── 8. ICE転送 ─────────── │                        │
    │                            │                        │
    │ <══════════ 直接P2P接続 ══════════════════════════> │
    │            (シグナリングサーバーは不要に)           │

SDP(Session Description Protocol)

オファーとアンサーは、機能を記述するSDP文字列です:

v=0
o=- 4567890 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=ice-ufrag:abcd
a=ice-pwd:efghijklmnop
a=fingerprint:sha-256 AA:BB:CC:...
a=setup:actpass
a=mid:0
a=sctp-port:5000

Rust/WasmでのWebRTC(web-sys使用)

web-sysクレートは、すべてのWebRTCブラウザAPIへのバインディングを提供します:

[dependencies.web-sys]
version = "0.3"
features = [
    "RtcPeerConnection",
    "RtcPeerConnectionIceEvent",
    "RtcSessionDescriptionInit",
    "RtcSdpType",
    "RtcDataChannel",
    "RtcDataChannelInit",
    "RtcDataChannelEvent",
    "RtcIceCandidateInit",
    "RtcConfiguration",
    "RtcIceServer",
    "MessageEvent",
]

ピア接続の作成

use web_sys::{RtcPeerConnection, RtcConfiguration, RtcIceServer};
use wasm_bindgen::prelude::*;
use js_sys::{Array, Object, Reflect};

fn create_peer_connection() -> Result<RtcPeerConnection, JsValue> {
    // STUN/TURNサーバーを設定
    let ice_server = Object::new();
    Reflect::set(&ice_server, &"urls".into(),
        &"stun:stun.l.google.com:19302".into())?;

    let ice_servers = Array::new();
    ice_servers.push(&ice_server);

    let config = RtcConfiguration::new();
    config.set_ice_servers(&ice_servers);

    RtcPeerConnection::new_with_configuration(&config)
}

オファーの作成

use wasm_bindgen_futures::JsFuture;

async fn create_offer(pc: &RtcPeerConnection) -> Result<String, JsValue> {
    let offer = JsFuture::from(pc.create_offer()).await?;
    let offer_sdp = Reflect::get(&offer, &"sdp".into())?
        .as_string()
        .unwrap();

    let mut desc = RtcSessionDescriptionInit::new(RtcSdpType::Offer);
    desc.sdp(&offer_sdp);
    JsFuture::from(pc.set_local_description(&desc)).await?;

    Ok(offer_sdp)
}

データチャネルのセットアップ

fn setup_data_channel(pc: &RtcPeerConnection) -> RtcDataChannel {
    let mut init = RtcDataChannelInit::new();
    init.ordered(true);

    let channel = pc.create_data_channel_with_data_channel_dict("chat", &init);

    // 受信メッセージの処理
    let onmessage = Closure::wrap(Box::new(move |evt: MessageEvent| {
        if let Some(text) = evt.data().as_string() {
            web_sys::console::log_1(&format!("受信: {}", text).into());
        }
    }) as Box<dyn FnMut(_)>);

    channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
    onmessage.forget();

    // チャネルオープンの処理
    let channel_clone = channel.clone();
    let onopen = Closure::wrap(Box::new(move |_: JsValue| {
        channel_clone.send_with_str("Hello from Rust/Wasm!").unwrap();
    }) as Box<dyn FnMut(_)>);

    channel.set_onopen(Some(onopen.as_ref().unchecked_ref()));
    onopen.forget();

    channel
}

ICE: ピア間の経路の発見

ICE(Interactive Connectivity Establishment)は、ピア間の最適なネットワーク経路を発見します:

  ICE候補の種類(優先順):

  1. ホスト候補            ← 直接LAN接続
     ┌──────┐               ┌──────┐
     │Peer A│═══════════════│Peer B│
     └──────┘  同一ネットワーク └──────┘

  2. サーバーリフレクシブ (srflx) ← STUN経由(パブリックIPを学習)
     ┌──────┐     ┌──────┐     ┌──────┐
     │Peer A│────>│ STUN │<────│Peer B│
     └──────┘     │サーバー│     └──────┘
       NAT ─ ─ ─ └──────┘ ─ ─ ─ NAT
     Peer A ═════════════════════ Peer B
              (NATを通して)

  3. リレー (relay)          ← TURN経由(中継トラフィック)
     ┌──────┐     ┌──────┐     ┌──────┐
     │Peer A│────>│ TURN │<────│Peer B│
     └──────┘     │サーバー│     └──────┘
                  └──────┘
               (全データを中継)

RustでのICE候補の処理

fn setup_ice_handling(pc: &RtcPeerConnection, ws: &WebSocket) {
    let ws_clone = ws.clone();

    let onicecandidate = Closure::wrap(Box::new(move |evt: RtcPeerConnectionIceEvent| {
        if let Some(candidate) = evt.candidate() {
            // シリアライズしてシグナリングサーバー経由で送信
            let msg = format!(
                r#"{{"type":"ice","candidate":"{}","sdpMid":"{}","sdpMLineIndex":{}}}"#,
                candidate.candidate(),
                candidate.sdp_mid().unwrap_or_default(),
                candidate.sdp_m_line_index().unwrap_or(0)
            );
            ws_clone.send_with_str(&msg).unwrap();
        }
    }) as Box<dyn FnMut(_)>);

    pc.set_onicecandidate(Some(onicecandidate.as_ref().unchecked_ref()));
    onicecandidate.forget();
}

データチャネルのメッセージ型

データチャネルはテキストとバイナリの両方のデータをサポートします:

// テキストメッセージ
channel.send_with_str("Hello!")?;

// バイナリメッセージ(例:ゲーム状態、ファイルチャンク)
let data: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04];
let array = js_sys::Uint8Array::from(&data[..]);
channel.send_with_array_buffer(&array.buffer())?;

順序付きチャネルと非順序チャネル

特性 順序付き (ordered: true) 非順序 (ordered: false)
メッセージ順序 保証あり 順序が入れ替わる可能性あり
レイテンシ 高い(先頭ブロッキング) 低い
ユースケース チャット、コマンド ゲーム状態、動画フレーム
再送 あり オプション (maxRetransmits)
// 非信頼・非順序 — リアルタイムゲーム状態に最適
let mut init = RtcDataChannelInit::new();
init.ordered(false);
init.max_retransmits(0);  // 送りっぱなし

let game_channel = pc.create_data_channel_with_data_channel_dict("game-state", &init);

WebSocketによるシグナリング

最小限のシグナリングサーバーがピア間でSDPとICEメッセージを中継します:

// クライアント側: シグナリングサーバーに接続
use web_sys::WebSocket;

let ws = WebSocket::new("wss://signal.example.com/room/abc")?;

let onmessage = Closure::wrap(Box::new(move |evt: MessageEvent| {
    let text = evt.data().as_string().unwrap();
    // パースして処理: オファー、アンサー、またはICE候補
    // その後 pc.set_remote_description() または pc.add_ice_candidate() を呼ぶ
}) as Box<dyn FnMut(_)>);

ws.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
onmessage.forget();

シグナリングプロトコルの例

// Alice -> サーバー -> Bob
{"type": "offer",  "sdp": "v=0\r\no=..."}

// Bob -> サーバー -> Alice
{"type": "answer", "sdp": "v=0\r\no=..."}

// 双方向
{"type": "ice", "candidate": "candidate:1 1 UDP ...",
 "sdpMid": "0", "sdpMLineIndex": 0}

接続のライフサイクル

  状態マシン:

  ┌─────┐  オファー/アンサー作成  ┌────────────┐  ICE完了  ┌───────────┐
  │ New │───────────────────────>│ Connecting │───────────>│ Connected │
  └─────┘                       └────────────┘            └─────┬─────┘
                                      │                         │
                                      │ 失敗                    │ ネットワーク
                                      ▼                         │ 変更
                                ┌──────────┐                    ▼
                                │  Failed  │              ┌──────────────┐
                                └──────────┘              │Disconnected  │
                                                          └──────┬───────┘
                                                                 │ ICE
                                                                 │ リスタート
                                                                 ▼
                                                          ┌───────────┐
                                                          │ Connected │
                                                          └───────────┘

Rust/Wasm WebRTCのパフォーマンスヒント

  1. バイナリデータをRustで処理する — 動画フレームのデコード、ゲーム状態の圧縮、ペイロードの暗号化をWasm側でデータチャネルを通じて送信する前に行います。

  2. SharedArrayBufferを使用してWasmとJavaScript間でゼロコピーデータ転送を行います(クロスオリジン分離ヘッダーが必要)。

  3. 小さなメッセージをバッチ処理する — 各send()呼び出しにはオーバーヘッドがあります。複数のゲームイベントを単一のバイナリメッセージにまとめましょう。

  4. リアルタイムデータには非順序チャネルを使用する — すべての更新よりも最新の状態が重要な場合。

パターン レイテンシ 信頼性 ユースケース
順序付き + 信頼性あり 高い 完全 チャット、ファイル転送
順序付き + maxRetransmits(3) 中程度 部分的 重要なゲームイベント
非順序 + maxRetransmits(0) 最低 なし 位置情報の更新

まとめ

WebRTCにより、Rust/Wasmアプリケーションはリレーサーバーなしでピアツーピア通信が可能になります。シグナリングフェーズ(オファー、アンサー、ICE候補)はWebSocketサーバーを通じて行われますが、接続が確立されるとすべてのデータはブラウザ間で直接流れます。データチャネルは柔軟で低レイテンシのメッセージングを提供します — 正確性が重要なデータには順序付き/信頼性ありを、リアルタイムストリームには非順序/非信頼を選択しましょう。

試してみる