图文讲透Golang标准库 net/http实现原理 客户端
客户端的内容将是如何发送请求和接收响应,走完客户端就把整个流程就完整的串联起来了!
这次我把调用的核心方法和流程走读的函数也贴出来,这样看应该更有逻辑感,重要部分用红色标记了一下,可以着重看下。
图片
先了解下核心数据结构Client和Request。
Client结构体
type Client struct { Transport RoundTripper CheckRedirect func(req *Request, via []*Request) error Jar CookieJar Timeout time.Duration }
四个字段分别是:
- • Transport:表示 HTTP 事务,用于处理客户端的请求连接并等待服务端的响应;
- • CheckRedirect:处理重定向的策略
- • Jar:管理和存储请求中的 cookie
- • Timeout:超时设置
Request结构体
Request字段较多,这里就列举一下常见的一些字段
type Request struct { Method string URL *url.URL Header Header Body io.ReadCloser Host string Response *Response ... }
- • Method:指定的HTTP方法(GET、POST、PUT等)
- • URL:请求路径
- • Header:请求头
- • Body:请求体
- • Host:服务器主机
- • Response:响应参数
构造请求
var DefaultClient = &Client{} func Get(url string) (resp *Response, err error) { return DefaultClient.Get(url) }
示例HTTP 的 Get方法会调用到 DefaultClient 的 Get 方法,,然后调用到 Client 的 Get 方法。
DefaultClient 是 Client 的一个空实例(跟DefaultServeMux有点子相似)
图片
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) } func NewRequest(method, url string, body io.Reader) (*Request, error) { return NewRequestWithContext(context.Background(), method, url, body) }
Client.Get() 根据用户的入参,请求参数 NewRequest使用上下文包装NewRequestWithContext ,接着通过 Client.Do 方法,处理这个请求。
NewRequestWithContext
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) { ... // 解析url u, err := urlpkg.Parse(url) ... rc, ok := body.(io.ReadCloser) if !ok && body != nil { rc = ioutil.NopCloser(body) } u.Host = removeEmptyPort(u.Host) req := &Request{ ctx: ctx, Method: method, URL: u, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: make(Header), Body: rc, Host: u.Host, } ... return req, nil }
NewRequestWithContext 函数主要是功能是将请求封装成一个 Request 结构体并返回,这个结构体的名称是req。
准备发送请求
构造好的Request结构req,会传入c.Do()方法。
我们看下发送请求过程调用了哪些方法,用下图表示下
图片
🚩 其实不管是Get还是Post请求的调用流程都是一样的,只是对外封装了Post和Get请求
func (c *Client) do(req *Request) (retres *Response, reterr error) { ... for { ... resp, didTimeout, err = send(req, deadline) if err != nil { return nil, didTimeout, err } } ... } //Client 调用 Do 方法处理发送请求最后会调用到 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 } ... return resp, nil, nil }
c.transport()方法是为了回去Transport的默认实例 DefaultTransport ,我们看下DefaultTransport长什么样。
DefaultTransport
var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, DialContext: defaultTransportDialContext(&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }), ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }
可以根据需要建立网络连接,并缓存它们以供后续调用重用,部分参数如下:
- • MaxIdleConns:最大空闲连接数
- • IdleConnTimeout:空闲连接超时时间
- • ExpectContinueTimeout:预计继续超时
注意这里的RoundTripper是个接口,也就是说 Transport 实现 RoundTripper 接口,该接口方法接收Request,返回Response。
RoundTripper
type RoundTripper interface { RoundTrip(*Request) (*Response, error) }
图片
虽然还没看完后面逻辑,不过我们猜测RoundTrip方法可能是实际处理客户端请求的实现。
我们继续追下后面逻辑,看下是否能验证这个猜想。
func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { ... resp, err = rt.RoundTrip(req) if err != nil { ... } .. }
👉 你看send函数的第二个参数就是接口类型,调用层传递的Transport的实例DefaultTransport。
而rt.RoundTrip()方法的调用具体在net/http/roundtrip.go文件中,这也是RoundTrip接口的实现,代码如下:
func (t *Transport) RoundTrip(req *Request) (*Response, error) { return t.roundTrip(req) }
Transport.roundTrip 方法概况来说干了这些事:
- • 封装请求transportRequest
- • 调用 Transport 的 getConn 方法获取连接
- • 在获取到连接后,调用 persistConn 的 roundTrip 方法等待请求响应结果
func (t *Transport) roundTrip(req *Request) (*Response, error) { ... for { ... // 请求封装 treq := &transportRequest{Request: req, trace: trace, cancelKey: cancelKey} cm, err := t.connectMethodForRequest(treq) if err != nil { ... } // 获取连接 pconn, err := t.getConn(treq, cm) if err != nil { ... } // 等待响应结果 var resp *Response if pconn.alt != nil { t.setReqCanceler(cancelKey, nil) resp, err = pconn.alt.RoundTrip(req) } else { resp, err = pconn.roundTrip(treq) } ... } }
封装请求transportRequeste没啥好说的,因为treq被roundTrip修改,所以这里需要为每次重试重新创建。
获取连接
获取连接的方法是 getConn,这里代码还是比较长的,会有不同的两种方式去获取连接:
图片
getConn
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) { ... // 初始化wantConn结构体 w := &wantConn{ cm: cm, key: cm.key(), ctx: ctx, ready: make(chan struct{}, 1), beforeDial: testHookPrePendingDial, afterDial: testHookPostPendingDial, } ... // 获取空闲连接 if delivered := t.queueForIdleConn(w); delivered { ... } // 异步创建新连接 t.queueForDial(w) select { // 阻塞等待获取到连接完成 case 0 { oldTime = time.Now().Add(-t.IdleConnTimeout) } //从idleConn根据w.key找对应的persistConn 列表 if list, ok := t.idleConn[w.key]; ok { stop := false delivered := false for len(list) > 0 && !stop { // 找到persistConn列表最后一个 pconn := list[len(list)-1] // 检查这个 persistConn 是不是过期 tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime) if tooOld { //如果过期进行异步清理 go pconn.closeConnIfStillIdle() } // 该 persistConn 被标记为 broken 或 闲置太久 continue if pconn.isBroken() || tooOld { list = list[:len(list)-1] continue } // 尝试将该 persistConn 写入到 wantConn(w)中 delivered = w.tryDeliver(pconn, nil) if delivered { // 写入成功,将persistConn从空闲列表中移除 if pconn.alt != nil { } else { t.idleLRU.remove(pconn) //缺省了最后一个conn list = list[:len(list)-1] } } stop = true } //对被获取连接后的列表进行判断 if len(list) > 0 { t.idleConn[w.key] = list } else { // 如果该 key 对应的空闲列表不存在,那么将该key从字典中移除 delete(t.idleConn, w.key) } if stop { return delivered } } // 如果找不到空闲的 persistConn if t.idleConnWait == nil { t.idleConnWait = make(map[connectMethodKey]wantConnQueue) } // 将该 wantConn添加到等待空闲idleConnWait中 q := t.idleConnWait[w.key] q.cleanFront() q.pushBack(w) t.idleConnWait[w.key] = q return false }
我们知道了为找到的空闲连接会被放到空闲 idleConnWait 这个等待map中,最后会被Transport.tryPutIdleConn方法将pconne添加到等待新请求的空闲持久连接列表中。
queueForDial创建新连接
queueForDial意思是排队等待拨号,为什么说是等带呢,因为最终的结果是在ready这个channel上进行通知的。
流程如下图:
图片
我们先看下Transport结构体的这两个map,名称不一样map的属性和解释都是一样的,其中idleConnWait是在没查找空闲连接的时候存放当前连接的map。
而connsPerHostWait用在了创建新连接的地方,可以猜测一下创建新链接的地方就是将当前的请求放入到 connsPerHostWait 等待map中。
// waiting getConns idleConnWait map[connectMethodKey]wantConnQueue // waiting getConns connsPerHostWait map[connectMethodKey]wantConnQueue
Transport.queueForDial
func (t *Transport) queueForDial(w *wantConn) { w.beforeDial() // 小于等于零,意思是限制,直接异步建立连接 if t.MaxConnsPerHost