こんにちは!2ちゃんねる互換掲示板システム「 つゔぁいちゃんねる(2ch.biz) 」の開発録、第33回です。
前回(第32回)は、部屋主が寝落ちしても安心な「ホストなすりつけ(管理権限移譲)機能」を実装しました。
コミュニティの居心地が良くなるにつれ、次に活発化したのが 「画像やスクリーンショット、ドキュメント、zipアーカイブなどのファイル共有」 です。
しかし、初期プロトタイプでファイルを送受信できるようにした途端、現場から悲鳴が上がりました。
「10人いる部屋で誰かが100MBの動画ファイルを投げた瞬間、送信者のPCが固まり、受信者全員のスマホのギガ(通信量)が一瞬で吹き飛んだ!」
「興味のない重いzipファイルを勝手に送りつけられてブラウザのタブがクラッシュした!」
本稿では、このWebRTCフルメッシュP2P特有の「帯域爆発問題」を解決するために下した、 Push型から「オンデマンドPull型(手動承認)」への大転換 について解説します!
1. なぜ「全自動Push型」P2P転送は破綻するのか?
チャットアプリでファイル送信といえば、送信者がファイルをドラッグ&ドロップすると受信者の画面に画像がポッと表示されるのが一般的です。
しかし、中央サーバーがなく全参加者が直接メッシュ状に繋がっているP2P環境において、従来の「全員へ強制送信(Push型)」を行うと、以下のような 帯域・メモリの二重地獄 が発生します。
-
送信者の上り回線のパンク :
- 部屋に10人の参加者がいた場合、100MBのファイルを送ると送信者は
100MB × 9人 = 900MBのアップロードを行わなければならず、家庭用回線では確実にバッファが詰まってフリーズします。
- 部屋に10人の参加者がいた場合、100MBのファイルを送ると送信者は
-
受信者の不要なパケット・メモリ浪費 :
- スマートフォン(4G/5G)で参加している人にとって、見たくもない巨大ファイルを勝手にダウンロードさせられるのは「ギガの強奪」に他なりません。
2. 解決策:メタデータ先行配信 + オンデマンドPull型プロトコル
この問題を根本解決するため、私たちはファイル転送プロトコルを完全に 「Pull型(欲しい人だけがダウンロードする)」 へと再構築しました。
プロトコルの3大原則
-
実データは要求されるまで1バイトも流さない :
- 最初の共有時には、ファイル名・サイズ・MIME・一意なFileIdなどのメタデータだけを暗号化パケット(
file_offer)として通知する。
- 最初の共有時には、ファイル名・サイズ・MIME・一意なFileIdなどのメタデータだけを暗号化パケット(
-
1対1のダイレクトストリーミング :
- 「ダウンロード」ボタンを押した相手とだけ1対1でDataChannelを使い、16KBごとのチャンクに分割して送信する。
-
無関心な参加者への影響はゼロ :
- ダウンロードボタンを押さない参加者にはバイナリデータが一切流れないため、スマホの通信量もブラウザメモリも一切消費されない。
3. WebRTCの鬼門:送信バッファ詰まり(Backpressure制御)
大容量ファイルをWebRTC DataChannelで高速転送する際、単純な for ループで dataChannel.send(chunk) を回すと、ブラウザ内部の送信バッファ(bufferedAmount)が瞬時に溢れ返り、パケットロスやブラウザのクラッシュを引き起こします。
これを防ぐため、 Backpressure(背圧)制御 を厳密に組み込みました。
/**
* 送信バッファの詰まりを検知し、安全にチャンクを送るストリーミング送信機
*/
async function streamFileChunks(peer, fileId, fileBlob) {
const CHUNK_SIZE = 16 * 1024; // 16KB
const totalChunks = Math.ceil(fileBlob.size / CHUNK_SIZE);
const channel = peer.dataChannel;
// バッファが64KB以下に減衰するまで送信を待機させる関数
function waitForBufferDrain() {
return new Promise((resolve) => {
if (channel.bufferedAmount <= 64 * 1024) {
resolve();
} else {
channel.onbufferedamountlow = () => {
channel.onbufferedamountlow = null;
resolve();
};
}
});
}
// バッファ減衰閾値を64KBに設定
channel.bufferedAmountLowThreshold = 64 * 1024;
for (let i = 0; i < totalChunks; i++) {
// バッファが詰まっている場合は待機
await waitForBufferDrain();
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, fileBlob.size);
const chunkBlob = fileBlob.slice(start, end);
const arrayBuffer = await chunkBlob.arrayBuffer();
// チャンクパケットの作成
const packet = {
type: 'file_chunk',
fileId: fileId,
chunkIndex: i,
totalChunks: totalChunks,
data: arrayBufferToBase64(arrayBuffer)
};
// 暗号化して送信
sendEncryptedPacket(channel, packet);
}
}
4. UI/UX:洗練されたファイル共有カードとインライン進捗
チャットログに流れるファイル共有表示も、使いやすさを極限まで高めました。
① ファイル共有カード
チャットログ内に、ファイル名、ファイルサイズ(KB/MB表記)、P2P転送バッジ、そして目立つ青色の 「📥 ダウンロード (P2P)」 ボタンを配置。
② インライン進捗バー
ダウンロードボタンを押すと、ボタン自体がプログレスバーへと滑らかに変形し、リアルタイムに 「⏳ 45% 受信中 (12.4MB / 27.5MB)」 と進捗を表示します。
③ 完了後の自動保存とBlob解放
全チャンクが揃った瞬間に、ブラウザ標準のダウンロードダイアログが発火してファイルがディスクに保存されます。カードのボタンは 「💾 保存 (85MB)」 リンクへと切り替わり、いつでも再保存が可能です。
5. 実証検証:Push型とPull型の通信量比較テスト
複数端末を繋いだテスト環境において、50MBのアーカイブファイルを共有した場合のネットワーク負荷を計測しました。
| 項目 | 従来のPush型(全員強制配信) | 今回のPull型(オンデマンド) | 改善効果 |
|---|---|---|---|
| 参加者数 | 5人(送信者1、DL希望者1、未希望者3) | 5人(同左) | - |
| 送信者の総送信量 | 200 MB (50MB × 4人) | 50.01 MB (メタデータ + 1人分) | 75% 削減 |
| 未希望者の受信量 | 50 MB (全員強制受信) | 1.2 KB (メタデータ通知のみ) | 99.99% 削減! |
| ブラウザの安定性 | 送信中の一時的フリーズ発生 | 全員コマ落ちゼロで超快適 | 劇的改善 |
ダウンロードを希望した1人にだけ確実にデータが届き、他の参加者の回線と端末リソースは完全に無傷で保護されることが実証されました。
6. まとめと次回予告
WebRTCにおけるファイル共有の最大の鬼門であった「帯域爆発問題」を、オンデマンドPull型プロトコルとBackpressure制御によって克服しました。
しかし、ファイル転送が誰でも手軽にできるようになったことで、次に直面したのが 「セキュリティと可用性の壁」 でした。
-
改ざん・ウイルス混入の脅威 :
- 悪意あるユーザーが「便利なツールだよ」と言って中身をウイルスやトロイの木馬にすり替えて送信してきたらどう防ぐのか?
-
送信者が落ちたらダウンロードできない問題 :
- 巨大ファイルをダウンロードしている途中で元の送信者(Alice)がブラウザを閉じてしまったら、ダウンロードは永久に失敗してしまうのか?
この2つの難問を同時に打ち破ったのが、BitTorrentの思想を取り入れた 「SHA-256ハッシュ検証」 と 「シーダー分散再配布」 です。
次回は、 【第034回】悪意あるファイル改ざん・ウイルス混入を許さない!SHA-256ハッシュ検証とシーダー分散再配布 をお届けします。お楽しみに!
本番サービス : https://2ch.biz/