2022/03/12 全国学生エンジニア交流会「NSEEM」にて発表 https://github.com/yt8492/NativeServer
サーバーフレームワークの仕組みが気になったので車輪の再発明をしてみた2022/03/12 NSEEM
View Slide
自己紹介HN: マヤミトID: yt8492会津大学 学部4年OtakuProject運営GitHub: https://github.com/yt8492趣味: Kotlin, Twitter, ウマ娘Twitter: yt8492
バックエンドの開発してる人
フレームワークの仕組み、気になりません?
じゃあ作ってみよう!w
今回のテーマ: サーバーフレームワーク自作
やること- TCPのソケット通信をラップしてHTTP通信を実装- それをもとにフレームワークを実装- 使用言語はKotlin/Native (苦行)- Cの標準関数のブリッジくらいしか提供されてないので逆に勉強になると思った- もしこのLTを見ている人が同じことをしたいと思ったら素直に Kotlin/JVMでやることをおすすめします
HTTP通信- HTTPの仕様はRFCで定義されている- 今回はHTTP/1.1でやるのでRFC2616- TCPのリクエストをどのように解釈して、レスポンスをどのような形式で送ればいいかなどが書かれている- リクエストを解釈してレスポンスを返せれば良い- リクエスト- リクエストライン- ヘッダ- ボディ- レスポンス- ステータスライン- ヘッダ- ボディ
HTTPリクエストを解釈する- TCPのリクエストから以下を解釈できればよい- リクエストライン- ヘッダ- ボディ- RFCではこのように定義されている- Request = Request-Line*(( general-header| request-header| entity-header ) CRLF)CRLF[ message-body ]
リクエストのリクエストラインを解釈する- RFCではこのように定義されている- Request-Line = Method SP Request-URI SP HTTP-Version CRLF- メソッド、URI、HTTPバージョンが空白区切りで、最後に CRLF(改行)がある- つまり、ソケット通信で以下のことができればよさそう- CRLFが現れるまで通信を読み込む- 読み込んだバイト列を文字列として扱う- 空白で区切り、それをメソッド、 URI、HTTPバージョンとする
リクエストのリクエストラインを解釈する
リクエストのヘッダを解釈する- ヘッダフィールドのフォーマットは以下のように定義されている- message-header = field-name ":" [ field-value ]field-name = tokenfield-value = *( field-content | LWS )field-content = <field-value を構成し、*TEXT あるいはtoken, separators, quoted-string を連結したものから成る OCTET>- 名前と値がコロン区切りになっていて、値は前後に 0個以上の空白を含む場合がある- リクエストのヘッダは 0個以上のヘッダフィールドが CRLF区切りになっていて、 CRLFのみの行がヘッダの終わりになる
リクエストのヘッダを解釈する- ソケット通信で以下のことができればよさそう- CRLFが現れるまで読み込む- 読み込んだ結果が空であればヘッダの終わり、そうでなければコロンで区切ってそれを名前と値とする- 終わりでない場合は繰り返す
リクエストのボディを解釈する- ボディは以下のように定義されている- message-body = entity-body| - ヘッダに Content-Length や Transfer-Encoding 各ヘッダフィールドを含むとボディが存在し、 なければボディが空- Content-Lengthが指定されている場合はそのバイト数だけボディをよみこむ
リクエストのボディを解釈する- 簡単のためTransfer-Encodingは今回考慮しないことに🙏
レスポンスをHTTPの形式にする- 以下の情報をTCPのレスポンスとして返せればよい- ステータスライン- ヘッダ- ボディ- RFCではこのように定義されている- Response = Status-Line*(( general-header| response-header| entity-header ) CRLF)CRLF[ message-body ]
ステータスラインをTCPのレスポンスに書き込む- RFCではこのように定義されている- Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF- つまり、ソケット通信で以下のことができるとよさそう- HTTPバージョンと区切りの空白を書き込む- ステータスコードと区切りの空白を書き込む- 説明句と終端のCRLFを書き込む
ヘッダをTCPのレスポンスに書き込む- 基本的にはリクエストと同じ- ヘッダフィールドの名前と値がコロン区切りになっていて、 CRLFで終わる- CRLFのみの行がヘッダの終わり
ボディをTCPのレスポンスに書き込む- 今回はTransfer-Encodingを考慮しないので単純にボディとして渡されたバイト列をそのまま書き込む
ここまでがRFCに沿った実装
ここからフレームワークとしての便利実装
フレームワークなら便利であってほしい- HTTPを最低限実装しただけではバックエンド開発に使うには厳しい- Nodeのhttpモジュールとかだけでやろうとするとしんどい- フレームワークなら次のような機能があってほしい- クエリパラメータ- パスパラメータ- ルーティング
クエリパラメータの取得- 今回は以下のようになっているという前提で扱う- URIを?で区切った後ろの部分- 名前=値という形でフィールドが構成され、それが &で連結されている- 名前と値はURLエンコードされる- 以下のように実装する- URIを?で区切った後ろの部分を取得する- 空でなければさらに &で区切る- 更に=で区切り、それぞれを URLデコードする
ルーティング- 今回はユーザーが以下のように使えるように実装する- get("/hoge") { … }, post("/fuga") { … } のように、メソッドごとに関数が用意されていて、それ にパスを指定し、ラムダ関数の中で実際にリクエストを受け取ったときの処理を書く- 次のように実装する- ユーザーが get("/hoge") { … } のように関数を呼び出すとハンドラが追加される- ソケットから受け取った TCPのリクエストをHTTPのリクエストとして解釈する- 受け取ったリクエストの URIをもとにどのハンドラで処理するべきか評価し、最も評価度が高いハンドラに実際に処理をさせる- 処理の結果ユーザーが生成したレスポンスのインスタンスを実際に HTTPのレスポンスとしてTCPのソケットに書き込む
ハンドラの実装- ハンドラ自体はパスとHTTPメソッドと処理をする関数を持つだけ- ユーザーがHTTPのリクエストに対応した関数を呼び出すとハンドラのリストに追加されていく
リクエストをハンドラにマッチさせる- 同じリクエストでも複数のハンドラにマッチする可能性がある- /hoge/fuga は /hoge/fuga, /hoge/:param, /hoge/* にマッチする可能性がある- マッチ度が高いのは /hoge/fuga > /hoge/:param > /hoge/*- これを実際にリクエストが来るたびに行う- ハンドラのパスを/で区切り、それぞれが定数、パスパラメータ、ワイルドカードかどうかによってそのハンドラがマッチした場合の評価度を計算する- パスパラメータを持つ場合は実際にそれを取り出す- ソースコードは結構長くなったのでGitHubまで見に来て
使う側はこんな感じで使える
実際のソースコードはこちら- yt8492/NativeServerhttps://github.com/yt8492/NativeServer
まとめ- ソケット通信からサーバーフレームワークの実装できた- RFCを読みながら実装するとかなり勉強になる- 自分の知らなかった仕様に出会える- 実際に動くと感動
みんなも車輪の再発明しよう!楽しいよ!