エンジニア養成機関 42Tokyo の課題で、WebサーバをC++で作る課題に取り組みました。
この記事では課題で学んだこと、取り組みについて振り返ります。
課題の概要
NGINXのようなWebサーバをC++98で作る課題です。
「NGINXのような」というのは以下のような意味です。
- HTTP/1.1に準拠していること。
- といっても完全準拠ではありません。
- 対応するメソッドは GET, POST, DELETE だけです。
- I/O多重化によって複数のリクエストをさばくこと。
- I/O多重化については、以下の記事が分かりやすいです。 christina04.hatenablog.com
- I/O多重化に使う関数は select, poll, epoll, kqueue から選択できます。
- Configファイルによってサーバの設定を読み込むこと。
※Configファイルの記法はNGINXに完全に準拠する必要はありません。
以下はNGINXと異なります。
- 直接のCGI実行
- POSTでのファイルアップロード
成果物
以下スクリーンショットです。
かかった時間
- 期間:約98日(2021/11/09〜2022/02/14)
- 時間:約197時間
開発の流れ
echoサーバを作ったあと、
- HTTPリクエストのパース
- I/O多重化対応
- Configファイル読み込み
など、少しずつ機能追加をしていきました。
実装
大まかなフロー
Configファイルを読み込み、開くポート毎にSocket(ServerSocket)を作成し、bind()とlisten()して接続を受け付けるようにします。
ServerSocketのディスクリプタをイベント(kevent)に登録し、クライアントからの接続を検知できるようにします。
その後イベントループに入ります。イベントが発生したら、発生したイベントに応じてSocketのメンバ関数を実行します。
- イベントは
void *
のユーザデータを持てるので、そこにオブジェクトのポインタをセットしています。 - ServerSocketは、クライアントから接続があったら、accept()して新たなSocket(ClientSocket)を作成します。
- ClientSocketは状態を持ちます。最初はHTTPリクエストを受信する状態です。 この状態に応じてイベントを登録・登録解除し、HTTPリクエストの受信、ファイルの読み込み、HTTPレスポンスの送信をしていきます。
I/O多重化の関数選定
課題の概要 で説明したように、I/O多重化の関数は select, poll, epoll, kqueue から選ぶことができます。
自分たちのチームはkqueueを採用しました。
理由はselect, pollに比べ、パフォーマンスが優れているためです。
- 各関数のパフォーマンスについては、 libevent のBenchmarkが参考になります。
- select, poll, epoll, kqueueのベンチマークが載っています。
- 画像サイズが小さいですが、URLの
benchmarks
をbenchmark
に変更すると、サイズの大きい画像にアクセスできます。
epollも候補でしたが、epollの場合はVMでレビューをする必要がありました。
VMはレビュー時に安定せず、レビューでの負荷テストに耐えられない可能性があったため、kqueueを選択しました。
パース方法
HTTPリクエストのパースは調べたところ、3つ方法があるようでした。
- 文字ごとに状態を持たせる
- NGINXはこの方法のようです。リクエスト行だけでも たくさん状態を持っています。
- 行ごとに状態を持たせる
- リクエスト行、ヘッダ、ボディと、行ごとに状態を持たせる方法です。
- "\r\n\r\n" を見つけたら一気にパースする
- ヘッダとボディの間の空行まで読み込んだら、リクエスト行とヘッダを一気にパースする方法です。
自分たちのチームは、行ごとに状態を持たせてパースする方法を採用しました。
NGINXと挙動を合わせたかったのですが、文字ごとに状態を持たせるのは大変そうだったので、行ごととしました。
テスト
HTTPリクエスト・レスポンスのテスト
Python + pytest + HTTPConnection を使って作成しました。
- pytestを使ったのは趣味です。
- HTTPConnectionは、CPythonのHTTPServerのテスト を参考にして採用しました。
もう少しテストケースを追加しやすい作りにすればよかったな……と後になって思いました。
Configファイルのテスト
ShellScriptで作成しました。不正なConfigファイルを読み込ませ、その出力と想定されるエラーメッセージとのdiffを取って結果を表示します。
Configファイルのテストは自分がベースを作り、チームメイトにメンテしていただきました。
チーム
この課題は2-3人で取り組むことができます。
課題に取り組み始めて1ヶ月は2人で、その後は1人入っていただき3人で取り組みました。
役割分担は以下のようになっていました。
- 私:HTTPサーバの実装
- yuka-dir さん:設定ファイルとCGIの実装
- tmurakam42 さん:進捗管理と実装のアドバイス
コミュニケーション
チームのコミュニケーションは試行錯誤をしていました。
- 1日2時間もくもく作業し、終了時に成果報告
- 週1で進捗報告
- 毎日5-10分で進捗報告 + timesで適宜つぶやく
最終的に一番下の方法でコミュニケーションを取っていました。
その他
チーム3人だけだと分からないことがあったりしたので、他の学生が別の課題で行っていた「P2P勉強会」を開催しました。
P2P勉強会は、週2-3回決まった時間に、課題に取り組んでいる学生が集まって進捗や困っていることを報告する会です。
他の方に聞くことで疑問点が解消されたり、モチベーションがアップしたりしたので、開催してよかったです。
大変だったこと
3つ大変だったことがありました。
機能追加による設計変更
echoサーバから機能を少しずつ追加したので、考慮できていないことがあり、後で設計を変更する必要がありました。
URIのルーティング
考慮できてないパターンが見つかり修正を複数回行いました。フローチャートを作った方がいいかもしれません。
siegeでのリクエスト詰まり
HTTPサーバは、siegeを使った負荷テストで一定のAvailabilityを保つ必要があります。
私達が作成したHTTPサーバは、siegeを使うと初めは動くのですが、しばらくするとリクエストが詰まり、時間を置くと解消されて、また詰まって……という動作をしました。時にはタイムアウトもしていました。
他のチームのselectを使ったプログラムでは、同事象は発生しないようでした。
selectのように動作が遅くなるよう、試しにイベントの発生毎に1msスリープすると、リクエストの詰まりは解消されました。
根本的な原因が分からず、kqueueの良さを打ち消す対処しかできなかったのが悔やまれます。
参考書籍・URL
HTTP全般
- HTTP — 日本語訳
- RFC7230とRFC7231を中心に読みました。
- RFC 3875 - The Common Gateway Interface (CGI) Version 1.1 日本語訳
- 『HTTPの教科書』
Webサーバの実装
- 『Head First C ―頭とからだで覚えるCの基本』
- Webサーバを作る大まかな流れが分かります。Webサーバは独自プロトコルで、HTTPはクライアントのみ実装します。
- 『コンピュータ・システム』
- HTTPサーバを作る大まかな流れが分かります。
- 『Linuxプログラミングインタフェース』
- 「59章 ソケット:インターネットドメイン」と「63章 高度なI/Oモデル」を中心に読みました。特に「63章 高度なI/Oモデル」はI/O多重化のための関数について載っており、参考になりました。
HTTPパーサ
- Writing a fast HTTP parser
- H2O - the optimized HTTP server
- http-parser と picohttpparser との比較 - methaneのブログ
I/O多重化