コラム: 行区切りテキストプロトコルの設計
mini_db のコマンドは POST A B\n のような 改行区切りのテキスト です。一見素朴ですが、これは Redis / SMTP / HTTP / IRC など、現役のプロトコルが採用してきた由緒正しい設計です。
このコラムでは「なぜ行区切りなのか」「どこまで通用するのか」「破綻するケース」を整理します。
1. 行区切りテキストの 3 つの長所
人間がデバッグできる : nc localhost 4343 で繋いで POST A B と打てば動く。バイナリ・プロトコルだと xxd 必須。
境界が単純 : 区切り文字が \n 1 文字。受信バッファに \n が現れたかどうかだけ見れば良い。
実装が薄い : 長さプレフィックス方式と違い、長さフィールドの読み取り → バッファリングという 2 段階を必要としない。std::string::find('\n') で 1 行。
2. 似た設計の現役プロトコル
| プロトコル | 区切り | コマンド例 | 応答例 |
|---|---|---|---|
| mini_db | \n | POST A B | 0 |
| Redis (Inline) | \r\n | SET A B | +OK |
| SMTP | \r\n | MAIL FROM:<a@b.com> | 250 OK |
| HTTP/1.1 (header) | \r\n | GET /index.html HTTP/1.1 | 200 OK |
| IRC | \r\n | PRIVMSG #ch :hello | :srv 332 … |
mini_db がほぼ唯一 \n のみで \r\n ではないのは、本実装が単純化のために採用した選択です。実プロダクトでは CRLF が事実上の標準。
3. 破綻するケース 1: 区切り文字を含めたい
たとえば POST greeting "hello world\n" のように、値の中に改行を含めたい とします。素直なプロトコルではここで詰みます。
逃げ道は 3 つ:
エスケープ : \n を \\n のように書き換えて送る。デコード時に逆変換。実装は地味に面倒で、テストも増える。
引用符 : POST greeting "hello\nworld" のように引用符で囲む。パーサがクォート状態を持つ必要があり、プロトコル全体の状態機械が複雑化する。
長さプレフィックス (length-prefixed) : 「次に来るバイト数」を先に送る。以下の Redis RESP がこの方式 (Bulk String):
*3\r\n ← 3 要素の配列
$4\r\nPOST\r\n ← 4 バイトの "POST"
$8\r\ngreeting\r\n
$11\r\nhello\nworld\r\n ← 11 バイト固定。中身に \n が入っていてもOKmini_db を発展させるなら length-prefixed が最も筋が良く、バイナリ値も自然に扱える という利点が出てきます。
4. 破綻するケース 2: 行が長すぎる
行が無限に届かない場合、buffers_[fd] が無制限に膨らみます。悪意あるクライアントが 'A' を \n 無しで送り続ける だけで、サーバの RAM を食い潰せる (DoS)。
mini_db のスコープでは「リクエストはせいぜい 1000 文字以内」という前提があるので無視していますが、本番サーバなら:
- バッファサイズに上限を設け、超えたら接続を切る
- アイドル時間 (
SO_RCVTIMEO/selectの timeout) で切断する
ぐらいは入れます。
5. 「テキスト vs バイナリ」をどう選ぶか
| 観点 | テキスト | バイナリ |
|---|---|---|
| 人間のデバッグ | ◎ telnet/nc でそのまま打てる | △ xxd か専用クライアントが必要 |
| 効率 | △ 数値を "12345" (5 byte) で送る | ◎ int32_t (4 byte) で済む |
| パース難度 | △ トークン分割と整数変換が要る | ◎ memcpy で構造体に詰めるだけ |
| 拡張性 | ○ 追加コマンドは新しい行を追加するだけ | ○ バージョンビットを使えば後方互換しやすい |
**「目で読める」**を捨てない限りはテキスト、性能要件が支配的になったらバイナリ — というのが大まかな目安です。Redis が両方サポートしている (Inline + RESP) のはこのトレードオフの妥協点です。
まとめ
- 行区切りテキストは「人が読める / 実装が薄い」が長所。SMTP・HTTP・Redis が証明している。
- 値に区切り文字を入れたくなった瞬間に破綻する。逃げ道は エスケープ / 引用 / 長さプレフィックス。
- 長さプレフィックス (Redis RESP 風) は技術的にいちばん筋が良いが、デバッグ性は下がる。
- スコープが広がるほど DoS 対策・サイズ上限・タイムアウト が必須になる。