图文讲透Golang标准库 net/http实现原理 客户端

2024年 2月 5日 82.2k 0

客户端的内容将是如何发送请求和接收响应,走完客户端就把整个流程就完整的串联起来了!

这次我把调用的核心方法和流程走读的函数也贴出来,这样看应该更有逻辑感,重要部分用红色标记了一下,可以着重看下。

图片图片

先了解下核心数据结构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,这里代码还是比较长的,会有不同的两种方式去获取连接:

  • 1. 调用 queueForIdleConn 排队等待获取空闲连接
  • 2. 如果获取空闲连接失败,那么调用 queueForDial 异步创建一个新的连接,并通过channel来接收readdy信号,来确认连接是否构造完成
  • 图片图片

    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

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论