|
試験ノート - 実装ドリル 1.0
|
select(2) を用いた単一プロセス TCP サーバの実装\n) 単位のメッセージ境界処理 (per-fd 受信バッファ)std::map / std::string / 例外なし設計でシンプルに保つ1ポート=1ファイルで動作するインメモリ Key-Value ストアサーバ。
127.0.0.1 上の指定ポートで TCP 接続を待ち受ける。db_ をテキスト形式 (<key> <value>\n) で path_ に保存する。構造視点での全体像。実行時の流れ (時間軸) は下の「全体シーケンス」「処理フロー」を参照。
| 分類 | 要素 | 役割 |
|---|---|---|
| 公開 API | MiniDb(port,path) / setup() / run() / save() | エントリポイント (main) が呼ぶ 4 メソッド |
| 内部状態 (private 変数) | port_ / path_ / listen_fd_ / max_fd_ / active_fds_ / buffers_ / db_ | サーバ状態 + KV 本体 |
| 内部実装 (private メソッド) | load / accept_client / recv_data / disconnect / handle_command | run() の中で使う実装部品 |
| コピー禁止 (private 宣言のみ) | MiniDb(const MiniDb&) / operator=(const MiniDb&) | 実体を持たず、誤コピーをコンパイル時に弾く |
| 無名 namespace (TU 限定) | g_running / on_sigint | シグナル ↔ メインループ間のフラグとハンドラ |
| 自由関数 | fatal() | システムコール失敗時の即終了 (exit(1)) |
g_running / on_sigint を mini_db.cpp の翻訳単位内に閉じ込め、外部リンケージを与えないため (static と等価の C++ 慣用)。他 TU から触れないことが SIGINT 関連の責務局所化に効く。volatile sig_atomic_t?: シグナルハンドラとメインループ間で共有する変数はこの型でなければ、読み書きが未定義動作になり得る。listen_fd_ やクライアント fd の所有が一意でないと close(2) が二重呼び出しになるため、コピー / 代入を private 宣言のみ (実体なし) で禁止する C++98 慣用。fatal() は自由関数?: MiniDb のメンバではなくファイルスコープの自由関数にしている。MiniDb のメンバにすると、コンストラクタ内 (= インスタンスが完全には未構築) の失敗時に呼びにくい上、main から MiniDb を介さず呼べる方が誤用が少ない。save() だけが SIGINT 経路で呼ばれる?: save() は std::ofstream 等の非 async-signal-safe な API を使う。シグナルハンドラはフラグを倒すだけにし、保存は通常コンテキスト (run() の脱出後) で行う。
| int main | ( | int | argc, |
| char ** | argv | ||
| ) |
プログラムのエントリポイント
argc != 3 なら stderr に "Wrong number of arguments" を出して exit(1)。argv[1] を std::atoi でポート番号にし、範囲外 (≤0 / >65535) なら 同じくエラー終了。SIGINT を on_sigint に紐付け (= g_running を 0 にするだけ)。SIGPIPE を SIG_IGN にし、切断済み fd への send でプロセスが 落ちないようにする。MiniDb server(port, argv[2]) を生成し、setup() → run() の順に呼ぶ。run() は SIGINT 受信時に save() してから戻るため、return 0 で正常終了する。| [in] | argc | 引数数 (3 を期待) |
| [in] | argv | argv[1] = port, argv[2] = persistence file path |
mini_db.cpp の 49 行目に定義があります。
| void anonymous_namespace{mini_db.cpp}::on_sigint | ( | int | ) |
SIGINT ハンドラ。g_running を 0 にするだけ。
g_running = 0; だけを行う。printf / std::cerr / malloc などを 呼ばない (非 async-signal-safe)。永続化 (save()) は通常コンテキストで行う。 mini_db.cpp の 79 行目に定義があります。
| void fatal | ( | void | ) |
Fatal error\n を stderr に書いて exit(1) する
std::cerr に "Fatal error" と std::endl を出す (改行 + flush)。std::exit(1) でプロセスを終了する。socket / bind / listen 失敗など 通常コンテキストでのみ呼ぶ。 mini_db.cpp の 95 行目に定義があります。
| MiniDb::MiniDb | ( | int | port, |
| const std::string & | path | ||
| ) |
ポートと永続化先パスを束縛する
| [in] | port | 1〜65535 の TCP ポート番号 |
| [in] | path | SIGINT 時にダンプするファイル、起動時にロードするファイル |
port_ / path_ を保存。listen_fd_ / max_fd_ を -1 (= 未確保) に置く。FD_ZERO(&active_fds_) でビット集合をクリア。socket も load も呼ばない。setup() で行う。 mini_db.cpp の 114 行目に定義があります。
| MiniDb::~MiniDb | ( | ) |
デストラクタ (全 fd をクローズ)
buffers_ を走査し、登録されている全クライアント fd を close。listen_fd_ が有効 (>= 0) なら close。mini_db.cpp の 129 行目に定義があります。
| void MiniDb::setup | ( | ) |
サーバ起動準備 (DB ロード → ソケット作成 → bind → listen → "ready")
load() を呼んで前回の状態を復元する。socket(AF_INET, SOCK_STREAM, 0) でリスニングソケットを作成。失敗で fatal()。setsockopt(SO_REUSEADDR) を付け、再起動時の "Address already in use" を回避。sockaddr_in を INADDR_LOOPBACK (= 127.0.0.1) と port_ で埋める。bind / listen(128) を行い、失敗で fatal()。active_fds_ に listen_fd_ をセットし、max_fd_ を更新。"ready\n" を出して接続準備完了を通知。INADDR_ANY ではないこと。 mini_db.cpp の 153 行目に定義があります。
|
private |
起動時に path_ を読み、空白区切り 1 行 1 ペアで db_ を初期化。
永続化ファイルから DB を読み戻す
std::ifstream でファイルを開く。失敗 (ファイル無し) なら何もせず返る。ifs >> key >> value で 1 行ずつ取り出し、db_[key] = value で登録する。>> で安全に読める。 mini_db.cpp の 192 行目に定義があります。
| void MiniDb::run | ( | ) |
サーバのメインループ。SIGINT 受信で抜け、save() してから戻る。
g_running が真の間ループする。rfds = active_fds_ でビット集合を複製。select(max_fd_+1, &rfds, NULL, NULL, NULL) でブロック。EINTR (= SIGINT 受信) なら continue し、ループ条件で抜ける。fatal()。0..max_fd_ の範囲で走査し、rv 件分だけ消化:fd == listen_fd_ → accept_client()recv_data(fd)save() を呼んでファイルへダンプし、戻る。save() してはいけない (signal-safe ではない)。 ハンドラはフラグを 0 にするだけにし、保存は通常コンテキスト (ループ脱出後) で行う。 mini_db.cpp の 220 行目に定義があります。
|
private |
accept(2) し、active_fds_ と buffers_ を更新する。
新規クライアントを accept し、状態を登録する
accept(listen_fd_, NULL, NULL) で新規 fd を取得。失敗時は黙って戻る。FD_SET(fd, &active_fds_) で監視対象に追加。fd > max_fd_ なら max_fd_ を更新。buffers_[fd] = "" で per-client 受信バッファを空で初期化。mini_db.cpp の 255 行目に定義があります。
|
private |
recv(2) でデータを取り込み、行ごとに handle_command → send する。
クライアント fd から読み、行ごとに handle_command を呼ぶ
recv(fd, buf, 1024, 0) で 1 チャンクを読む。n == 0 (相手切断) または n < 0 && errno != EINTR (失敗) なら disconnect(fd) を呼んで終わる。EINTR なら静かに戻る (次回 select で再度起こされる)。buffers_[fd] に追記。を見つける限り、その手前を 1 行として切り出し、 handle_command(line)の戻り文字列をsendする。 -# 改行を含まない残りはバッファに残し、次回のrecv` を待つ。| [in] | fd | 受信対象のクライアント fd |
mini_db.cpp の 288 行目に定義があります。
|
private |
fd を close し、active_fds_ / buffers_ から外し、max_fd_ を巻き戻す。
クライアント fd を閉じ、関連状態をクリーンアップする
close(fd) で fd を閉じる。FD_CLR(fd, &active_fds_) で監視対象から外す。buffers_.erase(fd) で受信バッファを破棄。max_fd_ だった場合に備え、active_fds_ に 含まれない fd の範囲を max_fd_ から降順に剥がし、max_fd_ を巻き戻す。max_fd_ が listen_fd_ を下回らないように > でガードする。| [in] | fd | 切断対象のクライアント fd |
mini_db.cpp の 323 行目に定義があります。
|
private |
1行の入力に対して、応答文字列 (0\n / 0 <v>\n / 1\n / 2\n) を返す。
1 行のリクエストを解釈して、応答文字列を返す
| 入力 | 応答 |
|---|---|
POST <key> <value> | "0\n" |
GET <key> (ヒット) | "0 <value>\n" |
GET <key> (ミス) | "1\n" |
DELETE <key> (成功) | "0\n" |
DELETE <key> (ミス) | "1\n" |
| 上記以外 / 空行 | "2\n" |
std::istringstream で行をホワイトスペース区切りトークン列に分解。"2\n"。db_ を更新/参照/削除。"2\n"。tok.size() == 3 (POST) や == 2 (GET/DELETE) で arity を厳密に確認する。| [in] | line | 1 行 (末尾の \n は呼び出し側で除去済み) |
\n を含む) mini_db.cpp の 357 行目に定義があります。
| void MiniDb::save | ( | ) | const |
現在の DB をテキスト形式で永続化する
std::ofstream で path_ を上書きモードで開く。<key> <value>\n で 1 行ずつ書き出す。mini_db.cpp の 402 行目に定義があります。
| volatile sig_atomic_t anonymous_namespace{mini_db.cpp}::g_running = 1 |
サーバ実行継続フラグ (SIGINT で 0 になる)
volatile sig_atomic_t 型でなければ、シグナルハンドラとメインループ間の 読み書きは未定義動作になるおそれがある。run() のループ条件として使う。
mini_db.cpp の 25 行目に定義があります。