Upgrade to Pro — share decks privately, control downloads, hide ads and more …

サーバーフレームワークの仕組みが気になったので車輪の再発明をしてみた

 サーバーフレームワークの仕組みが気になったので車輪の再発明をしてみた

2022/03/12 全国学生エンジニア交流会「NSEEM」にて発表
https://github.com/yt8492/NativeServer

Yuta Tomiyama

March 12, 2022
Tweet

More Decks by Yuta Tomiyama

Other Decks in Programming

Transcript

  1. サーバーフレームワークの
    仕組みが気になったので
    車輪の再発明をしてみた
    2022/03/12 NSEEM

    View Slide

  2. 自己紹介
    HN: マヤミト
    ID: yt8492
    会津大学 学部4年
    OtakuProject運営
    GitHub: https://github.com/yt8492
    趣味: Kotlin, Twitter, ウマ娘
    Twitter: yt8492

    View Slide

  3. バックエンドの開発してる人󰢧

    View Slide

  4. フレームワークの仕組み、気になりません?

    View Slide

  5. じゃあ作ってみよう!w

    View Slide

  6. 今回のテーマ: サーバーフレームワーク自作

    View Slide

  7. やること
    - TCPのソケット通信をラップしてHTTP通信を実装
    - それをもとにフレームワークを実装
    - 使用言語はKotlin/Native (苦行)
    - Cの標準関数のブリッジくらいしか提供されてないので逆に勉強になると思った
    - もしこのLTを見ている人が同じことをしたいと思ったら素直に Kotlin/JVMでやることをおすすめしま

    View Slide

  8. HTTP通信
    - HTTPの仕様はRFCで定義されている
    - 今回はHTTP/1.1でやるのでRFC2616
    - TCPのリクエストをどのように解釈して、レスポンスをどのような形式で送ればいいかなどが書かれ
    ている
    - リクエストを解釈してレスポンスを返せれば良い
    - リクエスト
    - リクエストライン
    - ヘッダ
    - ボディ
    - レスポンス
    - ステータスライン
    - ヘッダ
    - ボディ

    View Slide

  9. HTTPリクエストを解釈する
    - TCPのリクエストから以下を解釈できればよい
    - リクエストライン
    - ヘッダ
    - ボディ
    - RFCではこのように定義されている
    - Request = Request-Line
    *(( general-header
    | request-header
    | entity-header ) CRLF)
    CRLF
    [ message-body ]

    View Slide

  10. リクエストのリクエストラインを解釈する
    - RFCではこのように定義されている
    - Request-Line = Method SP Request-URI SP HTTP-Version CRLF
    - メソッド、URI、HTTPバージョンが空白区切りで、最後に CRLF(改行)がある
    - つまり、ソケット通信で以下のことができればよさそう
    - CRLFが現れるまで通信を読み込む
    - 読み込んだバイト列を文字列として扱う
    - 空白で区切り、それをメソッド、 URI、HTTPバージョンとする

    View Slide

  11. リクエストのリクエストラインを解釈する

    View Slide

  12. リクエストのヘッダを解釈する
    - ヘッダフィールドのフォーマットは以下のように定義されている
    - message-header = field-name ":" [ field-value ]
    field-name = token
    field-value = *( field-content | LWS )
    field-content = <field-value を構成し、*TEXT あるいはtoken, separators, quoted-string   
    を連結したものから成る OCTET>
    - 名前と値がコロン区切りになっていて、値は前後に 0個以上の空白を含む場合がある
    - リクエストのヘッダは 0個以上のヘッダフィールドが CRLF区切りになっていて、 CRLFのみの行がヘッ
    ダの終わりになる

    View Slide

  13. リクエストのヘッダを解釈する
    - ソケット通信で以下のことができればよさそう
    - CRLFが現れるまで読み込む
    - 読み込んだ結果が空であればヘッダの終わり、そうでなければコロンで区切ってそれを名前と値と
    する
    - 終わりでない場合は繰り返す

    View Slide

  14. リクエストのボディを解釈する
    - ボディは以下のように定義されている
    - message-body = entity-body
    |
    - ヘッダに Content-Length や Transfer-Encoding 各ヘッダフィールドを含むとボディが存在し、 な
    ければボディが空
    - Content-Lengthが指定されている場合はそのバイト数だけボディをよみこむ

    View Slide

  15. リクエストのボディを解釈する
    - 簡単のためTransfer-Encodingは今回考慮しないことに🙏

    View Slide

  16. レスポンスをHTTPの形式にする
    - 以下の情報をTCPのレスポンスとして返せればよい
    - ステータスライン
    - ヘッダ
    - ボディ
    - RFCではこのように定義されている
    - Response = Status-Line
    *(( general-header
    | response-header
    | entity-header ) CRLF)
    CRLF
    [ message-body ]

    View Slide

  17. ステータスラインをTCPのレスポンスに書き込む
    - RFCではこのように定義されている
    - Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
    - つまり、ソケット通信で以下のことができるとよさそう
    - HTTPバージョンと区切りの空白を書き込む
    - ステータスコードと区切りの空白を書き込む
    - 説明句と終端のCRLFを書き込む

    View Slide

  18. ヘッダをTCPのレスポンスに書き込む
    - 基本的にはリクエストと同じ
    - ヘッダフィールドの名前と値がコロン区切りになっていて、 CRLFで終わる
    - CRLFのみの行がヘッダの終わり

    View Slide

  19. ボディをTCPのレスポンスに書き込む
    - 今回はTransfer-Encodingを考慮しないので単純にボディとして渡されたバイト列
    をそのまま書き込む

    View Slide

  20. ここまでがRFCに沿った実装

    View Slide

  21. ここからフレームワークとしての便利実装

    View Slide

  22. フレームワークなら便利であってほしい
    - HTTPを最低限実装しただけではバックエンド開発に使うには厳しい
    - Nodeのhttpモジュールとかだけでやろうとするとしんどい
    - フレームワークなら次のような機能があってほしい
    - クエリパラメータ
    - パスパラメータ
    - ルーティング

    View Slide

  23. クエリパラメータの取得
    - 今回は以下のようになっているという前提で扱う
    - URIを?で区切った後ろの部分
    - 名前=値という形でフィールドが構成され、それが &で連結されている
    - 名前と値はURLエンコードされる
    - 以下のように実装する
    - URIを?で区切った後ろの部分を取得する
    - 空でなければさらに &で区切る
    - 更に=で区切り、それぞれを URLデコードする

    View Slide

  24. ルーティング
    - 今回はユーザーが以下のように使えるように実装する
    - get("/hoge") { … }, post("/fuga") { … } のように、メソッドごとに関数が用意されていて、それ に
    パスを指定し、ラムダ関数の中で実際にリクエストを受け取ったときの処理を書く
    - 次のように実装する
    - ユーザーが get("/hoge") { … } のように関数を呼び出すとハンドラが追加される
    - ソケットから受け取った TCPのリクエストをHTTPのリクエストとして解釈する
    - 受け取ったリクエストの URIをもとにどのハンドラで処理するべきか評価し、最も評価度が高いハン
    ドラに実際に処理をさせる
    - 処理の結果ユーザーが生成したレスポンスのインスタンスを実際に HTTPのレスポンスとしてTCP
    のソケットに書き込む

    View Slide

  25. ハンドラの実装
    - ハンドラ自体はパスとHTTPメソッドと処理をする関数を持つだけ
    - ユーザーがHTTPのリクエストに対応した関数を呼び出すとハンドラのリストに追加
    されていく

    View Slide

  26. リクエストをハンドラにマッチさせる
    - 同じリクエストでも複数のハンドラにマッチする可能性がある
    - /hoge/fuga は /hoge/fuga, /hoge/:param, /hoge/* にマッチする可能性がある
    - マッチ度が高いのは /hoge/fuga > /hoge/:param > /hoge/*
    - これを実際にリクエストが来るたびに行う
    - ハンドラのパスを/で区切り、それぞれが定数、パスパラメータ、ワイルドカードかど
    うかによってそのハンドラがマッチした場合の評価度を計算する
    - パスパラメータを持つ場合は実際にそれを取り出す
    - ソースコードは結構長くなったのでGitHubまで見に来て

    View Slide

  27. 使う側はこんな感じで使える

    View Slide

  28. 実際のソースコードはこちら
    - yt8492/NativeServer
    https://github.com/yt8492/NativeServer

    View Slide

  29. まとめ
    - ソケット通信からサーバーフレームワークの実装できた
    - RFCを読みながら実装するとかなり勉強になる
    - 自分の知らなかった仕様に出会える
    - 実際に動くと感動

    View Slide

  30. みんなも車輪の再発明しよう!
    楽しいよ!

    View Slide