試験ノート - 実装ドリル 1.0
読み取り中…
検索中…
一致する文字列を見つけられません
mini_serv (チャットサーバ)

詳解

練習のテーマ

mini_serv の位置付け

1ポート=1プロセスで動作するインメモリ・マルチクライアント・チャットサーバ。

グローバル状態

変数 役割
g_listen_fd リスニングソケットの fd (ブロードキャスト対象から除外するために保持)
g_maxfd select に渡す現在の最大 fd
g_next_id 次に採番するクライアント ID (0 から1ずつ加算)
g_ids[65536] fd → クライアント ID の対応表
g_msgs[65536] fd → 未完成行 (改行が来るまでのバッファ) のポインタ
g_readfds select 直前に g_activefds から複製する読み込み用集合
g_writefds select 直前に g_activefds から複製する書き込み用集合
g_activefds 現在アクティブな fd 集合 (リスニング + 全クライアント)
g_buf_read[4097] recv 用固定バッファ (4096 + 終端 '\0')
g_buf_write[200042] ブロードキャスト用の整形済みメッセージバッファ

全体構造

構造視点での全体像。mini_serv はクラスを持たないため、 翻訳単位 (TU) 内に閉じた static グローバルと自由関数群でモジュールを構成する。 実行時の流れ (時間軸) は下の「全体シーケンス」「処理フロー」を参照。

責務分類

分類 要素 役割
エントリ main() 引数チェック / ソケット作成 / グローバル初期化 / select ループ
接続管理 accept_client() / disconnect_client() 入退室処理と通知のブロードキャスト
I/O ループ receive_from() / send_all() 1 fd の受信処理 / 整形済みメッセージのブロードキャスト
buffer ops str_join() / extract_message() per-fd 動的バッファの結合と 1 行抽出 (所有権の出し入れ)
エラー fatal() システムコール / malloc 失敗時に Fatal error\n を吐いて即終了
グローバル状態 g_listen_fd / g_maxfd / g_next_id / g_ids / g_msgs / g_*fds / g_buf_* 翻訳単位内に閉じた static 変数 (mini_db の無名 namespace に対応する位置)

設計上の why

全体シーケンス

処理フロー (受信〜ブロードキャスト)

関数詳解

◆ main()

int main ( int  ac,
char **  av 
)

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

実装のステップ

  1. 引数チェック: ac != 2 なら stderr"Wrong number of arguments\n" を出して exit(1)
  2. socket(AF_INET, SOCK_STREAM, 0) でリスニングソケット作成。失敗で fatal()
  3. sockaddr_in を構築:
    • sin_family = AF_INET
    • sin_addr.s_addr = htonl(0x7f000001) (= 127.0.0.1 のみ)
    • sin_port = htons(atoi(av[1]))
  4. bind / listen(128) を実行。いずれも失敗で fatal()
  5. グローバル状態を初期化:
    • FD_ZERO(&g_activefds)FD_SET(g_listen_fd, &g_activefds)
    • g_maxfd = g_listen_fdg_next_id = 0
    • g_ids / g_msgsbzero で全消去
  6. 永久ループ:
    • g_readfds = g_writefds = g_activefds で複製。
    • select で待機。< 0 (= シグナル割込み等) なら continue でやり直し。
    • 0..g_maxfd を走査し、FD_ISSET(fd, &g_readfds) の最初の 1 件だけ処理:
    • break で内側 for を抜け、再度 select から始める。
注意
1 イテレーションで複数の fd を順に処理せず、break で 1 件ずつ 裁いて select に戻る設計。fd 集合 (g_writefds) が古くなる タイミングを最小化する。
引数
[in]ac引数数 (2 を期待)
[in]avav[1] = ポート番号 (文字列)
戻り値
通常は到達しない (永久ループ)。fatal() または引数エラーで終了する。

mini_serv.c299 行目に定義があります。

◆ fatal()

static void fatal ( void  )
static

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

実装のステップ

  1. write(2, "Fatal error\n", 12) で 12 バイト書き出す。
  2. exit(1) で終了する。
注意
printf 系は許可関数に含まれないため使えない。文字列リテラルの長さは ハードコードする (12 = strlen("Fatal error\n"))。

mini_serv.c358 行目に定義があります。

◆ accept_client()

static void accept_client ( void  )
static

新規クライアントを accept し、ID を採番して入室通知をブロードキャスト

実装のステップ

  1. accept(g_listen_fd, ...) で新しい fd を取得。失敗で fatal()
  2. cfd > g_maxfd なら g_maxfd を更新。
  3. g_ids[cfd] = g_next_id++ で新規 ID を発番。**ID は再利用しない** (subject: "the new client id is the maximum given id, +1")。
  4. g_msgs[cfd] = NULL で蓄積バッファを未確保状態に。
  5. FD_SET(cfd, &g_activefds)select の監視対象に追加。
  6. sprintf(g_buf_write, "server: client %d just arrived\n", g_ids[cfd])
  7. send_all(cfd)本人を除く 全クライアントに通知。
注意
自分自身に "just arrived" を送らない。except = cfd

mini_serv.c379 行目に定義があります。

◆ receive_from()

static void receive_from ( int  fd)
static

1 つのクライアント fd からデータを読み、行ごとに整形・転送する

実装のステップ

  1. recv(fd, g_buf_read, 4096, 0) で読み込む。
  2. r <= 0 なら disconnect_client(fd) を呼んで戻る。
  3. g_buf_read[r] = '\0' で null 終端する (g_buf_read[4097] で1バイト余裕あり)。
  4. g_msgs[fd] = str_join(g_msgs[fd], g_buf_read) で per-fd バッファに連結。
  5. extract_message(&g_msgs[fd], &msg) が真を返す限りループ:
    • sprintf(g_buf_write, "client %d: %s", g_ids[fd], msg) で整形 (msg は末尾に \n を含むため、書式に追加の \n は不要)。
    • send_all(fd) で送信元を除いて転送。
    • free(msg) で 1 行分のバッファを解放。

部分行の扱い

  • 1回の recv\n が無い: 蓄積するだけで何も送らない。
  • 1回の recv\n が複数: 各行を独立にブロードキャスト。
引数
[in]fd受信対象のクライアント fd

mini_serv.c415 行目に定義があります。

◆ send_all()

static void send_all ( int  except)
static

g_buf_write の内容を except と listen 以外の全クライアントへ送信する

実装のステップ

  1. strlen(g_buf_write) で送信長を求める。
  2. 0..g_maxfd を走査:
    • fd == g_listen_fd ならスキップ (リスニング fd には書かない)。
    • fd == except ならスキップ (送信元へエコーバックしない)。
    • FD_ISSET(fd, &g_writefds) (= select が書き込み可と判定) なら send
注意
「メッセージを読まない (lazy) クライアント」の send 失敗で プロセスを巻き込まれないよう、戻り値は無視する。SIGPIPE 対策は g_writefds (= writefds) のチェックに委ねる。
引数
[in]exceptブロードキャスト対象から除外する fd (送信元クライアント)

mini_serv.c451 行目に定義があります。

◆ disconnect_client()

static void disconnect_client ( int  fd)
static

クライアントを切断し、退室通知をブロードキャスト + 後始末

実装のステップ

  1. sprintf(g_buf_write, "server: client %d just left\n", g_ids[fd])
  2. FD_CLR(fd, &g_activefds) で監視から外す。
  3. close(fd) で fd を閉じる。
  4. send_all(fd)退室者を除く 他クライアントに通知。
  5. free(g_msgs[fd]) で蓄積バッファを解放。g_msgs[fd] = NULL に戻す。
注意
close の前に g_buf_write を作る順序は、g_ids[fd] が まだ有効な間に値を取り出すためでも問題ない (ID 配列は静的)。
引数
[in]fd切断対象のクライアント fd

mini_serv.c478 行目に定義があります。

◆ str_join()

static char * str_join ( char *  buf,
char *  add 
)
static

既存バッファ bufadd を結合した新しいバッファを返す (buf は free)

実装のステップ

  1. 結合後のサイズを (strlen(buf or 0)) + strlen(add) + 1 (終端 '\0' 用) で算出。
  2. malloc で新領域を確保。失敗で fatal()
  3. res[0] = '\0' で空文字列に初期化。
  4. buf が非 NULL なら strcat(res, buf) で先に旧内容をコピー。
  5. buffree
  6. strcat(res, add) で受信チャンクを末尾に追加。
  7. res を返す。所有権は呼び出し側へ移譲。
注意
戻り値で g_msgs[fd] を上書きする使い方を想定。 旧ポインタを使い続けないこと。
引数
[in]buf旧バッファ (NULL 可)。呼び出し後は無効。
[in]add末尾に追加する null 終端文字列
戻り値
新しい結合済みバッファ (呼び出し側で free する)

mini_serv.c507 行目に定義があります。

◆ extract_message()

static int extract_message ( char **  buf,
char **  msg 
)
static

蓄積バッファから先頭の 1 行を切り出す

実装のステップ

  1. *msg = NULL で初期化、*buf == NULL なら 0 を返す。
  2. *buf を 1 文字ずつ走査し、\n を探す。
  3. 見つかったら、改行の からの残りを calloc で新バッファに strcpy
  4. *buf*msg に渡し、 (*msg)[i + 1] = '\0' で改行の直後を切り、 1 行分 (\n 込み) を独立した null 終端文字列にする。
  5. *buf = newbuf (残り) に置き換え、1 を返す。
  6. 改行が見つからなければ 0 を返す。
注意
*msg の所有権は呼び出し側に渡る。free 必須。
引数
[in,out]buf蓄積バッファへのポインタ (改行までを除いた残りに置き換わる)
[out]msg切り出した 1 行 (NULL 終端) のポインタを格納する場所
戻り値
1: 1 行抽出した / 0: 改行が無かった

mini_serv.c539 行目に定義があります。