前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Golang源码深入-Go1.15.6发起http请求流程-1

Golang源码深入-Go1.15.6发起http请求流程-1

作者头像
公众号-利志分享
发布2022-04-25 09:39:12
7810
发布2022-04-25 09:39:12
举报
文章被收录于专栏:利志分享

http协议是业务中使用最广泛,开发者接触最多的协议之一。最近笔者我也是因为业务中遇到一些问题,才深入阅读一些源码,带着问题来学习,能学习得更好,更有效果。

发起http的请求主流程如下,这里整理了发起一个http请求的主函数调用,函数所在的文件,以及函数功能说明。

下面我们来看不同的模块的代码:

1 Client对象

代码语言:javascript
复制
// a Client是一个HTTP的client
// Client Transport保存了tcp的连接请求,支持http详情的配置
type Client struct {
    // 发送HTTP请求。
    Transport RoundTripper

    // CheckRedirect指定处理重定向的策略
    // 默认是为nil
    CheckRedirect func(req *Request, via []*Request) error

    // Jar指定cookie 是保存cookie的对象
    Jar CookieJar

    // Timeout指定请求的时间限制
    Timeout time.Duration
}

2 NewRequest对象

代码语言:javascript
复制
// NewRequest从新包装NewRequestWithContext。
func NewRequest(method, url string, body io.Reader) (*Request, error) {
    return NewRequestWithContext(context.Background(), method, url, body)
}

// 组装请求信息,其实主要是Request{}结构,一些简单校验
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
    if method == "" {
        // 判断method为空,则赋值是GET
        method = "GET"
    }
    // 验收method的字符是否是26个字母,如果是则报invalid method。
    if !validMethod(method) {
        return nil, fmt.Errorf("net/http: invalid method %q", method)
    }
    // 判断ctx是否是空
    if ctx == nil {
        return nil, errors.New("net/http: nil Context")
    }
    // 解析url是否是正常的
    u, err := urlpkg.Parse(url)
    if err != nil {
        return nil, err
    }
    // 断言body的类型,如果失败,则给rc重新赋值
    rc, ok := body.(io.ReadCloser)
    if !ok && body != nil {
        rc = ioutil.NopCloser(body)
    }
    // 处理u.Host可能带有端口
    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,
    }
    if body != nil {
        // 断言body的类型
        switch v := body.(type) {
        case *bytes.Buffer:
            req.ContentLength = int64(v.Len())
            buf := v.Bytes()
            req.GetBody = func() (io.ReadCloser, error) {
                r := bytes.NewReader(buf)
                return ioutil.NopCloser(r), nil
            }
        case *bytes.Reader:
            req.ContentLength = int64(v.Len())
            snapshot := *v
            req.GetBody = func() (io.ReadCloser, error) {
                r := snapshot
                return ioutil.NopCloser(&r), nil
            }
        case *strings.Reader:
            req.ContentLength = int64(v.Len())
            snapshot := *v
            req.GetBody = func() (io.ReadCloser, error) {
                r := snapshot
                return ioutil.NopCloser(&r), nil
            }
        default:
            // body的类型断言失败,不处理
        }
        // 对于body如果不等于nil,但是content-length又是0,则对req.Body和req.GetBody进行重新赋值。
        if req.GetBody != nil && req.ContentLength == 0 {
            req.Body = NoBody
            req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
        }
    }

    return req, nil
}

3 Do(req *Request)对象

代码语言:javascript
复制
// 调用do函数
func (c *Client) Do(req *Request) (*Response, error) {
    return c.do(req)
}

func (c *Client) do(req *Request) (retres *Response, reterr error) {
    // 如果有测试,则函数结束后调用测试
    if testHookClientDoResult != nil {
        defer func() { testHookClientDoResult(retres, reterr) }()
    }
    //req.URL 检测
    if req.URL == nil {
        req.closeBody()
        return nil, &url.Error{
            Op:  urlErrorOp(req.Method),
            Err: errors.New("http: nil Request.URL"),
        }
    }

    var (
        deadline      = c.deadline()
        reqs          []*Request
        resp          *Response
        copyHeaders   = c.makeHeadersCopier(req)
        reqBodyClosed = false // have we closed the current req.Body?

        // Redirect behavior:
        redirectMethod string
        includeBody    bool
    )
    // 定义错误处理函数 uerr
    uerr := func(err error) error {
        // 请求体可能已经被c.send()关闭了
        if !reqBodyClosed {
            req.closeBody()
        }
        var urlStr string
        // 如果 有响应且 响应请求 不为空,则跳过响应请求URL的账号密码校验
        if resp != nil && resp.Request != nil {
            urlStr = stripPassword(resp.Request.URL)
        } else {
            //只跳过请求体URL密码校验
            urlStr = stripPassword(req.URL)
        }
        return &url.Error{
            Op:  urlErrorOp(reqs[0].Method),
            URL: urlStr,
            Err: err,
        }
    }
    // 轮询(确保每个请求都能执行到)
    for {
        // 判断reqs是否为空,不为空则进行下面流程判断
        if len(reqs) > 0 {
            // 首先获取 请求头的Location
            loc := resp.Header.Get("Location")
            if loc == "" {
                resp.closeBody()
                return nil, uerr(fmt.Errorf("%d response missing Location header", resp.StatusCode))
            }
            // 解析请求URL的Location
            u, err := req.URL.Parse(loc)
            if err != nil {
                resp.closeBody()
                return nil, uerr(fmt.Errorf("failed to parse Location header %q: %v", loc, err))
            }
            host := ""
            // 如果请求的host 不等于请求的url的host
            if req.Host != "" && req.Host != req.URL.Host {
                // 如果调用者指定了自定义主机头并且重定向位置是相对的,则通过重定向保留主机头
                if u, _ := url.Parse(loc); u != nil && !u.IsAbs() {
                    host = req.Host
                }
            }
            ireq := reqs[0]
            req = &Request{
                Method:   redirectMethod,
                Response: resp,
                URL:      u,
                Header:   make(Header),
                Host:     host,
                Cancel:   ireq.Cancel,
                ctx:      ireq.ctx,
            }
            // 状态码307、308操作: 如果包含 请求体(临时重定向或者 永久重定向 的时候会包含),且 设置了GetBody,则 使用body重新 发送请求
            if includeBody && ireq.GetBody != nil {
                req.Body, err = ireq.GetBody()
                if err != nil {
                    resp.closeBody()
                    return nil, uerr(err)
                }
                req.ContentLength = ireq.ContentLength
            }

            // 在设置Referer之前复制原始头,以防用户在第一次请求时设置Referer; 如果他们真的想重写,他们可以在CheckRedirect函数中完成
            copyHeaders(req)

            // 将Referer头从最近的请求URL添加到新的请求URL,如果不是https->http
            if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {
                req.Header.Set("Referer", ref)
            }
            // 检查重定向
            err = c.checkRedirect(req, reqs)

            // Sentinel error允许用户选择上一个响应,而不关闭其 响应体. See Issue 10069.
            if err == ErrUseLastResponse {
                return resp, nil
            }

            // 检查响应体长度。 关闭上一个响应体。但至少要读一些正文,这样如果它很小,底层的TCP连接就会被重用。无需检查错误:如果失败,Transport无论如何也不会重用它
            const maxBodySlurpSize = 2 << 10
            if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
                // 从 响应体 复制maxBodySlurpSize 个字节,它返回复制的字节
                io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
            }
            // 关闭响应体
            resp.Body.Close()

            if err != nil {
                // 兼容性的特殊情况:如果CheckRedirect函数失败,则返回响应和错误。
                ue := uerr(err)
                ue.(*url.Error).URL = loc
                return resp, ue
            }
        }

        // 把req追加到reqs
        reqs = append(reqs, req)
        var err error
        var didTimeout func() bool
        // 第一个请求:调用内部发送请求方法
        if resp, didTimeout, err = c.send(req, deadline); err != nil {
            // c.send()发送之后出现错误,先关闭req.Body
            reqBodyClosed = true
            if !deadline.IsZero() && didTimeout() {
                err = &httpError{
                    // TODO: early in cycle: s/Client.Timeout exceeded/timeout or context cancellation/
                    err:     err.Error() + " (Client.Timeout exceeded while awaiting headers)",
                    timeout: true,
                }
            }
            return nil, uerr(err)
        }

        // 判断是否冲行动
        var shouldRedirect bool
        redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
        if !shouldRedirect {
            return resp, nil
        }
        // 关闭请求体
        req.closeBody()
    }
}

4 client.send方法,发送请求

代码语言:javascript
复制
// 客户机send()方法:客户端 发送请求获取响应 内部方法 只有在出现错误时,didTimeout才是非nil
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    // 如果客户端的cookie不为空,则会把cookie的信息加到请求里面去
    if c.Jar != nil {
        for _, cookie := range c.Jar.Cookies(req.URL) {
            req.AddCookie(cookie)
        }
    }
    resp, didTimeout, err = send(req, c.transport(), deadline)
    if err != nil {
        return nil, didTimeout, err
    }
    // 如果客户端cookie不为空,则会把cookie设置请求的cookie
    if c.Jar != nil {
        if rc := resp.Cookies(); len(rc) > 0 {
            c.Jar.SetCookies(req.URL, rc)
        }
    }
    return resp, nil, nil
}

5 send方法,调用发送HTTP request

代码语言:javascript
复制
// send() 方法发出一个HTTP请求。 调用方 在读完响应体时应关闭request请求
func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    req := ireq // req is either the original request, or a modified fork
    // 如果RoundTripper或者url 是空的,则不发送请求直接返回
    if rt == nil {
        req.closeBody()
        return nil, alwaysFalse, errors.New("http: no Client.Transport or DefaultTransport")
    }

    if req.URL == nil {
        req.closeBody()
        return nil, alwaysFalse, errors.New("http: nil Request.URL")
    }

    // 请求的URI不能被设置在客户端请求上
    if req.RequestURI != "" {
        req.closeBody()
        return nil, alwaysFalse, errors.New("http: Request.RequestURI can't be set in client requests")
    }

    // forkReq是一个函数:在第一次调用时将req分叉到ireq的浅克隆中。
    forkReq := func() {
        if ireq == req {
            req = new(Request)
            *req = *ireq // shallow clone
        }
    }

    // 如果请求头是空的,则调用forkReq函数(将req分叉到ireq的浅克隆中),然后初始化请求头
    if req.Header == nil {
        forkReq()
        req.Header = make(Header)
    }

    // 取出url中的User字段,给请求头设置 用户密钥(token)
    if u := req.URL.User; u != nil && req.Header.Get("Authorization") == "" {
        username := u.Username()
        password, _ := u.Password()
        forkReq()
        req.Header = cloneOrMakeHeader(ireq.Header)
        req.Header.Set("Authorization", "Basic "+basicAuth(username, password))
    }
    // 如果 截止时间 有值,给 再次调用forkReq函数(将req分叉到ireq的浅克隆中)
    if !deadline.IsZero() {
        forkReq()
    }
    stopTimer, didTimeout := setRequestCancel(req, rt, deadline)

    // 执行请求,拿到响应
    resp, err = rt.RoundTrip(req)
    if err != nil {
        // 调用 停止计时器
        stopTimer()
        if resp != nil {
            log.Printf("RoundTripper returned a response & error; ignoring response")
        }
        //  处理错误
        if tlsErr, ok := err.(tls.RecordHeaderError); ok {
            // 如果我们得到一个错误的TLS记录头,请检查响应是否像HTTP,并给出一个更有用的错误
            if string(tlsErr.RecordHeader[:]) == "HTTP/" {
                err = errors.New("http: server gave HTTP response to HTTPS client")
            }
        }
        return nil, didTimeout, err
    }
    // 响应结果判断
    if resp == nil {
        return nil, didTimeout, fmt.Errorf("http: RoundTripper implementation (%T) returned a nil *Response with a nil error", rt)
    }
    if resp.Body == nil {
        // 处理响应body为nil的情况,处理一些默认赋值
        if resp.ContentLength > 0 && req.Method != "HEAD" {
            return nil, didTimeout, fmt.Errorf("http: RoundTripper implementation (%T) returned a *Response with content length %d but a nil Body", rt, resp.ContentLength)
        }
        resp.Body = ioutil.NopCloser(strings.NewReader(""))
    }
    // 如果截止时间有值,则响应体 是带有 截止时间器的 cancelTimerBody
    if !deadline.IsZero() {
        resp.Body = &cancelTimerBody{
            stop:          stopTimer,
            rc:            resp.Body,
            reqDidTimeout: didTimeout,
        }
    }
    //这里是最终的结果正确响应的返回。
    return resp, nil, nil
}

关于发起http的client.go的源码就分享到这里。关于上面的源码阅读,从一定程度上解决了笔者的问题,开头笔者说过业务中遇到了一些问题。也从阅读源码中了解switch case 可以对interface进行断言,对http发送client请求的整个流程更加熟悉了,如何使用http.Client也是一种技巧。

总结:

( 1 )对interface进行断言,可以通过switch case,这样代码更加优雅(下面贴个示例)。

代码语言:javascript
复制
package test

import (
    "fmt"
    "testing"
)

func Test_Interface(t *testing.T) {
    checkSwitchInterface(10)
}

// 通过switch case 断言判断arr的类型
func checkSwitchInterface(arr interface{}) {
    switch t := arr.(type) {
    case int:
        fmt.Println("int:", t)
    case string:
        fmt.Println("string:", t)
    default:
        fmt.Println("未知")
    }
}

( 2 )客户端的Timeout实现是通过context,通过setRequestCancel方法起一个开启一个定时器,然后起一个协程来监听定时器timer的结果,最后调用到transport的CancelRequest取消请求的方法。

( 3 )http.Client在大量发送http请求的时候最好声明一个全局变量,尽量不要每次发送一个请求声明一个http.Client,方便统一申明transport来控制复用tcp请求。

注意:

1、http请求第一篇文章,是个总括,第二章会更加详细讲解tcp的复用,参数控制的一些注意事项。 2、笔者本着严谨的态度,流程中的很多细节并未详细提及或讲述,请读者酌情参考。

参考文档

https://blog.csdn.net/yyj18085644/article/details/112994499

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 利志分享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档