HTTPサーバーをC++で実装する課題の振り返り

42 TokyoでHTTPサーバーを自作する課題をクリアしたので振り返り記事を書きます。

課題概要

nginxのようなHTTPサーバーをC++98で実装する課題です。2~3人のチームで取り組みます。

課題要件

  • 対応メソッドはGET, POST, DELETE
  • サーバーの設定を記述したconfigファイルを読み込む
  • I/O多重化によるリクエストの処理
  • オートインデックス
  • リダイレクト
  • CGI
  • 負荷テストに耐える性能

成果物

スクリーンショット

html取得

オートインデックス

CGI実行

リポジトリ

github.com

実装

細かい実装については書くと長くなってしまうので、簡単にリンクを置くだけにしておきます。

I/O多重化

チームメイトがわかりやすくまとめてくれました。

fuschia-lift-12b.notion.site

I/O多重化

ノンブロッキングI/O と poll() 、 select() 、 epoll() 、 keque() などのシステムコールを利用して複数のファイルディスクリプタをシングルプロセス・シングルスレッドで管理すること。

CGI

こちらもチームメイトがまとめてくれました。

github.com

Common Gateway Interface の略、通常のサイトは予め用意したHTMLを返す(静的ページ)だけですがCGIを用いることでウェブページからの入力に応答したり、動的な出力を行うことができます。 例えば、サイトページの訪問者の表示やアクセス時間を表示、お問い合わせフォームなど作成することができます。

configファイル

configの仕様についてまとめました。

github.com

テスト

テスト作成にあまりコストはかけないようにしましたが、変更に耐えられるように最低限のテストは作成しました。

単体テスト

各自で実装したクラスのテストをGoogle Testを使って作成しました。
自分が担当した箇所では特にHTTPリクエストをパースする部分を重点的にテストしました。リクエストの入力パターンが無数にあるのでテストを作っていてよかったです。
作成したGoogle TestはGithub ActionsでPR時に動くようにしました。

結合テスト

Go言語でクライアントを作成してテストを行いました。
チャンク化されたデータ*1の送信で若干苦労したので残しておきます。

最終的に以下のようにしてリクエストを送りました。

func postChunkedData() {
    ...

    reader, writer := io.Pipe()

    req := &http.Request{
        Method:           "POST",
        URL:              url,
        ProtoMajor:       1,
        ProtoMinor:       1,
        TransferEncoding: []string{"chunked"},
        Body:             reader,
        Header:           make(map[string][]string),
    }

    client := http.DefaultClient
    bodyReader := strings.NewReader("HelloWorld")

    go func() {
        for {
            buf := make([]byte, 3)
            n, err := bodyReader.Read(buf)
            if n == 0 {
                break
            }
            if err != nil {
                panic(err)
            }
            writer.Write(buf)
        }
        err = writer.Close()
        if err != nil {
            panic(err)
        }
    }()

    resp, err := client.Do(req)

    ...

}

以下のようなHTTPリクエストを作ることができました。

POST / HTTP/1.1
Host: localhost:1111
Transfer-Encoding: chunked
User-Agent: Go-http-client/1.1

3
Hel
3
loW
3
orl
1
d
0

netパッケージを使って生のリクエストを作ることもできたのですが、正常系はなるべくnet/httpパッケージを使って実装したかったので実現できてよかったです。

その他

2つのシェルスクリプトを作成しました。

  • 不正なconfigファイルを引数にとってプログラムを実行しようとする
  • プログラムが開いているディスクリプタを監視する

課題の進め方

3人チームで取り組みました。課題着手からクリアまで大体2ヶ月半ほどかかりました。

準備

  • ClangFormat*2導入
  • clang-tidy*3導入
  • コミットメッセージ、PRのテンプレート作成

実装を始めてから

週2回時間を決めてミーティングを実施して、進捗報告や相談を行いました。
1人フルタイムで働いている方がいて、同期的にコミュニケーションを取るのが難しい部分もあったのでPRを丁寧に書いてキャッチアップしやすいようにしました。分岐が複雑な部分等はmermaid記法でフローチャートを書いたりしました。

反省点

例外処理

エラーレスポンスを返す場合に例外機構を使っていて処理を追うのが大変になってしまいました。*4
例外機構を使うことでコード量は少なくすることができましたが、エラー処理が分かりづらいのはデバッグのコストが高くなると思うので個人的に失敗だったと思いました。
Go言語のエラー処理のように例外機構を使わない実装の方が良かったのかもしれないです。

クラス設計

クラスの役割についての意識が足りていなかった気がします。
同じ課題に取り組んでいる他のチームの方に「このクラスがこの処理やってるの変じゃない?」と言われて「確かに」となる部分が何点かありました。
短期間で完結する課題だったので特に問題は起きてませんでしたが、もし長期間メンテを必要としたりプロジェクトメンバーが入れ替わるようなプロジェクトだった場合に不都合が発生しうるかもしれないと思いました。

最後に

大変だったこと

上でも書きましたが、オブジェクト指向に慣れていなかったのでクラス設計が大変でした。
チームで色々と議論して、Stateパターン*5やSingletonパターン*6を取り入れて、ある程度満足できる形にできました。
個人的に全体の設計は任せっきりになってしまった印象があるので、本を読んで勉強したいと思いました。

よかったこと

全体的にとても快適に開発を進めることができました。理由としては、以下の2つがあると思いました。

  • クラス間を疎結合に設計することで役割分担がスムーズにできた。
  • コミュニケーションのコストが低く、気軽に相談できた。
    • ミーティングは週2回でしたが、何かあればissueやボイスチャットで気軽に相談に乗ってもらいました。

WebやC++についての学習になったのはもちろんですが、チーム開発についての学びもあり楽しかったです。