【JavaScript 応用講座】:[P2P通信入門] simple-peerでシグナリングと接続を理解する

🏁 はじめに

■ WebアプリでP2P通信をしたいと思った理由

ゲーム開発やアセット共有を進める中で、「ユーザー同士が直接データをやり取りできたら便利なのでは?」というアイデアが浮かびました。 たとえば、自作のカードゲームエディタで作成したデータを、サーバを経由せずに別のユーザーと共有できたら、手軽かつプライバシーも守れる

特に、

  • Webアプリをローカルで完結させたい
  • サーバ構築や維持のコストを抑えたい
  • ユーザー間の小規模なデータ共有を実現したい

といった背景から、P2P(ピア・ツー・ピア)通信への関心が高まりました。


■ WebRTCは難しそう…でも simple-peer で意外と簡単に試せた

ブラウザ間のP2P通信といえば「WebRTC」が有名ですが、 調べてみると構成が複雑で、

  • ICE candidate?
  • STUN/TURNサーバ?
  • シグナリングって何?
  • SDPってなにかの暗号?

……と、完全に初心者殺しの世界。

そんな中で見つけたのが simple-peer というライブラリ。 これを使えば、WebRTCの複雑な部分をほぼ隠蔽してくれて、以下のような短いコードで P2P接続を確立し、データをやりとりできる ことがわかりました。

const peer = new SimplePeer({ initiator: true, trickle: false });

peer.on('signal', data => {
  console.log(JSON.stringify(data)); // シグナリング情報をコピー
});

peer.signal(remoteSignalData); // 相手から受け取った情報をここに入力

peer.on('connect', () => {
  peer.send("こんにちは!");
});

peer.on('data', data => {
  console.log("受信:", data.toString());
});

難解なWebRTCの山を前にしても、これなら “試してみる” ところまではすぐにたどり着ける。 この「手軽に入門できる」体験は、WebRTC学習のハードルをぐっと下げてくれました。


■ 今回のゴール:ブラウザ上でP2P接続を確立し、テキスト送信する

この記事では、以下のシンプルなP2P通信を ブラウザだけで完結 させることを目指します。

  • ① 自分が「接続情報(Signal)」を生成して
  • ② 相手にその情報を渡し(コピペでOK)
  • ③ 相手が返してくれた情報を再び自分が受け取り
  • ④ 接続が確立したら、テキストを送受信

ここまで到達すれば、次のステップとしてファイル送信やゲームデータ同期など、 より実践的なP2P活用にも応用できるようになります。

まずは 「相手に声を届ける」その最初の一歩 を、一緒に試してみましょう。

📦 使用ライブラリと構成

今回のP2P通信デモは、できるだけシンプルに、最小限の構成で動かすことを意識しました。 特別なビルドツールやフレームワークは使わず、HTMLとJavaScriptだけで完結します。


✅ 使用ライブラリ:simple-peer

simple-peer は、WebRTCの複雑な処理を隠蔽してくれる便利なラッパーライブラリです。

読み込み方法は2つあります。

1. CDNから直接読み込む(簡単)

<script src="https://unpkg.com/simple-peer@9.11.1/simplepeer.min.js"></script>

この方法なら、ファイルをダウンロードせずにすぐ使えます。 テストや学習目的ならこの方法で十分です。

2. ローカルにダウンロードして使う(安定)

CDNが使えない環境や、完全オフラインでの使用商用利用での検証には、ライブラリをダウンロードしておくと安心です。

<script src="./js/simplepeer.min.js"></script>

📁 ファイル構成例:

/sample/card-game/sha256/
├── index.html
├── p2p.js
└── js/
    └── simplepeer.min.js

🧩 構成:HTML + JS のみ

以下のように、HTMLとJavaScriptファイルを分けた非常にシンプルな構成にしました。

  • index.html:画面表示とボタンなどのUI
  • p2p.js:P2P通信のロジック
  • simplepeer.min.js:ライブラリ(CDNまたはローカル)

CSSは最低限に抑え、主に動作確認に集中できる構成です。


🌐 実行環境:ローカル or GitHub Pages

このプロジェクトは以下のような 静的サーバー 上で動かせます。

  • Visual Studio Code + Live Server 拡張機能(ローカル)
  • GitHub Pages(無料公開用)
  • Netlify / Vercel などのホスティングサービス

⚠ 注意:file://から直接HTMLを開くと、セキュリティ制限で動かない場合があります。 必ず http://localhost:XXXXhttps://... のようなURLで開いてください。

🧪 P2Pの基本フロー

simple-peer を使った WebRTC の通信は、次のようなシンプルなステップで構成されています。 「仲介サーバなし」でも手動で接続できる流れを見ていきましょう。


1. initiator: true 側が offer を生成

まず、片方のピア(ブラウザ)が initiator: true として初期化されると、自動的に「接続の提案(offer)」が生成されます。

const peer = new SimplePeer({
  initiator: true,
  trickle: false, // ICE候補をまとめて送る
});

このとき、以下のような signal イベントが発火します:

peer.on('signal', (data) => {
  console.log("生成されたOffer:", JSON.stringify(data));
});

2. 相手に手動で渡す(textarea + copy)

signal で得られた data(JSON)は、相手に伝える必要があります。 今回は、HTMLのテキストエリアを使って、コピー&ペーストで手動送信する形にしました。

💡このステップが、いわゆる「シグナリング」にあたります。


3. 相手が signal() に入れて answer を返す

受け取った相手側は、initiator: false としてピアを生成します。

const peer = new SimplePeer({
  initiator: false,
  trickle: false,
});

その後、送られてきた offerpeer.signal() に渡すことで処理が開始され、今度は相手側から answer が生成されます。

peer.signal(JSON.parse(remoteOfferJSON));

この answer も、再び手動でコピーして最初の人へ返します。


4. 双方 connect イベントが発火 → 通信確立

両者が signal() を適切に呼び出すと、自動的にNAT越えなどを交渉して接続が確立されます。

このとき peer.on('connect') が発火し、データ通信の準備が整った合図になります。

peer.on('connect', () => {
  console.log("✅ P2P 接続が確立しました!");
});

5. peer.send(“こんにちは!”) で通信確認

接続が確立すれば、文字列でもファイルでも自由にやり取りが可能です。 まずは試しにテキストを送信してみましょう。

peer.send("こんにちは、P2Pの世界へ!");

受信側は以下のイベントでキャッチできます:

peer.on('data', (data) => {
  console.log("📩 受信:", data.toString());
});

📌 補足:タイミングに注意 connect イベントより前に send() すると 送信に失敗するので、 必ず connect 後に送るようにしましょう。

🔐 シグナリングJSONの正体を読み解く

P2P通信を実現するためには、まずお互いに「自分の通信能力を自己紹介」する必要があります。 この自己紹介に使われるのが signal() に渡される 巨大なJSONデータ です。

実際に見たことがあるかもしれませんが、こんな感じの長い文字列です:

{
  "type": "offer",
  "sdp": "v=0\r\no=- 3431...(省略)"
}

このJSONは、2つのパーツに分けられます:


✅`type: “offer” or “answer”

このフィールドは、そのJSONが「誰のターンか?」を示します。

意味
"offer" initiatorが生成した接続要求
"answer" 相手がそれに応じた返答

simple-peer はこの辺をいい感じに抽象化してくれます。


📜 sdp: セッション記述プロトコル(Session Description Protocol)

この sdp の中に、通信のためのあらゆる情報が詰め込まれています。 内容はプレーンテキストで、例えば次のような情報が含まれています:

💡 ICE candidate(通信経路候補)

a=candidate:1840622005 1 udp 2113937151 192.168.1.10 58712 typ host

これは「自分が使えるIPとポートの候補」です。 複数の候補(ローカルIP、グローバルIP、STUNサーバ経由など)が並びます。

🌐 IP / Port情報(変わる!)

c=IN IP4 203.0.0.1

NATやプロキシを越えるために、自分の「見える外向きアドレス」も含まれます。 そのため、起動のたびに違う値になることもあります。

🔐 fingerprint(暗号鍵のハッシュ)

a=fingerprint:sha-256 07:BC:D3:...

これは 通信の相手の正当性を確認するための指紋情報です。 例えるなら、SSHの known_hosts に保存されてる「この人、信用してOK?」というハッシュと同じような意味です。

WebRTCは エンドツーエンド暗号化(E2EE) を必須とするため、これがとても重要な役割を果たします。


🔐 なぜこのJSONが必須?

このJSONは 「お互いが接続の条件を交渉する」ためのデータです。 通信できるかどうかは、以下が一致しないと成立しません:

  • IP/ポートが互いに見えるか
  • UDPが許可されているか
  • 対応プロトコル・暗号方式が合っているか
  • fingerprintが改ざんされていないか

つまりこれは「事前にすり合わせる設定ファイル」のようなもので、 このデータが無いとP2P通信は絶対に成立しません


✨ シンプルなまとめ

要素 意味
type offer か answer のどちらか
sdp 接続条件のすべてが詰まった長いテキスト
candidate IP + Port 候補(複数)
fingerprint 暗号通信のための証明鍵のハッシュ
setup どちらが送信開始側になるか(actpassなど)

🔐 セキュアで効率的な通信の裏には、これほど緻密な交渉がある。 それを抽象化してくれているのが simple-peer であり、WebRTCライブラリの役目です。

🤔 initiator 問題とその解決案

🔧 initiator とは?

simple-peer の初期化時に以下のように指定します:

const peer = new SimplePeer({ initiator: true, trickle: false });

この initiator フラグは、

  • 最初に offer を送る側か?
  • 相手の応答(answer)を待つ側か?

を決定する、P2P接続における重要なフラグです。


🚫 双方が initiator: true だとどうなる?

エラーになります。

どちらも「自分が先に送る!」という状態になってしまい、 お互いに offer を送り合うだけで、接続は確立しません。

これが 手動接続で最も起きやすいミスの1つです。


🔁 正しいペア構成は?

A側 B側 結果
initiator: true initiator: false ✅ 成功
initiator: false initiator: true ✅ 成功
initiator: true initiator: true ❌ 失敗
initiator: false initiator: false ❌ 失敗(両者待機)

つまり、「片方だけが initiator」でなければ接続はできません。


💥 手動接続の限界と不便さ

現在の手動方式だと、こんな問題があります:

  • 「自分が initiator でいいのか?」が分からない
  • 双方が同時に「接続したい!」と思うと失敗する
  • P2Pであるはずが、結局チャットで相談して決めている(非自動)

このままでは、匿名通信・非同期通信には向きません。


✅ 現実的な解決策(構想)

🅰️ 解決案 1:接続役(host側)を最初に立ち上げる

  • Aさんが initiator: true でページを開く(ホスト)
  • Bさんがあとから initiator: false で接続する(ゲスト)

🔸 メリット:最もシンプルで、今すぐ導入できる 🔸 デメリット:あらかじめ「役割分担」が必要


🅱️ 解決案 2:ボタンで initiator 切り替え

UI側で、「ホストになる」「接続する」などのボタンを用意し、 それに応じて initiator の値を変更する。

// ボタンAを押したら
const peer = new SimplePeer({ initiator: true });

// ボタンBを押したら
const peer = new SimplePeer({ initiator: false });

🔸 メリット:明示的で分かりやすい 🔸 デメリット:ユーザーに選ばせる手間が残る


🆑 解決案 3:自動仲介サーバの導入(将来的に)

✅ 概要:

  • 小さな PHP / Node.js サーバを立てて
  • 一時的に signal 情報を共有する
  • 最初にアクセスした側を initiator: true にし、もう片方は false に自動設定

🔄 流れ例:

  1. Aがアクセス → initiator: true で signal をサーバに POST
  2. Bがアクセス → サーバから Aの signal を取得 → initiator: false
  3. Bが返答を送信し、Aが受け取る → 接続確立!

🔸 メリット:匿名&非同期でも接続可能 🔸 デメリット:軽量とはいえ「サーバ」が必要になる


🧭 まとめ

方法 自動性 匿名性 導入の難易度
① 事前役割分担 ✖️ ✖️ ★☆☆(簡単)
② ボタンで選択 ✖️ ★★☆
③ サーバで自動仲介 ★★★(中)

今は①や②で試しつつ、将来的に③を目指すのが現実的です。

🌐 発展的な展望

WebRTC と simple-peer を使った P2P 通信は、 「チャット」や「テキスト送信」にとどまらず、さらに大きな可能性を秘めています。

ここでは、その未来の活用例と展望を紹介します。


📁 1. ファイル・アセットのP2P送信

P2P通信が確立した後は、peer.send() を使って以下のような データ送信が可能です:

  • JSONデータ(設定・メタ情報)
  • Blob(画像や音声ファイル)
  • ZIP形式にまとめたアセット
const file = new Blob([zipData], { type: "application/zip" });
peer.send(file);

これにより、たとえば…

  • 自作キャラクターアセットを他プレイヤーに配布
  • 音声/画像パックを配布して、即座に再現
  • コラボレーション相手とアセットを交換

といった活用が可能になります。


🗄️ 2. IndexedDBとの統合:オフライン×P2P

受信したZIPアセットやJSONを、そのまま IndexedDB に保存すれば、

  • ローカルアセットとして永続化
  • 次回以降の再ロードなし
  • オフライン時にも動作可能

など、分散型オフライン体験を実現できます。

受信 → 解凍 → JSON抽出 → IndexedDB保存、という自動連携も可能です。


🤝 3. 他ユーザーとのコラボレーション

さらに未来的な展望として、P2Pを用いたリアルタイム共同編集や通信も考えられます。

例:

  • 同じ「カードアセット編集画面」を複数ユーザーで開き、

    • 一方が修正すると、他方にも即反映
    • チャットやバージョン通知を交えたインタラクティブな共同作業
  • 送信ボタンで更新をプッシュし、相手側が反映する

  • お互いの更新を diff ベースで同期する

このようなアプローチは、Google Docs やFigmaに代表される「リアルタイム共同編集」と近く、 中央サーバなしでも一部の体験が可能になります。


🚪 4. 最終目標:サーバレス分散アプリの構想

P2P + IndexedDB + ブラウザUI のみで…

  • アセット管理
  • 履歴保存
  • 他ユーザーとの共有

が可能になると、Webブラウザだけで完結する「分散型アプリ(DApp)」 が完成します。

しかもそれは、

  • 匿名で接続可能
  • 中央サーバなしでも通信できる
  • ローカルファースト&セキュア

という、極めて自由で開かれた世界です。


🎯 今後のステップ

  • ✅ P2P接続&テキスト送信の基礎は完了
  • 🔜 次は Blob や ZIP、JSON の送信
  • 🧩 IndexedDBとの統合による自動保存
  • 🌐 Signal自動化による真の匿名・非同期化
  • 🤝 他ユーザーとのリアルタイム連携

🎉 おわりに

P2P通信は「難しそう」と思われがちですが、 一歩一歩進めば、ここまで自由な表現と共有が可能になります。

simple-peerを起点に、あなたのWebアプリも次世代の分散ツールに育つかもしれません。

あとがき

P2Pは難解だと思っていましたが、意外と簡単には出来ました。
WebRTCの複雑な処理を隠蔽してくれる便利なラッパーライブラリのお陰ではありますが…。

今後の課題として、シグナリングサーバーの実装と、それを踏まえたP2Pネットワークの構築で、それが完成すると希望していたアセット共有ネットワークができると思います。

その為の準備段階の一つとして本日は、ZIPファイルをオンライン上で生成するJavaScriptアプリを作成しました。

試験的に以下で公開してますが、あくまで、ゲーム用のZIPファイル作成に特化した物なので、普通にZIPファイルをドロップしても動かないようになっています。

使われるかどうかも分からないゲームにここまで心血を注ぐのは明らかにやり過ぎだと思いますが、作りたい願望がまさってしまって開発が辞められないですね…。

このゲーム制作で培った色々なノウハウは間違いなく今後の開発に生きると思います。

関連リンク