客户端的内容将是如何发送请求和接收响应,走完客户端就把整个流程就完整的串联起来了!
这次我把调用的核心方法和流程走读的函数也贴出来,这样看应该更有逻辑感,重要部分用红色标记了一下,可以着重看下。
图片
先了解下核心数据结构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