GoのHTTPクライアントの実装
GoのおけるHTTPクライアントの実装をまとめていきます。
- How to issue HTTP request
- Implementation
- type Client
- 1-1. func http.Get
- 1-2. Clinet.Get
- 2-1. Clinet.Do
- 2-2. Clinet.do
- 2-2. Clinet.send
- Transport 構造体の RoundTrip の実装
- 3-1. Transport.RoundTrip
- 3-2. Transport.roundTrip
- 3-3. persistConn.roundTrip
- 4-1. Transport.getConn
- 4-2. Transport.queueForDial
- 4-3. Transport.dialConnFor
- 4-4. Transport.dialConn
- 4-5. persistConn.readLoop
- 4-6. persistConn.readResponse
- 4-7. ReadResponse
- 参考
How to issue HTTP request
クライアントの実装はいくつかありますが、いずれもプリミティブなメソッドは同じです。高レベルの関数はプリミティブへのラッパーです。
1. http.Get を用いる
http.Get を用いる方法です。最も簡易的な HTTP のリクエストです。内部的にはリクエストに必要な構造体はデフォルトの値を用います。
package main import ( "fmt" "io/ioutil" "net/http" ) func main() { resp, err := http.Get("http://example.com/") if err != nil { panic(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(string(body)) }
2. http.Client を用いる
次にクライアントを作る方法です。HTTPクライアントヘッダーやリダイレクトポリシー、その他の設定を制御するには http.Client
を作成します。例えばリダイレクトしないようなHTTPクライアントにする場合は以下のようになります。
2-1. client.Get を用いる
http.Client でクライアントを作成し、client.Get で GET リクエストを発行します。
package main import ( "fmt" "io/ioutil" "net/http" ) func main() { client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } resp, err := client.Get("http://google.com") if err != nil { panic(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(resp.Status) fmt.Println(string(body)) }
301 Moved Permanently <HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8"> <TITLE>301 Moved</TITLE></HEAD><BODY> <H1>301 Moved</H1> The document has moved <A HREF="http://www.google.com/">here</A>. </BODY></HTML>
2-2. http.NewRequest を用いる
http.NewRequest を用いてリクエストを発行することもできます。
package main import ( "fmt" "io/ioutil" "net/http" ) func main() { client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } req, err := http.NewRequest("GET", "http://example.com", nil) if err != nil { panic(err) } req.Header.Add("If-None-Match", `W/"wyzzy"`) resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(resp.Status) fmt.Println(string(body)) }
2-3. http.Transport を用いる
プロキシ、TLS構成、キープアライブ、圧縮、およびその他の設定を制御するには、トランスポートを作成します。最もプリミティブな実装です。
package main import ( "fmt" "io/ioutil" "net/http" "time" ) func main() { tr := &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, DisableCompression: true, } client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Transport: tr, } req, err := http.NewRequest("GET", "https://example.com", nil) if err != nil { panic(err) } req.Header.Add("If-None-Match", `W/"wyzzy"`) resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(resp.Status) fmt.Println(string(body)) }
クライアント(http.Client)とリクエスト(http.Request)とリクエストを実際に発行する(client.Do)という構造が見えると見通しが良くなると思います。http.Get ではこれらをデフォルトのクライアント(DefaultClient) を用いてよしなにリクエストを発行しているのです。
Implementation
まずは公式ドキュメントに記載されている内容を見てみます。
type Client
- クライアントのトランスポートはTCPのキャッシュがあるため、クライアントは再利用すべき
- クライアントはゴルーチンセーフ
- RoundTripperよりもClientは高レイヤー
- CookieやリダイレクトなどのHTTPの詳細を処理
- リダイレクトは、最初のリクエストで設定されたすべてのヘッダーを転送するが以下の場合は除く
type CookieJar interface { // SetCookies handles the receipt of the cookies in a reply for the // given URL. It may or may not choose to save the cookies, depending // on the jar's policy and implementation. SetCookies(u *url.URL, cookies []*Cookie) // Cookies returns the cookies to send in a request for the given URL. // It is up to the implementation to honor the standard cookie use // restrictions such as in RFC 6265. Cookies(u *url.URL) []*Cookie }
- Go が提供している CookieJar の実装は https://godoc.org/net/http/cookiejar を参照
- クッキーの仕様は RFC 6265 を参照
1-1. func http.Get
一番最初に見た Get 関数を見てみます。
func Get(url string) (resp *Response, err error) { return DefaultClient.Get(url) }
デフォルトクライアントは以下のように宣言されている http.Client 構造体です。Client の設定値にはゼロ値です。
var DefaultClient = &Client{}
1-2. Clinet.Get
Client 型の Get メソッドは以下のようになっています。
func (c *Client) Get(url string) (resp *Response, err error) { req, err := NewRequest("GET", url, nil) if err != nil { return nil, err } return c.Do(req) }
1-2-1. http.NewRequest
NewRequest は Request 型を返す関数で、以下のように NewRequestWithContext へのラッパーになっています。
// NewRequest wraps NewRequestWithContext using the background context. func NewRequest(method, url string, body io.Reader) (*Request, error) { return NewRequestWithContext(context.Background(), method, url, body) }
1-2-2. http.NewRequestWithContext
NewRequestWithContext はメソッド、URL、オプションのbodyを与えられた新しいRequestを返します。リクエストを (http.Request) を生成します。
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) { // ... req := &Request{ ctx: ctx, Method: method, URL: u, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: make(Header), Body: rc, Host: u.Host, } if body != nil { // ... } return req, nil }
2-1. Clinet.Do
Do は、クライアントで設定されたポリシー (リダイレクト、クッキー、認証など) に従った HTTP リクエストを送信し、HTTP レスポンスを返します。
NewRequest でリクエストを生成した後 c.Do(req)
としてリクエストを実行しています。透過的に非公開の do メソッドを呼び出しています。実際のリクエストは RoundTripper というインターフェースが処理します。
RoundTripper は、単一の HTTP トランザクションを実行し、指定されたリクエストのレスポンスを取得する機能を表すインターフェイスです。
func (c *Client) Do(req *Request) (*Response, error) { return c.do(req) }
2-2. Clinet.do
func (c *Client) do(req *Request) (retres *Response, reterr error) { // ... for { // ... reqs = append(reqs, req) var err error var didTimeout func() bool if resp, didTimeout, err = c.send(req, deadline); err != nil { // c.send() always closes req.Body reqBodyClosed = true if !deadline.IsZero() && didTimeout() { err = &httpError{ // TODO: early in cycle: s/Client.Timeout exceeded/timeout or context cancellation/ err: err.Error() + " (Client.Timeout exceeded while awaiting headers)", timeout: true, } } return nil, uerr(err) } var shouldRedirect bool redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0]) if !shouldRedirect { return resp, nil } req.closeBody() } }
2-2. Clinet.send
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { // ... resp, didTimeout, err = send(req, c.transport(), deadline) if err != nil { return nil, didTimeout, err } if c.Jar != nil { if rc := resp.Cookies(); len(rc) > 0 { c.Jar.SetCookies(req.URL, rc) } } return resp, nil, nil }
// send issues an HTTP request. // Caller should close resp.Body when done reading from it. func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { req := ireq // req is either the original request, or a modified fork // ... resp, err = rt.RoundTrip(req) if err != nil { stopTimer() if resp != nil { log.Printf("RoundTripper returned a response & error; ignoring response") } if tlsErr, ok := err.(tls.RecordHeaderError); ok { // If we get a bad TLS record header, check to see if the // response looks like HTTP and give a more helpful error. // See golang.org/issue/11111. if string(tlsErr.RecordHeader[:]) == "HTTP/" { err = errors.New("http: server gave HTTP response to HTTPS client") } } return nil, didTimeout, err } if !deadline.IsZero() { resp.Body = &cancelTimerBody{ stop: stopTimer, rc: resp.Body, reqDidTimeout: didTimeout, } } return resp, nil, nil }
最終的に RoundTripper インターフェースの RoundTrip メソッドを呼び出していることが分かりました。このようにしてリクエストのクライアントとして定義した RoundTripper インターフェースを実装する構造体(通常は Transport)によって振る舞いを変えられるのが Go のインターフェースのお手本みたいな実装です。(Go だけに限らず一般的なインターフェースの実装ですが)
Transport 構造体の RoundTrip の実装
Transport 構造体の概要をドキュメントから確認してみます。
- HTTP と HTTPS を扱うプリミティブ
- HTTP, HTTPS, HTTP Proxy をサポートする RoundTripper の実装
- 接続をキャッシュする
- CloseIdleConnections メソッドで管理する
- MaxIdleConnsPerHost, DisableKeepAlives で設定可能
- Transport は再利用するべき
- ゴルーチンセーフ
- スキーマが HTTP の場合は HTTP/1.1 を使う、HTTPS の場合は HTTP/1.1 or HTTP/2 のいずれかを使う
- DefaultTransport は HTTP/2 をサポートしている
- ネットワークエラーが発生した場合のみリトライ
以下のように実装されています。
3-1. Transport.RoundTrip
func (t *Transport) RoundTrip(req *Request) (*Response, error) { return t.roundTrip(req) }
3-2. Transport.roundTrip
// roundTrip implements a RoundTripper over HTTP. func (t *Transport) roundTrip(req *Request) (*Response, error) { // ... for { select { case <-ctx.Done(): req.closeBody() return nil, ctx.Err() default: } // treq gets modified by roundTrip, so we need to recreate for each retry. treq := &transportRequest{Request: req, trace: trace} cm, err := t.connectMethodForRequest(treq) if err != nil { req.closeBody() return nil, err } // 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) if err != nil { t.setReqCanceler(req, nil) req.closeBody() return nil, err } var resp *Response if pconn.alt != nil { // HTTP/2 path. t.setReqCanceler(req, nil) // not cancelable with CancelRequest resp, err = pconn.alt.RoundTrip(req) } else { resp, err = pconn.roundTrip(treq) } if err == nil { return resp, nil } // Failed. Clean up and determine whether to retry. // ... } }
3-3. persistConn.roundTrip
pconn.alt の有無によって HTTP/2 でリクエストするのか HTTP/1.1 でリクエストするのか分かれています。
persistConn 構造体はコネクションをラップしています。
func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) { // ... // Write the request concurrently with waiting for a response, // in case the server decides to reply before reading our full // request body. startBytesWritten := pc.nwrite writeErrCh := make(chan error, 1) pc.writech <- writeRequest{req, writeErrCh, continueCh} resc := make(chan responseAndError) pc.reqch <- requestAndChan{ req: req.Request, ch: resc, addedGzip: requestedGzip, continueCh: continueCh, callerGone: gone, } var respHeaderTimer <-chan time.Time cancelChan := req.Request.Cancel ctxDoneChan := req.Context().Done() for { testHookWaitResLoop() select { // ... case re := <-resc: if (re.res == nil) == (re.err == nil) { panic(fmt.Sprintf("internal error: exactly one of res or err should be set; nil=%v", re.res == nil)) } if debugRoundTrip { req.logf("resc recv: %p, %T/%#v", re.res, re.err, re.err) } if re.err != nil { return nil, pc.mapRoundTripError(req, startBytesWritten, re.err) } return re.res, nil case <-cancelChan: pc.t.CancelRequest(req.Request) cancelChan = nil case <-ctxDoneChan: pc.t.cancelRequest(req.Request, req.Context().Err()) cancelChan = nil ctxDoneChan = nil } } }
resc
チャネルからの値を受信することでレスポンスを取得します。resc
はどこから値が送信されるか確認します。roundTrip メソッドの getConn を追っていくと分かります。
4-1. Transport.getConn
Transport.getConn によって persistConn を生成します。queueForIdleConn でアイドルの接続の有無を確認している(?)ようです。
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) { // ... // Queue for idle connection. if delivered := t.queueForIdleConn(w); delivered { pc := w.pc // Trace only for HTTP/1. // HTTP/2 calls trace.GotConn itself. if pc.alt == nil && trace != nil && trace.GotConn != nil { trace.GotConn(pc.gotIdleConnTrace(pc.idleAt)) } // 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 } // ... t.queueForDial(w) // ... }
4-2. Transport.queueForDial
queueForDial で wantConn という構造体を取得します。このあたりで Transport の MaxConnsPerHost の設定が効いています。MaxConnsPerHost は、ダイヤル状態、アクティブ状態、およびアイドル状態の接続を含む、ホストごとの総接続数を制限します。制限に違反した場合、ダイヤルはブロックされます。 ゼロは制限なしを意味します。
func (t *Transport) queueForDial(w *wantConn) { if t.MaxConnsPerHost <= 0 { go t.dialConnFor(w) return } // ... }
4-3. Transport.dialConnFor
func (t *Transport) dialConnFor(w *wantConn) { defer w.afterDial() pc, err := t.dialConn(w.ctx, w.cm) // ... }
4-4. Transport.dialConn
ここで persistConn を生成し、以下のようにゴルーチンを起動して、readLoop することで書き込んでいます。
func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) { pconn = &persistConn{ t: t, cacheKey: cm.key(), reqch: make(chan requestAndChan, 1), writech: make(chan writeRequest, 1), closech: make(chan struct{}), writeErrCh: make(chan error, 1), writeLoopDone: make(chan struct{}), } trace := httptrace.ContextClientTrace(ctx) // ... pconn.br = bufio.NewReaderSize(pconn, t.readBufferSize()) pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize()) go pconn.readLoop() go pconn.writeLoop() return pconn, nil }
4-5. persistConn.readLoop
readLoop では readResponse で生成したレスポンスを chan requestAndChan 型である rc 変数のチャネル ch にレスポンスを書き戻しています。
func (pc *persistConn) readLoop() { // ... alive := true for alive { // ... var resp *Response if err == nil { resp, err = pc.readResponse(rc, trace) } else { err = transportReadFromServerError{err} closeErr = err } // ... if !hasBody || bodyWritable { // ... if bodyWritable { closeErr = errCallerOwnsConn } select { case rc.ch <- responseAndError{res: resp}: case <-rc.callerGone: return } // Now that they've read from the unbuffered channel, they're safely // out of the select that also waits on this goroutine to die, so // we're allowed to exit now if needed (if alive is false) testHookReadLoopBeforeNextRead() continue } // ... resp.Body = body if rc.addedGzip && strings.EqualFold(resp.Header.Get("Content-Encoding"), "gzip") { resp.Body = &gzipReader{body: body} resp.Header.Del("Content-Encoding") resp.Header.Del("Content-Length") resp.ContentLength = -1 resp.Uncompressed = true } select { case rc.ch <- responseAndError{res: resp}: case <-rc.callerGone: return } } }
4-6. persistConn.readResponse
readResponse はサーバから HTTP レスポンスを読み込みます。最終的に100以外のものを返します。
func (pc *persistConn) readResponse(rc requestAndChan, trace *httptrace.ClientTrace) (resp *Response, err error) { if trace != nil && trace.GotFirstResponseByte != nil { if peek, err := pc.br.Peek(1); err == nil && len(peek) == 1 { trace.GotFirstResponseByte() } } num1xx := 0 // number of informational 1xx headers received const max1xxResponses = 5 // arbitrary bound on number of informational responses continueCh := rc.continueCh for { resp, err = ReadResponse(pc.br, rc.req) if err != nil { return } // ... break } // ... resp.TLS = pc.tlsState return }
4-7. ReadResponse
ReadResponseは、rからHTTPレスポンスを読み込んで返します。 reqパラメータは、オプションでこのレスポンスに対応するRequestを指定します。nilの場合は、GETリクエストを想定しています。クライアントは resp.Body の読み込みが終了したら resp.Body.Close を呼び出す必要があります。その呼び出しの後、クライアントは resp.Trailer を検査して、レスポンス・トレーラに含まれるキー/値のペアを見つけることができます。
最終的にこの ReadResponse 関数に行きつきます。textproto.NewReader を使ってレスポンスを読み込みながら、レスポンスを生成しています。読み込んだ文字列を RFC7230 の形式によってパースしていることが分かります。
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) { tp := textproto.NewReader(r) resp := &Response{ Request: req, } // Parse the first line of the response. line, err := tp.ReadLine() if err != nil { if err == io.EOF { err = io.ErrUnexpectedEOF } return nil, err } if i := strings.IndexByte(line, ' '); i == -1 { return nil, &badStringError{"malformed HTTP response", line} } else { resp.Proto = line[:i] resp.Status = strings.TrimLeft(line[i+1:], " ") } statusCode := resp.Status if i := strings.IndexByte(resp.Status, ' '); i != -1 { statusCode = resp.Status[:i] } if len(statusCode) != 3 { return nil, &badStringError{"malformed HTTP status code", statusCode} } resp.StatusCode, err = strconv.Atoi(statusCode) if err != nil || resp.StatusCode < 0 { return nil, &badStringError{"malformed HTTP status code", statusCode} } var ok bool if resp.ProtoMajor, resp.ProtoMinor, ok = ParseHTTPVersion(resp.Proto); !ok { return nil, &badStringError{"malformed HTTP version", resp.Proto} } // Parse the response headers. mimeHeader, err := tp.ReadMIMEHeader() if err != nil { if err == io.EOF { err = io.ErrUnexpectedEOF } return nil, err } resp.Header = Header(mimeHeader) fixPragmaCacheControl(resp.Header) err = readTransfer(resp, r) if err != nil { return nil, err } return resp, nil }