kazeburo/choconと、それを支えるnet/httpの実装について

先日、というか昨日、この資料が流れてきまして、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.ClientDo という、リクエストを処理するための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さんにお会いしたことはありません。現場からは以上です。

※上記のコードリーディングの過程・結果が誤っている場合はご指摘いただけますと幸いです。