|
Document of c-modernization-kit (porter) 1.0.0
|
Files | |
| tcpServer.c | |
| TCP サーバーサンプル共通実装。 | |
| tcpServer.h | |
| TCP サーバーサンプル共通定義。 | |
| tcpServer_common.c | |
| TCP サーバーサンプル 共通変数定義。 | |
| tcpServer_linux.c | |
| TCP サーバーサンプル Linux 実装。 | |
| tcpServer_windows.c | |
| TCP サーバーサンプル Windows 実装。 | |
TCP ポートを待ち受け、接続ごとに別プロセスで処理を行うサーバーの実装パターンを解説する。
ネットワークサーバーが複数のクライアントを同時に処理する方法として、プロセスベースのアプローチがある。各接続を独立したプロセスで処理することで、1つのクライアントの処理が他に影響を与えない堅牢な構造を実現できる。
本ドキュメントでは以下の2つのモデルを Linux と Windows それぞれで実装する。
| モデル | 特徴 |
|---|---|
| 接続ごとに fork | 接続を受け付けるたびに新しいプロセスを生成 |
| プリフォーク | 事前に N 個のワーカープロセスを起動しておく |
| 引数 | デフォルト | 説明 |
|---|---|---|
| --mode | prefork | fork = 接続ごと生成、prefork = 事前生成 |
| --port | 8080 | 待ち受けポート番号 |
| --workers | 4 | prefork 時のワーカー数 |
| --conns-per-worker | 1 | prefork 時の 1 ワーカーあたり同時接続数。1 の場合は逐次処理、2 以上の場合はイベント駆動型の多重処理 |
親プロセスは listen ソケットで待ち受け、accept() で接続を受け付けるたびに子プロセスを生成する。子プロセスは1つのクライアントだけを担当し、切断時に終了する。
Linux では fork() システムコールでプロセスを複製する。
fork() を呼び出すと、親プロセスのメモリ空間がコピーされ、ファイルディスクリプタも複製される。親と子で以下のように処理を分ける。
子プロセスが終了するとゾンビプロセスになるため、SIGCHLD シグナルハンドラで waitpid() を呼び出して回収する。
Windows には fork() がないため、CreateProcess() で自分自身を子プロセスとして再起動する。ソケットハンドルは --child <handle> の形式でコマンドライン引数に渡す。
ソケットハンドルを子プロセスで使用するには、以下の2点が必要。
起動時にあらかじめワーカープロセスを生成しておき、接続を処理するモデル。ワーカーは処理完了後も終了せず、次の接続を待つ。
全ワーカーが満杯の場合、新しい接続は listen キューに留まり、いずれかのワーカーに空きが生じるまで待機する。
--conns-per-worker に 2 以上を指定すると、各ワーカーがイベント駆動型 I/O で複数のコネクションを同時に処理する。最大同時接続数は「ワーカー数 × 1 ワーカーあたり同時接続数」になる。
Linux では複数の子プロセスが同じ listen ソケットに対して accept() を呼び出すことができる。カーネルが自動的に1つのプロセスだけを選んで接続を渡すため、親プロセスによる明示的な振り分けが不要。
--conns-per-worker 1 の動作:
--conns-per-worker N (N ≥ 2) の動作:
各ワーカーが epoll を使ったシングルスレッドのイベントループで複数のコネクションを同時処理する。
親プロセスは pause() で待機し、SIGINT (Ctrl+C) を受け取ると全ワーカーに SIGTERM を送信して終了させる。
Windows では fork() がないため、親プロセスが accept() を行い、空きワーカーにソケットハンドルを渡す方式を採用している。
ワーカーとの通信には名前付きパイプを使用する。ワーカーは --worker <pipe_name> --conns-per-worker <N> 引数付きで起動される。
--conns-per-worker 1 の動作:
--conns-per-worker N (N ≥ 2) の動作:
各ワーカーが PeekNamedPipe() でパイプを非ブロッキングチェックしながら select() でアクティブなソケットを監視するイベントループを使用する。接続が閉じるたびに完了通知を 1 バイト送信する。
各ワーカーに対して監視スレッドが割り当てられ、完了通知を受け取るたびに conn_count をデクリメントし、空きが生じたらイベントをシグナル状態にする。find_available_worker() は全ワーカーが満杯の場合、WaitForMultipleObjects() でいずれかに空きが生じるまで待機する。
| 関数 | 説明 |
|---|---|
| sigchld_handler() | ゾンビプロセスの回収 |
| shutdown_handler() | running フラグを 0 にセット |
| create_listen_socket(port) | bind + listen |
| worker_loop(server_fd, id, conns_per_worker) | ワーカーの accept ループ (N=1: 逐次、N>1: epoll) |
| platform_init(session_fn) | g_session_fn の設定 |
| platform_cleanup() | no-op |
| dispatch_internal_args(argc, argv) | 常に 0 を返す (Linux は内部起動なし) |
| run_fork_server(port) | accept → fork ループ |
| run_prefork_server(port, n, conns_per_worker) | n ワーカー生成 + pause + SIGTERM |
| 関数 | 説明 |
|---|---|
| create_listen_socket(port) | getaddrinfo + bind + listen |
| run_as_fork_child(socket) | –child 時のエコー処理 |
| worker_loop(pipe_name, conns_per_worker) | –worker 時のパイプ受信 + エコー処理 (N=1: 逐次、N>1: PeekNamedPipe+select) |
| worker_monitor_thread(arg) | 完了通知を受け取り conn_count をデクリメント |
| find_available_worker(workers, events, n, cpw) | conn_count < cpw のワーカー探索 |
| start_prefork_workers(workers, events, n, cpw) | n ワーカーを --worker --conns-per-worker で起動 |
| platform_init(session_fn) | WSAStartup + g_session_fn の設定 |
| platform_cleanup() | WSACleanup |
| dispatch_internal_args(argc, argv) | –child/–worker を検出して処理 |
| run_fork_server(port) | accept + CreateProcess(--child) ループ |
| run_prefork_server(port, n, conns_per_worker) | listen_socket 確保 + workers 起動 + 振り分けループ |
両モデルとも、子プロセスは親の標準入出力を継承する。子プロセスの printf() は親と同じコンソールに出力される。
fork() は親のファイルディスクリプタをすべて複製する。stdin (fd 0), stdout (fd 1), stderr (fd 2) も自動的に継承される。
CreateProcess() でコンソールアプリケーションを起動する場合、子プロセスはデフォルトで親のコンソールを共有する。STARTUPINFO の dwFlags に STARTF_USESTDHANDLES を設定していなければ、標準ハンドルも自動的に継承される。
| 項目 | 接続ごとに fork | プリフォーク |
|---|---|---|
| プロセス生成コスト | 接続のたびに発生 | 起動時のみ |
| 最大同時接続数 | 事実上無制限 (リソース次第) | N 個に固定 |
| メモリ使用量 | 接続数に比例 | 一定 |
| 実装の複雑さ | シンプル | やや複雑 |
| 過負荷時の挙動 | 新規 fork が遅くなる | 新規接続がキューで待機 |
--conns-per-worker 1 では、--workers を超える同時接続を試みると、既存の接続が切断されるまで待たされることを確認できる。
--conns-per-worker N (N ≥ 2) では、1 ワーカーが N 本の接続を同時に保持できる。ワーカー数 × N を超える接続が来ると、listen キューに留まる。
Ctrl+C で終了すると、prefork モードでは全ワーカーに SIGTERM が送信され、順次終了する。