← 戻る

WebRTC DataChannel と OPFS と sqlite-wasm を組み合わせた P2P SNS 試作 その2

前回の記事

WebRTC DataChannel を使った通信の実装

WebSocket のシグナリングサーバーができたら、 WebRTC を使って実際にピア同士をつなぐ。

ピア同士をつなぐには、SDPなるプロトコルとICEフレームワークなるものを使って互いに送信し、コネクションを作る。

具体的には、MDN にある以下のようなコードを書く必要がある。

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);

このサンプルはローカルでのコネクションだけを想定しており、STUN や TURN サーバーを使わない例になっている。
実際には、開発中は Google の無料の STUN サーバー stun:stun.l.google.com:19302 とかを使うことが多いと思う。

const peerConnection = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] });

また、シグナリングサーバーを経由して、 offer, answer, iceで接続、みたいな感じでフェーズを分けてデータを送り合うという実装もよくあるようだ。

TURN サーバーの組み込み

開発中は STUN サーバーを利用する方法でたいてい問題ないと思うが、実際の運用では WiFi ネットワーク外のモバイルネットワークとかで接続しようとした場合は TURN サーバーが必要になる。(詳しく説明したサイトが無数に存在するので説明は省略)

TURN サーバーは OSS を使って自分で立てたりもできるようだが、お金がかかるのは嫌なので、 Cloudflare の無料サーバーを使わせてもらうことにした。1000GB/月 までの転送量を無料で使える。

コードとしては以下のようになる。Cloudflare に TURN サーバーのアドレスや認証情報を動的に問い合わせる形式。

// サーバー側 async function getTurnCredentials(env: Env) { if (!env.CLOUDFLARE_TURN_REQUEST_URL || !env.CLOUDFLARE_TURN_API_TOKEN) { return new Response( JSON.stringify({ error: "Missing TURN configuration: CLOUDFLARE_TURN_REQUEST_URL or CLOUDFLARE_TURN_API_TOKEN is not set.", }), { status: 500, headers: { "Content-Type": "application/json", ...coopCoepHeaders, }, } ); } const response = await fetch(env.CLOUDFLARE_TURN_REQUEST_URL, { method: "POST", headers: { "Authorization": `Bearer ${env.CLOUDFLARE_TURN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ ttl: 86400, }), }); return response.json(); }
// クライアント側 // useCloudflareTurn.tsx import { useEffect, useState } from "react"; export type TurnIceServer = { urls: string[]; username: string; credential: string; }; export type TurnCredentials = { iceServers: TurnIceServer[]; }; export default function useCloudflareTurn() { const [turnCredentials, setTurnCredentials] = useState<TurnCredentials | null>(null); const [turnError, setTurnError] = useState<string | null>(null); const [turnLoading, setTurnLoading] = useState(false); useEffect(() => { const fetchTurnCredentials = async () => { setTurnLoading(true); try { const response = await fetch('/turn'); if (!response.ok) { const errorText = await response.text(); setTurnError(errorText || `Failed to fetch TURN credentials: ${response.status}`); setTurnCredentials(null); return; } const data = await response.json(); setTurnCredentials(data as TurnCredentials); } finally { setTurnLoading(false); } }; fetchTurnCredentials(); }, []); return { turnCredentials, turnError, turnLoading }; } // home.tsx export default function Home() { // 省略 const { turnCredentials, turnError, turnLoading } = useCloudflareTurn(); const peerConnection = new RTCPeerConnection({ iceServers: turnCredentials.iceServers, }); // 省略 }

MPがつきたので、OPFS と sqlite-wasm については次回。。

<!-- # 感想 P2Pのサービスは接続している人がいなければ成り立たない。そのため、いかに繋がりっぱなしにしてもらうか、という課題が発生する。 既存のP2Pサービスは、アップロードした人が優先的にダウンロードを受け取れる仕組みだとか、IPFSみたいにファイルをホストし続ければ報酬がもらえるとかの仕組みでインセンティブを設計している。 しかしそういう仕組がなければ、接続しっぱなしにするための動機が生まれず、データを非同期的にネットワーク上に維持することが難しくなる。 P2Pを使ったら何か面白いSNSが生まれるのではと思ったが、完全に浅はかだった。自分の頭の悪さを再確認するだけで終わった。 -->