コラム: ブロードキャスト・パターン
mini_serv の中心には「全クライアントに送る (送信元を除く)」という非常にシンプルな転送ルールがあります。これを N-1 ブロードキャスト と呼んだりします。一見素朴ですが、ID 採番や Pub/Sub への発展まで含めると意外に奥が深い。
1. mini_serv のブロードキャスト
シンプルに 0..max_fd を走査して、listen_fd と送信元を除いた fd に send するだけ:
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\n | 5 (新規本人) |
| 退室 | server: client 5 just left\n | 5 (退室者) |
| メッセージ | client 5: hello\n | 5 (送信者) |
ロジックの単純さが、コードの正しさの担保になっています。
2. ID 採番ポリシー
ID のルールはシンプルです: 「これまで配った最大の ID + 1」を新しい人に渡し、退室した番号は二度と使い回さない。
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 等の一意 ID | 7c3f-…-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)
← 配送される実装イメージ:
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-once | 1 回送って終わり | UDP, mini_serv |
| At-least-once | ACK 付き、来なければ再送 | TCP の ACK は OS レベル。アプリ層 ACK は別物 |
| Exactly-once | At-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 で十分。