先日、というか昨日、この資料が流れてきまして、Private Networkの外部との通信を効率良く行うためのミドルウェア、choconというproxyサーバーが紹介されていました。SSL, HTTP/2を加味した上での超シンプルで高速なforward proxyサーバー実装という印象です。
使い方やAPIの叩き方は上記のリンクを参考にしていただくとして、やたらマイクロな実装でなぜこうも高速に、コネクションプールしたサーバーが動いてるのか、気になって読み進めていました。
調べていくうちに、思っているよりはるかに自分がnet/httpの実装について知らなかったことに驚愕したので、メモがてら書いておきます。(kazeburoさんでもないのに偉そうなタイトルをつけましたが、他にいいタイトルが思いつきませんでした。)
choconのリポジトリ
下準備
choconを読むにあたって微妙に下調べが必要なのが以下の要素です。
MITM proxyについて
何も考えずproxy serverを実装しても、HTTPの通信しかサポートできず、HTTPSの通信については、以下のように仲介者としてProxyサーバーが暗号化される通信内容を強引に解析しなければいけません。
詳しくは以下の参考資料とか読んでみると良いかもしれません。 ネイティブクライアントを開発してる人で、mitmproxyを使ってる人はそれをイメージすると良いのかもしれません。
choconではその点、単なるHTTPだけのforward proxyではなく、Hostヘッダにオプションをつけて、「HTTPS通信を行うよ」という宣言をすれば、平易にHTTPSを含めたproxyリクエストを投げることができます。
なので、MITM proxyを挟まなくても、HTTPS, HTTP/2の通信をproxyしてもらえる点が、forward proxyとしてのchoconのメリットの一つになります。
Golangのhttp.Client, RoundTripper, Transportの関係について
ざっくりと、GolangでHTTPリクエストを送信するとき、重要な構成要素として
- http.RoundTripper
- http.Transport
- http.Client
という3つがあります。
http.RoundTripper
は、HTTPのトランザクションを実行し、req/resを処理するためのインターフェースです。
http.Transport
は、上記のhttp.RoundTripper
を実装した構造体で、通信に必要な設定と、RoundTripメソッドの実装を持ちます。
通信処理を実際に行う実装はhttp.Transport
が保有することになります。 デフォルトでKeep-Aliveが有効で、接続が再利用・キャッシュされるようにできています。
最後に、http.Client
ですが、http.Transport
を内部に持った、HTTPプロトコルでの通信をおこなうための素のクライアント実装です。
http.Client
はDo
という、リクエストを処理するためのAPIを用意していますが、これは内部で以下のようにhttp.RoundTripper(の実装であるTransport)
のRoundTrip
メソッドを呼んでいます。
つまり、実際のリクエストを処理しているのはRoundTrip
というメソッドであり、HTTP以外のプロトコルでの通信や、proxyサーバーだったりの実装をするときは、http.Client
を介さずにTransport
を拡張して通信を行えばよいことになります。
逆に、HTTP以外のプロトコルでの通信を意図しても、デフォルトでは以下で弾かれることになります。
以下加えて参考になった資料です。
choconの実装
choconの内部実装を読むと、
- 投げられたHTTPリクエストを意図した形式でコピーする
- 通信をキャッシュして効率良く外部にリクエストを送るためのTransportのオブジェクトを用意する
http.Handler
インターフェースの実装を用意し、ServeHTTP
の中でリクエスト内容のコピー・変換・処理したresponse Bodyのコピーおよび返却等を行う- Log出力を行う
などをしている。
投げられたHTTPリクエストを意図した形式でコピーする
このrequestConverter
は、Hostヘッダに指定されたオプションから、httpとhttpsのどちらで通信を行うかの決定、実際にリクエストを投げる先のHostの設定を行う。
httpsを使う場合、HTTP/2が使える場合はクライアントとして通常のhttpsとどちらを用いるか、Goが内部でよしなに切り替えてくれる。
通信をキャッシュして効率良く外部にリクエストを送るためのTransportのオブジェクトを用意する
Transportのオブジェクト自体はシンプルで、多少オプションに応じてパラメーターが変化する程度。
http.Handler
インターフェースの実装を用意し、ServeHTTP
の中でリクエスト内容のコピー・変換・処理したresponse Bodyのコピーおよび返却等を行う
proxyサーバーとしてhttpリクエストを処理するには、Serveメソッドを呼ばなければなりませんが、そのためにはhttp.Handler
を実装する必要があります。 choconでいうと、Proxy
という構造体がそれにあたります。
これが、ServeHTTP
を実装しており、中ではoriginalのHTTPリクエストから必要な値をコピーして、Proxy構造体が持つTransport
に渡して、レスポンスオブジェクトを受け取り、返却します。
毎度新しくhttp.Client
を生成しているわけではないので、Proxyが持つTransportを通じ、通信をキャッシュ・再利用して、2回目以降のリクエストを高速に処理することができます。
Log出力を行う
Serveメソッドを実際に持ち、Proxyサーバーに向けられたリクエストを真っ先に処理するAPIはhttp.Server
ですが、このLogHandlerにhttp.Handler
を渡し、実際にServeする用のhttp.Server
を返却しています。
ログ出力を行うためのミドルウェアの実装はこちら。
まとめ
以上のように、自分なりにchoconの実装を読んでみたわけですが、net/httpの内部実装をちゃんと追わないと、全然何やってるかわからなかったので、これを機に中身を追うことができてよかったです。
高速な通信を実現するためのコネクションを容易に使いまわせるという点で、改めてGoの書きやすさと速度のバランスは素晴らしいと感じました。
また、githubのコードへのリンクを貼り付けたことで一生分のkazeburoさんのProfile画像を目にすることになりました。生のkazeburoさんにお会いしたことはありません。現場からは以上です。
※上記のコードリーディングの過程・結果が誤っている場合はご指摘いただけますと幸いです。