Skip to content

コラム: ブロードキャスト・パターン

mini_serv の中心には「全クライアントに送る (送信元を除く)」という非常にシンプルな転送ルールがあります。これを N-1 ブロードキャスト と呼んだりします。一見素朴ですが、ID 採番や Pub/Sub への発展まで含めると意外に奥が深い。

1. mini_serv のブロードキャスト

シンプルに 0..max_fd を走査して、listen_fd と送信元を除いた fd に send するだけ:

c
static void send_all(int except) {
    int len = strlen(g_buf_write);
    for (int fd = 0; fd <= g_maxfd; fd++) {
        if (fd == g_listen_fd || fd == except) continue;
        if (FD_ISSET(fd, &g_writefds))
            send(fd, g_buf_write, len, 0);
    }
}

3 種類のイベントすべてが、この 1 関数を再利用しています:

イベント整形例除外対象
入室server: client 5 just arrived\n5 (新規本人)
退室server: client 5 just left\n5 (退室者)
メッセージclient 5: hello\n5 (送信者)

ロジックの単純さが、コードの正しさの担保になっています。

2. ID 採番ポリシー

ID のルールはシンプルです: 「これまで配った最大の ID + 1」を新しい人に渡し、退室した番号は二度と使い回さない

c
g_ids[cfd] = g_next_id++;

実装は g_next_id++ 1 行で終わりですが、「再利用する/しない」は実は思想の選択で、後から効いてきます。再利用すると、さっきまで愛想よく喋っていた client 3 と、今しゃべっている client 3 が別人になり、ログを読み返した人が静かに発狂します。

方針利点欠点
採番のみ (mini_serv)0, 1, 2, 3 → 退室 → 4ログ追跡が容易、衝突しないID が単調増加で永遠に大きくなる
再利用0, 1, 2, 3 → 1 退室 → 1 を新人に再割当ID 番号がコンパクト「client 1」が誰なのかログだけでは区別不能
UUID 等の一意 ID7c3f-…-9b1e完全一意、グローバル分散可能文字列が長い、表示が冗長

mini_serv は 「採番のみ」 を選択。チャットログを後から読み返すとき、「client 1」が時間軸で常に同一人物を指すのは、デバッグ・観察の上で大きな利点です。

3. fan-out のスケーリング

「送信元を除く全員に送る」は概念上 O(N) の操作。これを言い換えると:

  • N=10 → 各メッセージで 9 send
  • N=100 → 99 send
  • N=1000 → 999 send
  • N=10000 → 9999 send / メッセージ

ある瞬間に M 人が同時に発言したら、合計 O(M × N) の send。N が増えるほど CPU と帯域がガンガン食われ、メッセージが多いと選別ロジックそのものがボトルネックになる 段階に到達します。

実プロダクトの解決策:

トピック化 (Pub/Sub) : 全員に送るのではなく、「発言者がいる部屋」だけに送る。Slack / Discord / IRC のチャネル機構。

専用ルータ : クライアント数が増えたら、サーバ間で fan-out を分散させる。Redis Pub/Sub、NATS、Kafka などのメッセージブローカ。

UDP マルチキャスト : ローカルネットなら、TCP の N-1 の代わりに 1 パケットで済ませられる。ただし配送保証なし、インターネット越しは使えない。

4. Pub/Sub への発展

mini_serv に「SUBSCRIBE <topic> / PUBLISH <topic> <msg> 」を足すと、たちまち 小さな Pub/Sub システム になります:

                   ┌── client 0 (subscribe: weather)
client A ─PUB──▶ ─┤
   weather: rain   ├── client 1 (subscribe: news)        ← 配送されない
                   └── client 2 (subscribe: weather)
                                                          ← 配送される

実装イメージ:

c
struct subscription { int fd; char *topic; };
/* subscribe コマンドで配列に追加、broadcast 時は topic 一致のみに send */

これだけで、「誰がどの話題に興味あるか」を表現する小さな ESB (Enterprise Service Bus) が出来上がります。Redis Pub/Sub、MQTT、NATS、Kafka はみな、この発想を巨大化したものです。

5. 配送保証の議論

mini_serv はベストエフォートです:

  • lazy client は writefds で弾かれ、メッセージをロストする
  • ネットワーク経路で TCP RST が来れば、その瞬間以降は届かない

これを強化したい場合:

段階仕組み
At-most-once1 回送って終わりUDP, mini_serv
At-least-onceACK 付き、来なければ再送TCP の ACK は OS レベル。アプリ層 ACK は別物
Exactly-onceAt-least-once + 重複排除Kafka transactional, RabbitMQ 確認応答

「全員に確実に届けたい」要求は意外と高価です。チャットサーバならベストエフォートで十分なことが多く、mini_serv の選択は妥当。

まとめ

  • mini_serv のブロードキャストは N-1 送信の素直な実装。
  • ID は 再利用しない ことで、ログ可読性とデバッグ性を確保。
  • N が大きくなると fan-out コストが線形に増え、Pub/Sub でトピック分割するのが定石。
  • Redis Pub/Sub / MQTT / Kafka はこの発想の巨大化版。
  • 配送保証 (at-most-once / at-least-once / exactly-once) はそれぞれ実装コストが跳ね上がる。チャットなら at-most-once で十分。