Go 语言网络编程系列(四)—— HTTP 编程篇:http.Client 底层实现剖析
- 2019 年 10 月 31 日
- 筆記
除了上篇教程介绍的基本 HTTP 操作,Go 语言标准库也提供了比较底层的 HTTP 相关库,让开发者可以基于这些库灵活定制 HTTP 服务器并使用 HTTP 服务。
1、http.Client 的数据结构
前面我们已经介绍过,http.Get()
、http.Post()
、http.PostForm()
和 http.Head()
方法其实都是在 http.DefaultClient
的基础上进行调用的。
http.DefaultClient
是 net/http 包提供的 HTTP 客户端默认实现:
// DefaultClient is the default Client and is used by Get, Head, and Post.var DefaultClient = &Client{}
实际上,我们还可以基于 http.Client
自定义 HTTP 客户端实现,在此之前,我们先来看看 Client
类型的数据结构:
type Client struct { // Transport 用于指定单次 HTTP 请求响应的完整流程 // 默认值是 DefaultTransport Transport RoundTripper // CheckRedirect 用于定义重定向处理策略 // 它是一个函数类型,接收 req 和 via 两个参数,分别表示即将发起的请求和已经发起的所有请求,最早的已发起请求在最前面 // 如果不为空,客户端将在跟踪 HTTP 重定向前调用该函数 // 如果返回错误,客户端将直接返回错误,不会再发起该请求 // 如果为空,Client 将采用一种确认策略,会在 10 个连续请求后终止 CheckRedirect func(req *Request, via []*Request) error // Jar 用于指定请求和响应头中的 Cookie // 如果该字段为空,则只有在请求中显式设置的 Cookie 才会被发送 Jar CookieJar // 指定单次 HTTP 请求响应事务的超时时间 // 未设置的话使用 Transport 的默认设置,为零的话表示不设置超时时间 Timeout time.Duration
其中 Transport
字段必须实现 http.RoundTripper
接口,Transport
指定了一次 HTTP 事务(请求响应)的完整流程,如果不指定 Transport
,默认会使用 http.DefaultTransport
这个默认实现,比如 http.DefaultClient
就是这么做的,后面我们会深入探讨 http.DefaultTransport
的底层实现。
CheckRedirect
函数用于定义处理重定向的策略。当使用 HTTP 默认客户端提供的 Get()
或者 Head()
方法发送 HTTP 请求时,如果响应状态码为 30x
(比如 301
、302
等),HTTP 客户端会在遵循跳转规则之前先调用这个 CheckRedirect
函数。
Jar
可用于在 HTTP 客户端中设置 Cookie,Jar
类型必须实现 http.CookieJar
接口,该接口预定义了 SetCookies()
和 Cookies()
两个方法。如果 HTTP 客户端中没有设置 Jar
,Cookie 将被忽略而不会发送到客户端。实际上,我们一般都用 http.SetCookie()
方法来设置 Cookie。
Timeout
字段用于指定 Transport
的超时时间,没有指定的话则使用 Transport
自定义的设置。
2、http.Transport 的底层实现
下面我们通过 http.DefaultTransport
的实现来重点介绍下 http.Transport
,没有显式设置 Transport
字段时,就会使用 DefaultTransport
:
func (c *Client) transport() RoundTripper { if c.Transport != nil { return c.Transport } return DefaultTransport}
DefaultTransport
是 Transport
的默认实现,对应的初始化代码如下:
var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second,}
这里只设置了 Transport
的部分属性,Transport
类型的完整数据结构如下所示:
type Transport struct { ... // 定义 HTTP 代理策略 Proxy func(*Request) (*url.URL, error) // 用于指定创建未加密 TCP 连接的上下文参数(通过 net.Dial()创建连接时使用) DialContext func(ctx context.Context, network, addr string) (net.Conn, error) // 已废弃,使用 DialContext 替代 Dial func(network, addr string) (net.Conn, error) // 创建加密 TCP 连接 DialTLS func(network, addr string) (net.Conn, error) // 指定 tls.Client 所使用的 TLS 配置 TLSClientConfig *tls.Config // TLS 握手超时时间 TLSHandshakeTimeout time.Duration // 是否禁用 HTTP 长连接 DisableKeepAlives bool // 是否对 HTTP 报文进行压缩传输(gzip) DisableCompression bool // 最大空闲连接数(支持长连接时有效) MaxIdleConns int // 单个服务(域名)最大空闲连接数 MaxIdleConnsPerHost int // 单个服务(域名)最大连接数 MaxConnsPerHost int // 空闲连接超时时间 IdleConnTimeout time.Duration // 从客户端把请求完全提交给服务器到从服务器接收到响应报文头的超时时间 ResponseHeaderTimeout time.Duration // 包含 "Expect: 100-continue" 请求头的情况下从客户端把请求完全提交给服务器到从服务器接收到响应报文头的超时时间 ExpectContinueTimeout time.Duration ... }
结合 Transport
数据结构来看下 DefaultTransport
的设置:
- 通过
net.Dialer
初始化 Dial 上下文配置,默认超时时间设置为 30 秒; - 通过
MaxIdleConns
指定最大空闲连接数为 100,未显式设置MaxIdleConnsPerHost
和MaxConnsPerHost
,MaxIdleConnsPerHost
有默认值,通过http.DefaultMaxIdleConnsPerHost
设置,对应缺省值是 2; - 通过
IdleConnTimeout
指定最大空闲连接时间为 90 秒,即当某个空闲连接超过 90 秒没有被复用,则销毁,空闲连接需要DisableKeepAlives
为false
的情况下才可用,即 HTTP 长连接状态下有效(HTTP/1.1以上版本支持长连接,对应请求头Connection:keep-alive
); - 通过
TLSHandshakeTimeout
指定基于 TLS 协议的安全 TCP 连接在被建立时握手阶段的超时时间为 10 秒; - 通过
ExpectContinueTimeout
指定客户端想要使用 POST 请求把一个很大的报文体发送给服务端的时候,先通过发送一个包含了Expect: 100-continue
的请求报文头,来询问服务端是否愿意接收这个大报文体对应的超时时间,这里默认设置为 1 秒。
另外,Transport
包含了 RoundTrip
方法实现,所以实现了 RoundTripper
接口。下面我们来看看 Transport
中 RoundTrip
方法的实现。
3、Transport.RoundTrip() 方法实现
首先我们来看看 http.RoundTripper
接口的具体定义:
type RoundTripper interface { RoundTrip(*Request) (*Response, error)}
从上述代码中可以看到,http.RoundTripper
接口很简单,只定义了一个名为 RoundTrip
的方法。RoundTrip()
方法用于执行一个独立的 HTTP 事务,接受传入的 *Request
请求值作为参数并返回对应的 *Response
响应值,以及一个 error
值。
在实现具体的 RoundTrip()
方法时,不应该试图在该函数里边解析 HTTP 响应信息。若响应成功,error
的值必须为 nil
,而与返回的 HTTP 状态码无关。若不能成功得到服务端的响应,error
必须为非零值。类似地,也不应该试图在 RoundTrip()
中处理协议层面的相关细节,比如重定向、认证或是 Cookie 等。
非必要情况下,不应该在 RoundTrip()
方法中改写传入的请求对象(*Request
),请求的内容(比如 URL 和 Header 等)必须在传入 RoundTrip()
之前就已组织好并完成初始化。
任何实现了 RoundTrip()
方法的类型都实现了 http.RoundTripper
接口,http.Transport
正是实现了 RoundTrip()
方法继而实现了该接口,在底层,Go 通过 WHATWG Fetch API 实现了单次 HTTP 请求响应事务:
func (t *Transport) RoundTrip(req *Request) (*Response, error) { if useFakeNetwork() { return t.roundTrip(req) } ac := js.Global().Get("AbortController") if ac != js.Undefined() { // Some browsers that support WASM don't necessarily support // the AbortController. See // https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility. ac = ac.New() } opt := js.Global().Get("Object").New() // See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch // for options available. opt.Set("method", req.Method) opt.Set("credentials", "same-origin") if h := req.Header.Get(jsFetchCreds); h != "" { opt.Set("credentials", h) req.Header.Del(jsFetchCreds) } if h := req.Header.Get(jsFetchMode); h != "" { opt.Set("mode", h) req.Header.Del(jsFetchMode) } if ac != js.Undefined() { opt.Set("signal", ac.Get("signal")) } headers := js.Global().Get("Headers").New() for key, values := range req.Header { for _, value := range values { headers.Call("append", key, value) } } opt.Set("headers", headers) if req.Body != nil { // TODO(johanbrandhorst): Stream request body when possible. // See https://bugs.chromium.org/p/chromium/issues/detail?id=688906 for Blink issue. // See https://bugzilla.mozilla.org/show_bug.cgi?id=1387483 for Firefox issue. // See https://github.com/web-platform-tests/wpt/issues/7693 for WHATWG tests issue. // See https://developer.mozilla.org/en-US/docs/Web/API/Streams_API for more details on the Streams API // and browser support. body, err := ioutil.ReadAll(req.Body) if err != nil { req.Body.Close() // RoundTrip must always close the body, including on errors. return nil, err } req.Body.Close() a := js.TypedArrayOf(body) defer a.Release() opt.Set("body", a) } respPromise := js.Global().Call("fetch", req.URL.String(), opt) var ( respCh = make(chan *Response, 1) errCh = make(chan error, 1) ) success := js.FuncOf(func(this js.Value, args []js.Value) interface{} { result := args[0] header := Header{} // https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries headersIt := result.Get("headers").Call("entries") for { n := headersIt.Call("next") if n.Get("done").Bool() { break } pair := n.Get("value") key, value := pair.Index(0).String(), pair.Index(1).String() ck := CanonicalHeaderKey(key) header[ck] = append(header[ck], value) } contentLength := int64(0) if cl, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64); err == nil { contentLength = cl } b := result.Get("body") var body io.ReadCloser // The body is undefined when the browser does not support streaming response bodies (Firefox), // and null in certain error cases, i.e. when the request is blocked because of CORS settings. if b != js.Undefined() && b != js.Null() { body = &streamReader{stream: b.Call("getReader")} } else { // Fall back to using ArrayBuffer // https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer body = &arrayReader{arrayPromise: result.Call("arrayBuffer")} } select { case respCh <- &Response{ Status: result.Get("status").String() + " " + StatusText(result.Get("status").Int()), StatusCode: result.Get("status").Int(), Header: header, ContentLength: contentLength, Body: body, Request: req, }: case <-req.Context().Done(): } return nil }) defer success.Release() failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} { err := fmt.Errorf("net/http: fetch() failed: %s", args[0].String()) select { case errCh <- err: case <-req.Context().Done(): } return nil }) defer failure.Release() respPromise.Call("then", success, failure) select { case <-req.Context().Done(): if ac != js.Undefined() { // Abort the Fetch request ac.Call("abort") } return nil, req.Context().Err() case resp := <-respCh: return resp, nil case err := <-errCh: return nil, err } }
因为实现了 http.RoundTripper
接口的代码通常需要在多个 goroutine 中并发执行,因此我们必须确保实现代码的线程安全性。
以上就是 http.Client
底层实现的几个核心组件及其默认实现,重点关注 http.Transport
,它定义了一次 HTTP 事务的完整流程,我们可以通过自定义 Transport
实现对 HTTP 客户端请求的定制化,了解即可,实际开发的时候,我们一般只需要调用上篇教程提供的几个方法即可,除非需要做底层开发和自定义,否则一般不会涉及到这些。
Tips:想要阅读全部 Go 语言从入门到精通教程,请点击左下角“阅读原文”链接。https://xueyuanjun.com/post/21003