試験ノート - 実装ドリル 1.0
読み取り中…
検索中…
一致する文字列を見つけられません
mini_db (KV ストア)

詳解

練習のテーマ

MiniDb クラスの位置付け

1ポート=1ファイルで動作するインメモリ Key-Value ストアサーバ。

クラス設計全体像

構造視点での全体像。実行時の流れ (時間軸) は下の「全体シーケンス」「処理フロー」を参照。

責務分類

分類 要素 役割
公開 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))

設計上の why

全体シーケンス

処理フロー (受信〜応答)

関数詳解

◆ main()

int main ( int  argc,
char **  argv 
)

プログラムのエントリポイント

実装のステップ

  1. 引数チェック: argc != 3 なら stderr"Wrong number of arguments" を出して exit(1)
  2. argv[1]std::atoi でポート番号にし、範囲外 (≤0 / >65535) なら 同じくエラー終了。
  3. SIGINTon_sigint に紐付け (= g_running を 0 にするだけ)。
  4. SIGPIPESIG_IGN にし、切断済み fd への send でプロセスが 落ちないようにする。
  5. MiniDb server(port, argv[2]) を生成し、setup()run() の順に呼ぶ。
  6. run() は SIGINT 受信時に save() してから戻るため、return 0 で正常終了する。
引数
[in]argc引数数 (3 を期待)
[in]argvargv[1] = port, argv[2] = persistence file path
戻り値
0: 正常終了, 1: 引数エラー

mini_db.cpp49 行目に定義があります。

◆ on_sigint()

void anonymous_namespace{mini_db.cpp}::on_sigint ( int  )

SIGINT ハンドラ。g_running を 0 にするだけ。

実装のステップ

  1. g_running = 0; だけを行う。
注意
シグナルハンドラ内で printf / std::cerr / malloc などを 呼ばない (非 async-signal-safe)。永続化 (save()) は通常コンテキストで行う。

mini_db.cpp79 行目に定義があります。

◆ fatal()

void fatal ( void  )

Fatal error\nstderr に書いて exit(1) する

実装のステップ

  1. std::cerr"Fatal error"std::endl を出す (改行 + flush)。
  2. std::exit(1) でプロセスを終了する。
覚え書き
シグナル安全ではない。socket / bind / listen 失敗など 通常コンテキストでのみ呼ぶ。

mini_db.cpp95 行目に定義があります。

◆ MiniDb()

MiniDb::MiniDb ( int  port,
const std::string &  path 
)

ポートと永続化先パスを束縛する

引数
[in]port1〜65535 の TCP ポート番号
[in]pathSIGINT 時にダンプするファイル、起動時にロードするファイル

実装のステップ

  1. メンバ初期化子で port_ / path_ を保存。
  2. listen_fd_ / max_fd_ を -1 (= 未確保) に置く。
  3. FD_ZERO(&active_fds_) でビット集合をクリア。
注意
ここではまだ socketload も呼ばない。setup() で行う。

mini_db.cpp114 行目に定義があります。

◆ ~MiniDb()

MiniDb::~MiniDb ( )

デストラクタ (全 fd をクローズ)

実装のステップ

  1. buffers_ を走査し、登録されている全クライアント fd を close
  2. listen_fd_ が有効 (>= 0) なら close
覚え書き
save() はここでは呼ばない。永続化は SIGINT 経路で run() の最後に行う。

mini_db.cpp129 行目に定義があります。

◆ setup()

void MiniDb::setup ( )

サーバ起動準備 (DB ロード → ソケット作成 → bind → listen → "ready")

実装のステップ

  1. load() を呼んで前回の状態を復元する。
  2. socket(AF_INET, SOCK_STREAM, 0) でリスニングソケットを作成。失敗で fatal()
  3. setsockopt(SO_REUSEADDR) を付け、再起動時の "Address already in use" を回避。
  4. sockaddr_inINADDR_LOOPBACK (= 127.0.0.1) と port_ で埋める。
  5. bind / listen(128) を行い、失敗で fatal()
  6. active_fds_listen_fd_ をセットし、max_fd_ を更新。
  7. 標準出力に "ready\n" を出して接続準備完了を通知。
注意
bind 先は **127.0.0.1 のみ**。INADDR_ANY ではないこと。

mini_db.cpp153 行目に定義があります。

◆ load()

void MiniDb::load ( )
private

起動時に path_ を読み、空白区切り 1 行 1 ペアで db_ を初期化。

永続化ファイルから DB を読み戻す

実装のステップ

  1. std::ifstream でファイルを開く。失敗 (ファイル無し) なら何もせず返る。
  2. ifs >> key >> value で 1 行ずつ取り出し、db_[key] = value で登録する。
  3. EOF まで繰り返す。空白区切り・改行区切りどちらも許容される。
注意
キーと値は空白を含まない (subject の前提) ため、>> で安全に読める。

mini_db.cpp192 行目に定義があります。

◆ run()

void MiniDb::run ( )

サーバのメインループ。SIGINT 受信で抜け、save() してから戻る。

実装のステップ

  1. g_running が真の間ループする。
  2. 毎回 rfds = active_fds_ でビット集合を複製。
  3. select(max_fd_+1, &rfds, NULL, NULL, NULL) でブロック。
    • EINTR (= SIGINT 受信) なら continue し、ループ条件で抜ける。
    • その他のエラーは fatal()
  4. レディな fd を 0..max_fd_ の範囲で走査し、rv 件分だけ消化:
  5. ループを抜けたら save() を呼んでファイルへダンプし、戻る。
注意
SIGINT ハンドラ内で save() してはいけない (signal-safe ではない)。 ハンドラはフラグを 0 にするだけにし、保存は通常コンテキスト (ループ脱出後) で行う。

mini_db.cpp220 行目に定義があります。

◆ accept_client()

void MiniDb::accept_client ( )
private

accept(2) し、active_fds_buffers_ を更新する。

新規クライアントを accept し、状態を登録する

実装のステップ

  1. accept(listen_fd_, NULL, NULL) で新規 fd を取得。失敗時は黙って戻る。
  2. FD_SET(fd, &active_fds_) で監視対象に追加。
  3. fd > max_fd_ なら max_fd_ を更新。
  4. buffers_[fd] = "" で per-client 受信バッファを空で初期化。
覚え書き
接続元アドレスは保存しない (機能上不要)。

mini_db.cpp255 行目に定義があります。

◆ recv_data()

void MiniDb::recv_data ( int  fd)
private

recv(2) でデータを取り込み、行ごとに handle_commandsend する。

クライアント fd から読み、行ごとに handle_command を呼ぶ

実装のステップ

  1. recv(fd, buf, 1024, 0) で 1 チャンクを読む。
  2. n == 0 (相手切断) または n < 0 && errno != EINTR (失敗) なら disconnect(fd) を呼んで終わる。
  3. EINTR なら静かに戻る (次回 select で再度起こされる)。
  4. 受け取ったバイト列を buffers_[fd] に追記。
  5. ‘’
    'を見つける限り、その手前を 1 行として切り出し、 handle_command(line)の戻り文字列をsendする。 -# 改行を含まない残りはバッファに残し、次回のrecv` を待つ。

部分行の例

recv: "POS" -> buffers_[fd] = "POS" (応答なし)
recv: "T A B\nGET A\n" -> "POST A B" を処理 → "0\n"
"GET A" を処理 → "0 B\n"
std::map< int, std::string > buffers_
fd → 未完成行バッファ
Definition mini_db.hpp:190
引数
[in]fd受信対象のクライアント fd

mini_db.cpp288 行目に定義があります。

◆ disconnect()

void MiniDb::disconnect ( int  fd)
private

fd を close し、active_fds_ / buffers_ から外し、max_fd_ を巻き戻す。

クライアント fd を閉じ、関連状態をクリーンアップする

実装のステップ

  1. close(fd) で fd を閉じる。
  2. FD_CLR(fd, &active_fds_) で監視対象から外す。
  3. buffers_.erase(fd) で受信バッファを破棄。
  4. 切断した fd が max_fd_ だった場合に備え、active_fds_ に 含まれない fd の範囲を max_fd_ から降順に剥がし、max_fd_ を巻き戻す。
注意
max_fd_listen_fd_ を下回らないように > でガードする。
引数
[in]fd切断対象のクライアント fd

mini_db.cpp323 行目に定義があります。

◆ handle_command()

std::string MiniDb::handle_command ( const std::string &  line)
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"

実装のステップ

  1. std::istringstream で行をホワイトスペース区切りトークン列に分解。
  2. トークン数が 0 の行は即 "2\n"
  3. 先頭トークンと残りの個数で分岐し、辞書 db_ を更新/参照/削除。
  4. それ以外 (引数数不一致を含む) はすべて "2\n"
注意
キー/値は空白を含まない (subject の前提)。 tok.size() == 3 (POST) や == 2 (GET/DELETE) で arity を厳密に確認する。
引数
[in]line1 行 (末尾の \n は呼び出し側で除去済み)
戻り値
クライアントへ送るべき応答文字列 (末尾 \n を含む)

mini_db.cpp357 行目に定義があります。

◆ save()

void MiniDb::save ( ) const

現在の DB をテキスト形式で永続化する

実装のステップ

  1. std::ofstreampath_ を上書きモードで開く。
  2. 各エントリを <key> <value>\n で 1 行ずつ書き出す。

出力フォーマット例

A B
persist hello
覚え書き
save() は SIGINT 受信後の正常終了パスで呼ばれる (run()g_running == 0 抜け道) ので、 本体はシグナルハンドラ内ではなく通常コンテキストで動く。

mini_db.cpp402 行目に定義があります。

変数詳解

◆ g_running

volatile sig_atomic_t anonymous_namespace{mini_db.cpp}::g_running = 1

サーバ実行継続フラグ (SIGINT で 0 になる)

volatile sig_atomic_t 型でなければ、シグナルハンドラとメインループ間の 読み書きは未定義動作になるおそれがある。run() のループ条件として使う。

mini_db.cpp25 行目に定義があります。