
まずは、golangの標準パッケージnetパッケージを触っておきます。
netパッケージのListen APIを利用して、サーバー側でTCP通信を検知する姿勢を準備しておきます。
TCPコネクションリクエストが来ない限りは、Acceptのところにブロックされてます。
netパッケージのDial APIを利用して、サーバー側とTCP通信を設立します。
そして、設立されたTCPコネクションにリクエストを投げたり、サーバーからのレスポンスを受け取ったりします。
サーバー側には2点問題が残っています
リクエストを処理したら、すぐ終了になります、それはサーバーの役割にあっていないです。永遠に回しつけられるように改善する必要があります。
サーバー側はクライアントからの複数リクエストに対して、順番で処理しています。
効率よくさせるため、並列処理に改善します。
TCP通信の設立はコストかかりますので、なるべく一回のTCP通信を設立してから、複数回の相互通信を行いたいために、以下のようにコードの改善を行う必要があります。
サーバー側で、TCP設立したあと、for分を回して処理し続けるようにします。
サーバー側で、クライアント側からcloseの指示が来て初めて、設立したTCPのコネクションを切断します。
http.HandleFuncを呼ぶことによって、グローバル変数のDefaultServeMuxを弄ることになります。
DefaultServeMuxはServeMux型のストラクチャーのポインターです。
ハンドラーを登録するのはDefaultServeMuxにあるmという属性の拡張することになります。
mという属性はmap型のデータです。keyはstring型,valueはmuxEntry型のデータです。
muxEntryにはHandler型のh属性は保持しています。
hはmain関数から渡しているハンドラー関数を型変換にしたものです。
クライアントからリクエストが来たら、まずTCPのコネクションを設立します。
コネクションが設立した上で、リクエストを受け取ります。
受け取ったリクエスト情報にあるRequestURIをもとに、グローバル変数であるDefaultServeMuxから該当するハンドラーを取り出して、実行します。
ハンドラーありの使い方とは、ListenAndServeの第二引数にnilではなく、ハンドラーを渡されたものです。
ハンドラーはmain関数から渡されてきましたので、リクエストが来た時に、DefaultServeMuxを利用しなく、直接main関数にあるハンドラーのServeHttpメソッドを呼びます。
net/httpパッケージを利用して、httpサービスを立ち上げるためには、二つの方法があります
1. ハンドラーなしの方法
2. ハンドラーありの方法
今まではの開発は二つのデメリットが存在しています。
ビジネスロジックとunderlyingの技術ロジックは同じスコープに混じっています。
ビジネスロジックから直接underlying技術ロジックのAPIを使用しています、不便さは存在しています。
たとえば、以下の要望が来ました。
「/students?name={name}というAPIを用意して、json型のレスポンスを返してください。」
serveHTTP関数にif分を追加したり、httpパッケージのAPIを利用したら、標準ライブラリを利用したりする必要があります。
ビジネスロジックを膨大になると、serveHTTPも膨大になり、開発に負担をかけてしまいます。
しかもRestAPIになる動的なルーティングの対応は不便になります。
ビジネスロジックと技術ロジックを分離するために、真ん中にフレームワーク層を入れます。
今回はそのフレームワーク層の雛形を整えてみます。
ServeHTTPは各リクエストに対して、該当するコントローラー(ハンドラー)を探し出すために、if分を追加する必要があります。もしこれからAPIを追加する時になったら、if分をさらに追加する必要があります。
しかし、理想的な形は、HTTPのメソッド名とpathnameから該当するコントローラー(ハンドラー)を導くロジックは、フレームワーク層で自動化するべきなものです。
今回ルーティングテーブルを用意して、ServeHTTPから自動的にHTTPメソッドとpathnameに該当するコントローラー(ハンドラー)を取り出します。
静的なルーティングテーブルには限界があります。
RestAPIは動的なURLは数千、数万件になります。それを全部MAP型のルーティングテーブルに登録する必要になります。
もし、このようにしたら、ルーティングテーブルは膨大になり、登録する側のmain関数も煩雑になります。
その問題を解決するのは動的なルーティングが登場します。
動的なルーティング仕組みは利用するデーター構造はMAP型のデータではなく、ツリー構造です。
トライツリーは単語を探す出すためのアルゴリズムです。
単語をアルファベットごとに区切って、一個一個のノードとして入れています。
最後のアルファベットをほかのアルファベットと区別しています。
leetcodeの典型的なトライツリーの問題集を解きます
今回はトライツリーを使って、今までのハッシュマップ型のルーティングテーブルをトライツリー型のルーティングテーブルに差し替えていきます。
例えば、以下のような動的なparamsが存在するpathnameがあります。
/lists/1
/lists/123
/users/c-ronalda
/students/messi
すべての可能性があるpathnameをトライツリーに登録していくわけにはいかないです。
それらの潜在的なpathnameを対応するために、以下のようにトライツリーに登録します。
/lisrs/:list_item_id
/users/:user_id
/students/:student_id
#### 背景
serveHTTPの膨大化になるリスクは二つの原因があります
- ルーティングは分岐によって、膨大化になります
- ビジネスロジックを処理するため、http.ResponseWriterを弄るのは手間かかる作業です
#### 解決
- framework層を切り出します。framework層でやることは
- 起動処理
- ルーティング処理
- 静的なルーティング(ハッシュマップ)
- 動的なルーティング(トライツリー)
-
- ビジネスロジック層を切り出します。
- net/http細かいAPI操作はビジネスロジック層で行います。
net/httpパッケージを細かく利用する粒度が高いAPIををラッピングして、カスタマイズしたcontextに入れます。
controller層はラッピングしたAPIを利用します。
RestAPIのURLにparamは動的に変動されてます。
それらの動的なparamを取得するために、contextからAPIを用意しておきます。
HTTPメソッドはGETだけではなく、ほかによく使われるHTTPメソッドはPOST,PATCH,PUT,DELETEもあります。
それらのHTTPメソッドを対応するために、それぞれのHTTPメソッドごとにトライツリーを用意します。
net/httpパッケージはformを解析するAPIをすでに用意してくれましたが、もっと使いやすくように改善します。
BindJSONをContextから用意します。
標準のtemplateパッケージを利用して、htmlファイルからhtmlのレスポンスを返します。
cross origin対策の一つとしてあるのはJSONPです。コントローターからJSONPを利用しやすくためのContextにJSONPのAPIを用意します。
フレームワークの独自のContextが値を管理するようにMap型のkeysを用意しました。
並列処理でも安全に取り扱うため、RWMutexを導入します。
ビジネスロジックの時間が長くなったら、timeoutのレスポンスを返します。timeoutの仕組みを作るためには、goの標準パッケージcontextを利用します。
サブgoroutineでcontrollerを実行しているため、panicが起こってしまう場合には、サーバーが止まりますので、そうならないように、panicハンドリングを入れます。
静的なファイルもルーティングできるように、http.FileServerを利用して、handlerを作ります。
/lists/:list_idにtimeoutのmiddlewareを適応しましたが、/listsにもtimeoutのmiddlewareを適応したい場合は、一旦timeoutのmiddlewareを切り出して、他のhandlerも利用できるようにします。
たくさんのmiddlewareを追加する場合は、純粋なmiddlewareを保持できません。
middlewareの中に関心するものは、middleware自身のロジックと次のmiddlewareを実行させることだけです。
ServeHTTPの中に全てのmiddlewareを持っていくと、以下のようなデメリットが生じます。
・登録したmiddlewareがすべてのリクエストに適用されてしまう。
・新しいmiddlewareを追加する場合、ServeHTTPを変更する必要がある。
これらの問題を回避するには、middlewareをそれぞれのツリーノードのhandler関数にラッピングすることが必要です。
課題
各handlerにmiddlewareを適用する必要がある。
Static file handlerはツリーノードに属していないため、middlewareとして分離する場合、どこで分離するかが問題となります。
middleとhandlerは実質的には同じタイプの関数であり、まとめてスライスの中に入れておくことは可能です。
middlewaresを各treeNodeに登録する場合、冗長な重複処理が発生しやすくなります。これを回避するためには、middlewaresを共通の領域に登録し、serveHttpの時にスライスにまとめて、contextに格納していきます。また、contextのNext APIを用意して、スライスにまとめたmiddlewaresとhandlerを順番に実行していくことで、重複処理を回避できます。
現在、timeCost middlewareとtimeout middlewareが機能していない原因は、これらのmiddlewareが次のmiddlewareを呼び出していないことにあります。そのため、Next APIを改造して、middlewareから次のmiddlewareを呼び出す必要があります。
middlewareとしてNotFoundを作成することができます。もしtreeNodeが見つからない場合は、あらかじめ用意したnot foundを最後のhandlerとしてhandlersスライスに追加します。
静的なファイルの処理をServeHTTPから分離し、middlewareとして作成することができます。もしpathnameが静的なファイルに該当する場合、以降のmiddleware処理を実行する必要がない場合があります。その場合は、Abort APIを呼び出すことで、以降のmiddleware処理を中断します。
サーバーをシャットダウンする際に、処理が完了していないリクエストがある可能性があります。
その場合には、これらのリクエストを全て処理し終わった後、サーバーをシャットダウンさせる必要があります。
このコースは以下の流れで進みます。
まず、Goの標準ライブラリであるnet/httpについて説明します。netパッケージを使用して、tcp通信のサーバー側とクライアント側を実装します。その後、net/httpパッケージを使用してhttp通信のサーバー側を実装します。
http通信のサーバー側の実装方法は、ハンドラーなしの方法とハンドラーありの方法があります。しかし、Rest APIに対応するために、ハンドラーありの方法を使用することにします。
ただし、net/httpパッケージはハンドラーありの方法に完全に対応できないため、カスタマイズしたフレームワークを作成する必要があります。カスタマイズしたフレームワークは、主に以下の3つの部分で構成されます。
リクエストURIからコントローラー(ハンドラー)までルーティングする仕組み。
Contextを用意して、便利なAPIを提供すること。
Middlewareチェーンを用意して、コントローラー(ハンドラー)実行前の前処理仕組み。
このコースを学ぶことで、以下のことができるようになります。
トライツリーアルゴリズムが実際の生産上でどのように使用されるかを理解することができます。
普段開発で使用しているGo言語のウェブフレームワークの仕組みをある程度理解できます。
普段開発で使用しているGo言語のウェブフレームワークの内部ソースコードを読み解く勇気を身につけることができます。
面接でウェブフレームワークの仕組みについて深く語ることができるようになります。
このコースでは、Go1.19を使用して説明します。