【JavaScript 応用講座 #07】[WebRTC/P2P] トークンパッシング入門:バケツリレーで繋ぐ通信フロー

1. はじめに

P2P(ピア・ツー・ピア)通信を使ったWebアプリの実装では、自由度が高い反面、「制御のなさ」が常に課題になります。

特に、WebRTCを用いた**メッシュ型の接続(全ユーザーが相互に接続)**は、理論上は誰とでも通信できる理想的な方式に見えますが、実際に構築・運用してみると次のような問題に直面します:

  • 接続が増えるごとに負荷が急増(ユーザー数 n に対して n(n-1)/2 の接続が必要)
  • モバイル環境やブラウザの制約により、10人程度で破綻する
  • 誰が誰と接続しているかを完全に把握・制御できない
  • データ転送のタイミングがバラバラで、協調処理が難しい

私自身、最大10人で動作するメッシュ接続を構築しましたが、次第にこう感じるようになりました:

「自由すぎて、みんなが勝手に話し始めて収拾がつかない」
「誰に送るか、いつ送るか、誰が終わったか…混乱する」


こうした状況を解決するために注目したのが、トークンパッシング(Token Passing) という通信モデルです。

これは、一人ずつ順番に「発言権」や「送信権限」を持ち回りで渡していく という、シンプルながらも強力な制御方法です。
ちょうど「回覧板」のように、トークンを持つ人が何かを実行し、それを次の人へ渡していくことで、秩序ある通信が実現します。


本記事では、WebRTC上におけるトークンパッシングの仕組みとその実装方法 について、背景・設計・課題・応用例を含めて詳しく解説します。


2. トークンパッシングとは?

トークンパッシング(Token Passing)は、複数の参加者の間で「権利(トークン)」を順番に回していく という通信制御の仕組みです。

この「トークン」は、通信権、送信権、処理権、発言権…など、自由に定義できる「順番の合図」のようなものです。


🪣 イメージ:バケツリレーのような仕組み

たとえば火事の現場でのバケツリレー。 一人ひとりが自分の役目(バケツを渡す)を終えてから、次の人へとバケツを手渡します。

同じように、トークンパッシングでは 「今はあなたの番です」→「終わったら隣に渡してください」 というルールで、1人ずつ順番に「操作権」が渡されていきます。

[ユーザーA] → [ユーザーB] → [ユーザーC] → ...
    🔁              🔁              🔁
 (トークン)   (トークン)   (トークン)

この方式は非常にシンプルでありながら、**「同時にみんなが処理を始めて衝突する」**といった問題を防ぐことができます。


⚙️ 仕組み:トークンの受け渡し

トークンパッシングの基本ルールは以下の通りです:

  1. 最初の1人がトークン(=処理権限)を持つ
  2. トークンを持っている人だけが、あるアクションを実行できる(例:送信)
  3. 終わったら、次の人にトークンを渡す
  4. 最後の人が終わったら、また最初に戻る(リング構造)

これを ローカルで実装する場合は「接続順リスト」
P2Pで実装する場合は「userId順、nickname順などで定義された並び」 を使って制御します。


🛠 実用例:こんな場面で役に立つ

💬 1. チャットの順番制御(例:発言は1人ずつ)

リアルタイムな議論や会議で「一人ずつしか発言できない」制御が必要な場面で使えます。

📦 2. ZIPアセット送信の順番制御

P2P環境で一度に1人にしか送信できないような仕組みをトークンで制御することで、ファイルの衝突や混線を回避できます。

🎮 3. ターン制ゲームの実装

ボードゲームやターン制RPGなど、プレイヤーが順番に行動する 場面で、トークンは「行動権」として機能します。
不正行動や多重送信を防ぐためにも有効です。


このように、「順番に何かをさせたい」すべての状況で、トークンパッシングはシンプルかつ堅実な解決策 となります。

3. 実装設計(WebRTC上で使う意味)

トークンパッシングは、特にP2P(ピア・ツー・ピア)通信のような中央サーバの存在しない環境で、分散的に順序や制御を維持するための技法として極めて有効です。

本章では、WebRTCを用いたP2Pネットワーク上で、なぜトークンパッシングが「意味ある構造」になるのかを整理します。


🌐 全員と接続しているわけではない

WebRTCで構成されたP2Pネットワークは、全ユーザーが全員と直接繋がっているわけではありません

たとえば以下のような「中継的」な接続になっている場合:

[A] ↔ [B] ↔ [C] ↔ [D]

このような構成では、トークンを**中継してリレー形式で回す(順番に渡す)**方式が自然です。

もし全員が全員と接続していた場合でも、順番制御がなければ同時多発的にアクションが発生し、衝突や破損が起こりやすくなります


🤝 サーバ制御ではなく「分散協調」

従来のサーバ制御型では、サーバ側でユーザーの順番や制御ロジックを管理することができます。

しかし、P2P環境では「中央の判断役」が存在しません。 そのため、各ノードが協調的に動作するロジックが必要になります。

ここでトークンパッシングは、「いま誰の番か?」を全員が共有し、明示的なルールで制御を行うための最小単位になります。


🧭 「順番制御」で管理的な役割を導入する

P2Pネットワークでは、本来「誰が管理者か?」という概念はありません。 それゆえ、無秩序なアクションが同時に発生してしまうことも珍しくありません。

このような場面で、トークンを持っている人が管理的な役割を一時的に担うことで、 以下のような秩序を持ち込むことができます:

  • ファイル送信の調整
  • チャットやアセット同期の指揮
  • ターン管理(ゲーム進行など)

つまり、トークンの存在は、一時的な「主導権」の象徴であり、 「次に誰が何をするか」を全員が共有する仕組みです。


⚠️ トークンの信頼性と例外対処

ただし、トークン制御には以下の注意点もあります:

  • 誰かがトークンを持ったまま落ちたら? → タイムアウトでスキップなどの対策が必要
  • 同時に2人がトークンを持ったら? → 「1周に1トークン」というルール厳守+再同期の仕組みが重要
  • 新しく接続した人はどこに入る? → リストの末尾に追加する等のルール設定

これらはシステム設計側の責任として、堅牢な実装が求められます。


次の章では、実際にWebRTC環境下で「どのようにトークンを回すのか」という**技術的な設計(データ構造・処理の流れ)**について解説していきます。

4. 実装に必要な要素(データ構造と回し方)

トークンパッシングの実装には、現在のトークン保持者が誰かを常に追跡し、適切なタイミングで次のユーザーに渡すという基本ロジックが必要です。

そのために、最低限以下の3つの要素を正しく構成・同期させる必要があります。


✅ ① ユーザーリスト(userList

  • server.js から定期的に送信される user-list メッセージは、すべてのユーザーが共通の順序を持つための信頼ソースです。
  • 通常、userId でソートされた配列とし、回覧順はこの配列のインデックスに従って進みます。
const userList = [
  { userId: "userA", nickname: "Aさん" },
  { userId: "userB", nickname: "Bさん" },
  { userId: "userC", nickname: "Cさん" },
];
  • 新規接続や切断によって更新されるため、最新のリストに常に追従 する必要があります。

✅ ② 自分の userIdmyUserId

  • 自分がどの順番にいるのか判定するため、自身のユーザーID をローカル変数に保持します。
const myUserId = P2P_USER_INFO.userId;
  • userList.findIndex(u => u.userId === myUserId) によって、トークンパスの順番を判断可能です。

✅ ③ 現在のトークン保持者(tokenHolder

  • 現在トークンを保持しているユーザーの userId を示す変数です。
  • 初期状態では tokenHolder = userList[0].userId など、先頭ユーザーに割り当てるとシンプル。
let tokenHolder = userList[0]?.userId;
  • 各ユーザーが tokenHolder を監視し、自分の番になったら権利を得て処理を開始します。

🔁 トークンを「回す」流れ

トークンは以下のような流れで順番に次の人へ回されます。

function passToken() {
  const index = userList.findIndex(u => u.userId === tokenHolder);
  const nextIndex = (index + 1) % userList.length;
  tokenHolder = userList[nextIndex].userId;

  // P2Pまたはシグナリング経由で通知
  sendTokenTo(tokenHolder);
}
  • 全員がこの順序ロジックを共有していれば、誰がトークンを持っているかは単一の事実として一致します。
  • 通常はトークン受け取り者が、一定時間後に passToken() を実行することでリレーされます。

🧠 自分の番か判定する

if (tokenHolder === myUserId) {
  // 自分の番!何か処理する
}

📡 通知の手段

  • WebSocket 経由で全体に token-pass イベントをブロードキャストしても良いですし、
  • 最小通信で済ませたいなら nextUserId だけに token-pass メッセージを送る方式も可能です。
{
  type: "token-pass",
  from: "userB",
  to: "userC"
}

5. トークンが届いたときにどう振る舞うか/自分の番が来たらどうするか

トークンパッシングでは、自分がトークンを受け取ったときの処理をどれだけシンプルかつ明確に書けるかが鍵です。 ここでは、トークン受信イベントの処理と、自分の番で行うタスクの実装例を紹介します。


✅ トークン受信イベントの処理(token-pass)

サーバまたはP2Pで「トークンを受け取ったよ」と通知されるのが基本です。 以下のようなシンプルな受信処理が典型です:

signalingSocket.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.type === "token-pass" && msg.to === myUserId) {
    console.log(`🎯 トークンを受信しました!from: ${msg.from}`);

    // トークンを保持中としてマーク
    tokenHolder = myUserId;

    // トークン処理開始
    onTokenReceived();
  }
};
  • msg.to で対象が自分かどうかを確認
  • 自分が受け取ったら onTokenReceived() を呼び出す

✅ トークン保持中に行う処理

トークンを保持中は、自分の担当タスクを行ってから次の人にトークンを渡す必要があります。

function onTokenReceived() {
  console.log("🟢 自分の番が来ました!");

  // 例:ZIPファイル送信処理
  sendZipFileToEveryone().then(() => {
    console.log("✅ ZIP送信完了。トークンを渡します。");
    passTokenToNext();
  });
}
  • 非同期タスクを含めても問題ありません(完了後に passTokenToNext()
  • sendZipFileToEveryone() は仮想的な処理関数

✅ 次のユーザーへトークンを渡す

次のユーザーを順番で決定し、トークンを送信します。

function passTokenToNext() {
  const currentIndex = userList.findIndex(u => u.userId === myUserId);
  const nextIndex = (currentIndex + 1) % userList.length;
  const nextUser = userList[nextIndex];

  signalingSocket.send(JSON.stringify({
    type: "token-pass",
    from: myUserId,
    to: nextUser.userId
  }));

  tokenHolder = null; // 自分の番は終了
}
  • ユーザーリストの順番に沿って回していく
  • 最後の人まで届いたら最初に戻る(円環構造)

✅ タイムアウト対策(optional)

もし onTokenReceived() 内で処理が滞る場合は、タイマーで次に回すことも検討できます。

setTimeout(() => {
  if (tokenHolder === myUserId) {
    console.warn("⏱ タイムアウトによりトークンを強制パス");
    passTokenToNext();
  }
}, 10000); // 10秒待って反応なければパス

🧩 自分の番でできる処理の例

処理内容 説明
ZIPファイル送信 大容量転送を分散実行し、衝突を回避できる
チャット送信許可 同時発言を避け、ターン制チャットとして運用可
ゲームの手番 ボードゲームの順番制御にも応用可能
処理完了のログ送信 デバッグログや進捗報告にも利用できる

6. 切断・再接続時のリカバリ(トークン消失時の再生成)

トークンパッシングは分散型の協調制御であるがゆえに、**一部のノードが切断されるとトークンの所在が不明になる=“トークン消失”**という重大な問題が発生します。 このセクションでは、トークン消失の検知と再生成について考察します。


✅ どんなときにトークンが「消失」するか?

状況 起きるタイミング
トークン保持者が切断 自分の番の最中に切断 tokenHolder === "userX" だったが userX が離脱
トークン転送時に失敗 token-pass が届かない 送信側が落ちた/受信側がオフライン
通信の遅延や分断 タイムアウト発生 トークンが渡ったかどうか不明な状態

✅ リカバリのためのアプローチ

🔁 ① トークンパスに確認応答(ACK)をつける

// トークン送信側
sendTokenTo(nextUserId);
waitAck(5000).then(() => {
  // OK
}).catch(() => {
  // 失敗したら次の次へ
  skipAndPassToken();
});
  • ACKが返らなければ「トークンが落ちた」とみなしてリカバリを開始
  • ユーザーが抜けたと判断してスキップ

🔄 ② タイマーによる監視

setTimeout(() => {
  if (tokenHolder === myUserId && !tokenPassed) {
    console.warn("⚠️ トークン保持が長すぎます。再送または再生成を検討。");
  }
}, 10000);

🔧 ③ サーバで監視・再生成

サーバが全員の user-list を管理している場合、最後の token-pass 時間を記録しておくことでトークンの生存を見張れます:

// サーバ側での例
let lastTokenPassTime = Date.now();

setInterval(() => {
  if (Date.now() - lastTokenPassTime > 15000) {
    console.log("⚠️ トークン消失と判断。最初のユーザーに再生成!");
    const firstUser = userList[0];
    ws.send(JSON.stringify({ type: "token-pass", to: firstUser.userId }));
    lastTokenPassTime = Date.now();
  }
}, 5000);

✅ 再接続ユーザーはトークンを「持っていない」状態から復帰

  • 接続直後、トークン保持者の userId を受信し、自分が持っているか確認
  • 自分でなければ受け身に徹する(何も行わない)
// 再接続後に tokenHolder をサーバから受信
signalingSocket.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === "token-info") {
    tokenHolder = msg.userId;
  }
};

✅ トークンが「複数存在」してしまうリスクと対策

トークンが行方不明になった際、複数ユーザーが同時にトークンを再生成してしまうと、**トークンの二重化(ダブルエントリー)**が起きます。これを避けるために:

  • 再生成の責任者を1人に絞る(例:userList[0])
  • 再生成ログをサーバに記録し、全員に通知
// 再生成する権利があるのは userList[0] のみ
if (myUserId === userList[0].userId) {
  signalingSocket.send(JSON.stringify({
    type: "token-pass",
    to: nextUserId,
    from: myUserId,
    reason: "token-timeout"
  }));
}

🔚 安定運用のために

安定化の鍵 説明
定期的な ping/pong 生存確認で切断を即時検知
ACK & タイムアウト監視 トークンが落ちたことを早期に検知
サーバでの保険 最悪のケースではサーバがトークン復元
トークン二重生成の防止 誰が再生成できるかを明示しておく

7. デモ用コード・擬似実装例

ここでは、トークンパッシングの最小構成をJavaScriptでシミュレーションします。 実際のP2P通信ではなく、ブラウザ上で複数のクライアントがつながっている前提の擬似コードです。


✅ 前提データ

// ユーザーリスト(順番は信頼できると仮定)
const userList = ["alice", "bob", "charlie"];

// 自分のID
const myUserId = "bob";

// 現在のトークン保持者(初期は最初のユーザー)
let tokenHolder = userList[0];

✅ トークン受信処理(自分が受け取ったとき)

function onReceiveToken(fromUserId) {
  console.log(`🎫 ${myUserId} がトークンを受け取った(from ${fromUserId})`);

  // 自分の番の処理(例:チャット送信、ZIP送信、ターン処理など)
  performMyAction();

  // 次の人へトークンを渡す(2秒後)
  setTimeout(() => {
    const nextUserId = getNextUserId(myUserId);
    passTokenTo(nextUserId);
  }, 2000);
}

✅ 自分の処理(performMyAction)

function performMyAction() {
  console.log(`🟢 ${myUserId} のアクション中...`);
  // ここでチャット送信やZIP送信などを行う
}

✅ トークンを渡す処理

function passTokenTo(targetUserId) {
  console.log(`➡️ ${myUserId} から ${targetUserId} へトークンを送信`);
  // 擬似的に他のユーザーに「トークンを渡した」ことにする
  simulateNetworkSend(targetUserId);
}

✅ ユーティリティ関数(順番決定)

function getNextUserId(currentId) {
  const idx = userList.indexOf(currentId);
  return userList[(idx + 1) % userList.length];
}

✅ ネットワーク送信のシミュレーション

function simulateNetworkSend(toUserId) {
  // ここでは、他ユーザーが onReceiveToken を呼ぶと仮定
  if (toUserId === myUserId) {
    // 自分宛てにトークンが戻ってきたら受け取る
    onReceiveToken("someone");
  } else {
    // 他の誰かが受け取る(シミュレーションではログだけ)
    console.log(`📩 ${toUserId} がトークンを受け取る想定`);
  }

  tokenHolder = toUserId;
}

✅ 起動(最初のトークンを受け取ったとき)

// 自分が最初のトークン保持者ならスタート
if (tokenHolder === myUserId) {
  onReceiveToken("system");
}

🔄 実環境への応用のヒント

  • simulateNetworkSend() の代わりに WebRTCのDataChannel を使ってトークンを送る
  • onReceiveToken() を受信イベントとして待機
  • tokenHolder を同期する仕組み(共有ステート)を確保
  • サーバから全体の userList を受け取る仕組み更新通知が必要

8. 応用アイデア(ゲームやチャットへの展開)

トークンパッシングは、集中管理せずに順番や権利を公平に回す必要がある場面で非常に有効です。 P2Pベースのアプリケーションにおいて、以下のような形で活用できます。


🎮 ゲーム:ターン制バトル、カードゲームなど

✅ 用途:

  • 自分のターンが来た人だけがカードを出せる
  • トークンを保持しているプレイヤーのみが操作可能

✅ メリット:

  • 中央サーバなしでも整合性ある順番制御
  • 複数プレイヤー間でスムーズな交代制処理

✅ 例:

if (myTurn()) {
  drawCard();
  playCard();
  endTurn(); // 次の人にトークンを渡す
}

💬 チャット:順番制限付きチャット

✅ 用途:

  • ひとりずつ話す「会議モードチャット」
  • 教育用途で「順番に発言する練習」などにも活用

✅ 特徴:

  • トークン保持者だけが send ボタンを押せる
  • 非保持者はメッセージを送れない、またはキューに入る

📦 アセット送信:ZIPファイルの転送順を制御

✅ 問題:

  • 同時に送るとネットワークが混雑しやすい
  • 特にP2P通信では帯域の衝突が起きやすい

✅ 解決:

  • トークンを持っている人だけが sendZip(zipBlob) を実行
  • 送信完了後、トークンを次の人へ渡す

🧪 その他の応用シーン

シーン 応用ポイント
P2Pベースの投票システム 投票の受付順を制御し、不正防止
分散処理のキュー制御 同時アクセスを避けるため順番に処理を配分
IoT制御ネットワーク 各ノードが順番にセンサーデータを送信

🚫 注意点と対策

課題 解決策
接続断でトークンが消失 自動再生成 or バックアップ保有者による再配布
順番にズレが出る user-list定期的に同期して整合性維持
P2P間通信の失敗 WebSocketサーバ経由で再送信フォールバックを実装

トークンパッシングは「軽量な分散協調の実現手段」として、多くの可能性を秘めています。

9. まとめ:トークンパッシングが拓くP2Pの未来

トークンパッシングは、 シンプルでありながら強力な順番制御の仕組みです。

P2P通信という分散型のネットワークにおいては、 中央サーバが存在しないからこそ、 「誰が今行動してよいのか」という順序と権利の管理が重要になります。


🔑 本記事のポイント振り返り

セクション 内容
1. はじめに メッシュ構造の限界と、順番制御の必要性
2. トークンパッシングとは? バケツリレーのような制御方法の概要
3. 実装設計 サーバレスで協調制御する意義
4. 必要な要素 user-listと現在の保持者などの情報構造
5. トークンを受け取ったときの挙動 どのように処理し、どう渡すか
6. 切断・再接続時の扱い トークンの消失や再配布対策
7. 擬似コード 実装イメージと基本的な流れ
8. 応用例 ゲーム・チャット・アセット送信への展開

🌍 今後の展望

  • WebRTCと組み合わせることで、自律的な分散協調ネットワークの構築が可能
  • 低リソース・低信頼環境でも使える、堅牢なフロー制御
  • Webブラウザ上のP2Pアプリケーション開発における新たな選択肢となる

🎯 最後に

複雑な合意形成や同期処理を必要とせず、 **「一周回すだけ」**という素朴なロジックでここまで多様な活用ができるのは、 トークンパッシングの最大の魅力です。