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

WebRTC DataChannel と OPFS と sqlite-wasm を組み合わせた P2P SNS 試作 その1
概要 P2P で動くSNS Webアプリが作ってみたくなって、ChatGPT と相談しながら作ってみた。 WebRTC DataChannel で P2P っぽい部分を実現し、データは完全にローカル環境の OPFS にしかない。 ストレージには SQLite を使った。WASM 版があるのでそれを OPFS に組...
合同会社うみがめ
https://www.umigame.tech/post/203
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);

A simple RTCDataChannel sample - Web APIs | MDN
The RTCDataChannel interface is a feature of the WebRTC API which lets you open a channel between two peers over which you may send and receive arbitrary data. The API is intentionally similar to the WebSocket API, so that the same programming model can be used for each.
MDN Web Docs
https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Simple_RTCDataChannel_sample
このサンプルはローカルでのコネクションだけを想定しており、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 Realtime | リアルタイム音声・動画アプリを構築
開発者はCloudflare Realtimeを使って、リアルタイムの音声・動画アプリをグローバル規模で構築できます。今すぐCloudflare Realtimeで構築を始めましょう。
https://www.cloudflare.com/ja-jp/developer-platform/products/cloudflare-realtime/
コードとしては以下のようになる。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 については次回。。