WebRTC ファイル転送: Browser-to-Browser チュートリアル
ファイル directly between browsers using WebRTC. Data channels, signaling, and peer connection setup for real-time ファイル共有.
WebRTCのファイル転送は、最初のハンドシェイク後はサーバーをデータパスに挟まず、RTCDataChannelを使って2つのブラウザ間で直接バイトを送ります。必要なもの:SDPオファーとICE候補を交換するシグナリングチャネル(WebSocketまたは小さなリレー)、NAT検出のためのSTUNサーバー、対称NATのためのTURNフォールバック、信頼性のある順序付き配信用に設定されたデータチャネル。ピア接続が確立したら、16KB〜256KBのチャンクをchannel.send()し、DTLS 1.2で暗号化されたSCTP接続を通じてほぼネット回線速度でバイトが流れます。
WebRTCがピアツーピアを実現する仕組み
WebRTCは魔法ではなく、ICE+SDP+DTLS+SCTPが積み重なったものです。送信側はRTCPeerConnectionを作成し、データチャネルを開き、SDPオファーを生成して、シグナリングチャネル経由で受信側に送ります。受信側が応答します。両側はICE候補(ローカルIP・STUNによるリフレクシブIP・TURNによるリレーIP)を交換し、機能するパスを見つけます。DTLS 1.2がエンドツーエンドでハンドシェイクし、SCTPがその上で信頼性のあるストリーミングを提供し、バイトが流れ始めます。
暗号化は必須で組み込まれています。無効化できません。これは重要なセキュリティ上の利点です。WebSocketと異なり、ネットワークの盗聴者から保護するためにアプリケーション層の暗号化を追加することを覚えておく必要がありません。自分のシグナリングサーバーに対する真のE2EEには、悪意のあるシグナリングサーバーが独自のDTLS証明書を差し込める可能性があるため、AES-256-GCMの第2レイヤーを追加してください。
シグナリング:WebRTCが定義しない部分
WebRTCはシグナリングを意図的にあなたに委ねています。サーバー上のWebSocketで十分です。Firebase RealtimeDBに投稿した共有ルームコードでも、手動でペーストしたSDP文字列でも構いません。重要なのは、両ピアが最終的にオファー・アンサー・ICE候補のストリームを交換することです。
Nodeでの最小限のシグナリングサーバー:
const rooms = new Map();
wss.on('connection', (ws) => {
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'join') {
const room = rooms.get(msg.room) ?? new Set();
room.add(ws); rooms.set(msg.room, room);
} else {
for (const peer of rooms.get(msg.room) ?? []) {
if (peer !== ws) peer.send(raw);
}
}
});
});
20行以下です。サーバーはファイルのバイトを一切見ません。SDPとICEのメタデータだけです。月700円程度のVPSまたはCloudflare Workersでホストして、数百の同時転送を捌けます。
ピア接続の確立
GoogleのパブリックSTUNとTURNフォールバックで接続を作成します。
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.com:3478',
username: 'user', credential: 'pass' }
]
});
住宅用接続の約15〜25%がSTUNで穴が開けられない対称NATの背後にあるため、TURNは本番サービスでは省略不可です。リレーされたトラフィックを処理できる帯域幅を持つVPSでcoturnを実行するか、XirsysやTwilioのようなマネージドTURNプロバイダーを使ってください。
オファーを作成する前にオファー側でデータチャネルを開きます。
const channel = pc.createDataChannel('file', {
ordered: true, maxRetransmits: null
});
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: offer.sdp });
順序付き+無制限の再送はTCP相当の信頼性を提供します。非順序モードはより高速ですが、アプリケーション層での再組み立てが必要です。
チャネル用のファイルのチャンク化
SCTPのメッセージ当たりの実用的な上限は256KBで、古いブラウザは16KBを超えると苦労します。安全なデフォルトはFile.slice()でファイルから読み取った16KBのチャンクです。
const chunkSize = 16 * 1024;
let offset = 0;
channel.bufferedAmountLowThreshold = 1024 * 1024;
function sendNext() {
while (offset < file.size && channel.bufferedAmount < 4 * 1024 * 1024) {
const chunk = file.slice(offset, offset + chunkSize);
chunk.arrayBuffer().then((buf) => channel.send(buf));
offset += chunkSize;
}
}
channel.onbufferedamountlow = sendNext;
sendNext();
bufferedAmountのウォーターマークにより、SCTPの送信バッファにギガバイトをキューイングしてメモリを使い果たすのを防ぎます。バッファが1MBを下回ったら4MBまで補充します。ローカルネットワークで40〜80MB/s、一般的な住宅ブロードバンドで5〜20MB/sに近いスループットが得られます。
バイトの受信とディスクへの書き込み
応答側では、着信チャネルをリッスンします。
pc.ondatachannel = ({ channel }) => {
const chunks = [];
let received = 0;
channel.onmessage = ({ data }) => {
chunks.push(data);
received += data.byteLength;
updateProgress(received);
if (received === expectedSize) finish(chunks);
};
};
500MBを超えるファイルはRAMに蓄積しないでください。File System Access APIを使ってディスクに直接ストリームしてください。
const handle = await window.showSaveFilePicker({ suggestedName: fileName });
const writable = await handle.createWritable();
channel.onmessage = async ({ data }) => writable.write(data);
FirefoxとSafariはまだshowSaveFilePickerをサポートしていないため、それらのブラウザではBlob+URL.createObjectURLダウンロードにフォールバックし、上限を2GBにしてください。
バイトの前にファイルメタデータを送信する
受信者はバイトストリームが始まる前にファイル名・サイズ・MIMEタイプを知る必要があります。データチャネルで小さなJSONハンドシェイクを使います。
channel.send(JSON.stringify({
type: 'metadata', name: file.name,
size: file.size, mime: file.type, sha256: fileHash
}));
その後バイナリモードに切り替えます。受信者はdataが文字列かArrayBufferかで切り替えます。転送後の整合性検証のためにファイルのSHA-256を含め、アプリケーション層のAES-GCMを追加している場合はオプションでキーフィンガープリントも含めてください。
接続障害への対処
WebRTCデータチャネルは3つの方法で失敗します。ICEが完了しない(NAT・ファイアウォールブロック)、DTLSハンドシェイクが失敗する(クロックスキュー・証明書の問題)、または転送中に接続が切れる(ラップトップのスリープ・ネットワーク変更)。pc.oniceconnectionstatechangeをリッスンして'failed'または'disconnected'に対処してください。Chromeは'failed'に移行する前に数秒'disconnected'を保持しますが、Safariはより短気です。
転送中断時は、接続全体を再構築せずにICEを再起動します。
await pc.restartIce();
const offer = await pc.createOffer({ iceRestart: true });
// シグナリング経由で再送
再起動が失敗した場合は、サーバー経由の再開可能なアップロードにフォールバックしてください。WormholeやjustbeamitのようなWebRTC転送ツールの一部がこのパターンを採用しているのは、純粋なP2Pではどうしても機能しないネットワーク状況が全体の15%存在するからです。
自分のサーバーに対するエンドツーエンド暗号化の追加
WebRTC組み込みのDTLSはネットワーク攻撃者から守りますが、悪意のある・または侵害されたシグナリングサーバーからは守りません。真のE2EEには、両ピアがECDH P-256鍵ペアを生成し、短い帯域外コード(QRコードまたは6単語のパスフレーズ)で公開鍵を交換し、HKDF-SHA256で共有シークレットを導出し、sendを呼び出す前に各データチャネルメッセージをAES-256-GCMで暗号化します。これにより、シグナリングサーバーがDTLS証明書を差し替えても、ファイルを読むことができません。
HexaTransferはハイブリッドアーキテクチャを採用しています。クライアントサイドのAES-256-GCMで暗号文をサーバー側に保存し、P2Pの直接性とオフラインでも利用可能な共有を交換しています。hexatransfer.com で試せます — 無料、アカウント不要、最大10GB。
WebRTCが勝つ場面と負ける場面
WebRTCのファイル転送が輝くのは、両ピアが同時にオンラインの場合、自分のサーバーからのプライバシーが重要な場合、そしてリレー帯域幅のコストが大きい十分に大きなファイル(100MB超)の場合です。負けるのは、ユーザーが送信して離れたい場合、受信者が数時間後にリンクを開く場合、またはSTUNとTURNをブロックする制限的な企業ネットワークにいる場合です。汎用の転送ツールとして見ると、純粋なP2Pが快適にカバーできるのはユースケースの約60%です。残り40%はサーバーバックのフォールバックが必要です。これが「P2Pファイル転送」を謳うほぼすべての製品がアーキテクチャのどこかにリレーを持っている理由です。
エンドツーエンド暗号化で大容量ファイルを安全に送信
エンドツーエンド暗号化で最大10GBのファイルを無料で転送。アカウント不要。ファイルはアップロード前にブラウザで暗号化されるため、他の誰にも読まれません。
ファイルを送信