前言
上一篇文章我们讲了 net/http
库客户端 request 的构建,接下来继续讲构建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 *Response
if pconn.alt != nil {
// HTTP/2 path.
t.setReqCanceler(cancelKey, nil) // not cancelable with CancelRequest
resp, err = pconn.alt.RoundTrip(req)
} else {
// 处理响应
resp, err = pconn.roundTrip(treq)
}
if err == nil {
resp.Request = origReq
return 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.Request
trace := treq.trace
ctx := 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.pc
return 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.err
case <-req.Cancel:
return nil, errRequestCanceledConn
case <-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
连接的建立可以优化性能;