背景
僕が保守している go-iapというGoで書いたAppStore, GooglePlay AmazonAppStore用の課金ライブラリ があるのですが、そこに以下のissueがあがっていました。
ざっくり説明すると「GAEでは普通のhttp.Client使えないからカスタムClientサポートして」という内容で、AWAではAWSがメインでGAEからこれを使うことがなかったため検証もできず、誰か対応するプルリクくれないかなぁとずっと放置して待っていました。
これを最近対応したのですが、その時にRoundTripper
やTransport
について学んだのでまとめます。
環境
- golang 1.10.2
http.Client.Do
の仕組み
http.Client.Do
は以下の図のようになっています。
ref: Timeout in Go net/http client
これを見るとClientはTransportのRoundTrip()
というメソッドを呼ぶことでリクエストを送っていることが分かります。
RoundTripperとTransport
項目 | 説明 |
---|---|
http.RoundTripper |
単一のHTTPトランザクションを実行しのreq/resを処理するためのインターフェース |
http.Transport |
http packageでhttp.RoundTripper を実装した構造体 |
つまりhttp.Clientは、中のRoundTripperというインタフェースを実装したTransportを埋め込むことで通信の最適化であったり、背景で述べたGAE対応などができるわけです。
http.Transport
Transport
デフォルトのhttp.Transport
は色んなケースに対応できるよう、
- dial
- proxy
- TLS
- keep-alives
- データ圧縮
などなど、様々な設定が可能です。
RoundTrip
またこのTransportが実装しているRoundTrip()
は以下のようになっています。
接続を再利用するロジックがあり
func (t *Transport) RoundTrip(req *Request) (*Response, error) { ... // Get the cached or newly-created connection to either the // host (for http or https), the http proxy, or the http proxy // pre-CONNECTed to https server. In any case, we'll be ready // to send it requests. pconn, err := t.getConn(treq, cm)
ref: go/transport.go at 226651a541286726df30ff067d519f4efd57cec7 · golang/go · GitHub
すでに接続があればそれを利用しますし
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) { ... if pc, idleSince := t.getIdleConn(cm); pc != nil { if trace != nil && trace.GotConn != nil { trace.GotConn(pc.gotIdleConnTrace(idleSince)) } // set request canceler to some non-nil function so we // can detect whether it was cleared between now and when // we enter roundTrip t.setReqCanceler(req, func(error) {}) return pc, nil }
ref: go/transport.go at 226651a541286726df30ff067d519f4efd57cec7 · golang/go · GitHub
なければ新しくdialします。
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) { ... go func() { pc, err := t.dialConn(ctx, cm) dialc <- dialRes{pc, err} }()
ref: go/transport.go at 226651a541286726df30ff067d519f4efd57cec7 · golang/go · GitHub
urlfetch.Transport
Transport
GAEではurlfetchで生成したClientを使用するようにドキュメントに書かれています。
http.Transportと違い、Context
とAllowInvalidServerCertificate
という2つしかフィールドがありません。GAE専用のTransportなので不要な部分は削ぎ落とされているようです。
ちなみにこのContextでrequestのdeadlineなどを設定できます。
func (t *Transport) RoundTrip(req *http.Request) (res *http.Response, err error) { ... if deadline, ok := t.Context.Deadline(); ok { freq.Deadline = proto.Float64(deadline.Sub(time.Now()).Seconds()) }
ref: appengine/urlfetch.go at b9aad5d628b283f265adf8d3557faae187a8d015 · golang/appengine · GitHub
RoundTrip
RoundTripもロジックが異なることが分かります。
ここの処理のように、protocol bufferで扱うための処理などが入っていますね。だから通常のhttp.Transportだと駄目なんでしょう。
func (t *Transport) RoundTrip(req *http.Request) (res *http.Response, err error) { methNum, ok := pb.URLFetchRequest_RequestMethod_value[req.Method] if !ok { return nil, fmt.Errorf("urlfetch: unsupported HTTP method %q", req.Method) } method := pb.URLFetchRequest_RequestMethod(methNum) ...
ref: appengine/urlfetch.go at b9aad5d628b283f265adf8d3557faae187a8d015 · golang/appengine · GitHub
まとめ
- http.Clientの処理の実態はRoundTripperインタフェースを実装したTransport
- RoundTripperを実装すれば通信の最適化であったりHTTP以外のプロトコルなどにも拡張できる
ということでした。