Carpe Diem

備忘録

GolangのRoundTripperとTransport

背景

僕が保守している go-iapというgolangで書いたAppStore, GooglePlay AmazonAppStore用の課金ライブラリ があるのですが、そこに以下のissueがあがっていました。

github.com

ざっくり説明すると「GAEでは普通のhttp.Client使えないからカスタムClientサポートして」という内容で、AWAではAWSがメインでGAEからこれを使うことがなかったため検証もできず、誰か対応するプルリクくれないかなぁとずっと放置して待っていました。

これを最近対応したのですが、その時にRoundTripperTransportについて学んだのでまとめます。

環境

http.Client.Doの仕組み

http.Client.Doは以下の図のようになっています。

f:id:quoll00:20180518125301j:plain

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
  • データ圧縮

などなど、様々な設定が可能です。

github.com

RoundTrip

またこのTransportが実装しているRoundTrip()は以下のようになっています。

github.com

接続を再利用するロジックがあり

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を使用するようにドキュメントに書かれています

github.com

http.Transportと違い、ContextAllowInvalidServerCertificateという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もロジックが異なることが分かります。

github.com

ここの処理のように、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以外のプロトコルなどにも拡張できる

ということでした。

ソース