前言
上一篇文章我们讲了 net/http
库客户端 request 的构建,接下来继续讲构建HTTP
请求之后的处理操作
net/http 库的客户端实现(上)
net/http 库的客户端实现(下)
net/http 库的服务端实现
启动事务
构建 HTTP
请求后,接着需要开启HTTP
事务进行请求并且等待远程响应,以net/http.Client.Do()
方法为例子,理一下它的调用链路:
- net/http.Client.Do()
- net/http.Client.do()
- net/http.Client.send()
- net/http.Send()
- net/http.Transport.RoundTrip()
RoundTrip()
是RoundTripper
类型中的一个的方法,net/http.Transport
是其中的一个实现,可以在net/http/transport.go
文件中找到这个方法,看一下源码。
func (t *Transport) roundTrip(req *Request) (*Response, error) {// 省略for {// 检测 ctx 退出信号select {case <-ctx.Done():req.closeBody()return nil, ctx.Err()default:}// 获取连接,通过使用连接池对资源进行了复用;pconn, err := t.getConn(treq, cm)if err != nil {t.setReqCanceler(cancelKey, nil)req.closeBody()return nil, err}var resp *Responseif pconn.alt != nil {// HTTP/2 path.t.setReqCanceler(cancelKey, nil) // not cancelable with CancelRequestresp, err = pconn.alt.RoundTrip(req)} else {// 处理响应resp, err = pconn.roundTrip(treq)}if err == nil {resp.Request = origReqreturn resp, nil}// Rewind the body if we're able to.req, err = rewindBody(req)if err != nil {return nil, err}}
}
重点看两部分
net/http.Transport.getConn()
获取连接net/http.persistConn.roundTrip()
处理写入HTTP
请求,并在select
中等待响应的返回
获取连接(getConn)
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {req := treq.Requesttrace := treq.tracectx := req.Context()if trace != nil && trace.GetConn != nil {trace.GetConn(cm.addr())}w := &wantConn{cm: cm,key: cm.key(),ctx: ctx,ready: make(chan struct{}, 1),beforeDial: testHookPrePendingDial,afterDial: testHookPostPendingDial,}defer func() {if err != nil {w.cancel(t, err)}}()// 在队列中有闲置连接,直接返回if delivered := t.queueForIdleConn(w); delivered {pc := w.pcreturn pc, nil}cancelc := make(chan error, 1)t.setReqCanceler(treq.cancelKey, func(err error) { cancelc <- err })// 放到队列中等待建立新的连接t.queueForDial(w)// 阻塞等待连接select {case <-w.ready:return w.pc, w.errcase <-req.Cancel:return nil, errRequestCanceledConncase <-req.Context().Done():return nil, req.Context().Err()case err := <-cancelc:if err == errRequestCanceled {err = errRequestCanceledConn}return nil, err}
}
因为连接的建议会消耗比较多的时间,带来较大的开下,所以Go语言使用了连接池对资源进行分配和复用,先调用 net/http.Transport.queueForIdleConn()
获取等待闲置的连接,如果没有获取到在调用net/http.Transport.queueForDial
在队列中等待建立新的连接,通过select
监听连接是否建立完毕,超时未获取到连接会上剖错误,我们继续在queueForDial
追踪TCP
连接的建立:
启动一个goroutine
做tcp
的建连,最终调用dialConn
方法,在这个方法内做持久化连接,调用net
库的dial
方法进行TCP
连接:
在连接建立后,代码中我们我们还看到分别启动了两个goroutine
,
readLoop
用于从tcp
连接中读取数据,writeLoop
用于从tcp
连接中写入数据;
writeLoop
方法监听writech
通道,在循环中写入发送的数据。
net/http.Transport{}
中提供了连接池配置参数,可以自定义
处理 HTTP 请求
net/http.persistConn.roundTrip()
处理HTTP请求
有两个通道:
pc.writech
:其类型是chan writeRequest
,writeLoop
协程会循环写入数据,net/http.Request.write
会根据net/http.Request
结构中的字段按照HTTP
协议组成TCP
数据段,TCP
协议栈会负责将HTTP
请求中的内容发送到目标服务器上;pc.reqch
:其类型是chan requestAndChan
,readLoop
协程会循环读取响应数据并且调用net/http.ReadResponse
进行协议解析,其中包含状态码、协议版本、请求头等内容;
总结
net/http.Client
是级别最高的抽象,其中transport
用于开启HTTP
事务,jar
用于处理cookie
;net/http.Transport
中主要逻辑两部分:- 从连接池中获取持久化连接
- 使用持久化连接处理HTTP请求
net/http
库中默认有一个DefaultClient
可以直接使用,DefaultClient
有对应DefaultTransport
,可以满足大多数场景。
- 如果需要使用自己管理
HTTP
客户端的头域、重定向等策略,可以自定义Client
, - 如果需要管理代理、TLS配置、连接池、压缩等设置,可以自定义
Transport
;
因为HTTP
协议的版本是不断变化的,所以为了可扩展性,transport
是一个接口类型,具体的是实现是Transport
、http2Transport
、fileTransport
,这样实现扩展性变得很高。
HTTP
在建立连接时会耗费大量的资源,需要开辟一个goroutine
去创建TCP
连接,连接建立后会在创建两个goroutine
用于HTTP
请求的写入和响应的解析,然后使用channel
进行通信,所以要合理利用连接池,避免大量的TCP
连接的建立可以优化性能;