阅读 575

初探 http 客户端源码 1

这是我参与更文挑战的第10天,活动详情查看:更文挑战

如果❤️我的文章有帮助,欢迎点赞、关注。这是对我继续技术创作最大的鼓励。更多文章在我博客

初探 http 客户端源码 1

golang 源码基于 golang 1.16, 由于进入 RoundTripper 发起请求, 到获取内容等篇幅太长。为了良好的阅读体验, 这里是第 1 篇。

golang cient 发起请求的流程

  1. 根据请求需要(连接池、重定向策略、cookie加载),构建 结构体 Client
  2. 结构体 Client 调用请求方法GetPostOption, 最后会统一调用 func (c *Client) do(req *Request) (retres *Response, reterr error)
  3. c.do() 内部调用 方法send() 发起请求,获取内容并返回 func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error)
  4. 步骤3中 方法send() 中的参数 rt RoundTripper 是处理该请求往返的事务接口,实际调用方法为 func (t *Transport) roundTrip(req *Request) (*Response, error)
  5. Transport.roundTrip() 调用 Transport.getConn() 方法 使用连接池获取缓存或新创建的连接 *persistConn。方法结构为: func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error)

主要结构体体

http 客户端 源码位于 golang\src\net\http\client.go 文件

需要关注 两个结构体 客户端 Client 和 连接池 RoundTripper

客户端 Client

// http 客户端
type Client struct {
	// 连接池
	Transport RoundTripper

	// 处理重定向的策略。如果 CheckRedirect 不为 nil,则客户端之前调用它
	CheckRedirect func(req *Request, via []*Request) error

	// 有些网站的请求,需要带上cookie。会用CookieJar获取cookie值
	Jar CookieJar

	// 请求超时时间
	Timeout time.Duration
}
复制代码

连接池 RoundTripper

type RoundTripper interface {
	// 请求下游接口,返回请求的响应内容。
	RoundTrip(*Request) (*Response, error)
}
复制代码

发起请求

HTTP 客户端 调用请求方法 Get 函数, 向指定的 URL 发出 GET。 如果响应是其中之一跟随重定向代码,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) // 完成读取后,需要关闭 resp.Body。否则 resp.Body 将一直占用资源
}
复制代码

Get函数 的内部 c.Do() 会在其内部再调用 c.do()

func (c *Client) Do(req *Request) (*Response, error) {
	return c.do(req)
}

func (c *Client) do(req *Request) (retres *Response, reterr error) {
	for {
		// 对于除第一个请求以外的所有请求(重定向),创建下一个请求请求跳跃并替换 req.
...
		reqs = append(reqs, req)
		var err error
		var didTimeout func() bool
		if resp, didTimeout, err = c.send(req, deadline); err != nil {
			// c.send() always closes req.Body
			reqBodyClosed = true
			if !deadline.IsZero() && didTimeout() {
				err = &httpError{
					err:     err.Error() + " (Client.Timeout exceeded while awaiting headers)",
					timeout: true,
				}
			}
			return nil, uerr(err)
		}

		
        /**
            判断 response 响应是否存在 以下重定向行为
                301 (Moved Permanently)
                302 (Found)
                303 (See Other)
                307 (Temporary Redirect)
                308 (Permanent Redirect)
            有则根据状态重设相关属性
                redirectMethod, shouldRedirect, includeBody
        */
        var shouldRedirect bool
		redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
        
        // 判断重定向 或 返回响应数据
		if !shouldRedirect {
			return resp, nil
		}

        // 默认关闭 req.Body,但存在因为恐慌导致 “未关闭req.Body” 直接返回情况
		req.closeBody()
	}

    func (r *Request) closeBody() error {
        if r.Body == nil {
            return nil
        }
        return r.Body.Close()
    }
}
复制代码

Client.do() 内部调用 Client.send()

func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
	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
	}
	if c.Jar != nil {
		if rc := resp.Cookies(); len(rc) > 0 {
			c.Jar.SetCookies(req.URL, rc) // 使用 CookieJar 获取 cookie 并设置
		}
	}
	return resp, nil, nil
}



// send 方法复制发起一个 HTTP 请求 
func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
	......
	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 {
			if string(tlsErr.RecordHeader[:]) == "HTTP/" {
				err = errors.New("http: server gave HTTP response to HTTPS client")
			}
		}
		return nil, didTimeout, err
	}
	......
	return resp, nil, nil
}
复制代码

Client.send() 调用 rt.RoundTrip()

上方 send 方法中 rt RoundTripper DefaultTransport的RoundTrip方法,实际就是Transport结构体的RoundTrip方法 golang\src\net\http\transport.go

// roundTrip implements a RoundTripper over HTTP.
func (t *Transport) roundTrip(req *Request) (*Response, error) {
        ......
	for {
		select {
		case <-ctx.Done():
			req.closeBody()
			return nil, ctx.Err()
		default:
		}

               //为了避免请求体在请求过程中被 roundTrip 修改, 所以每次封装新的request
		treq := &transportRequest{Request: req, trace: trace, cancelKey: cancelKey}
		cm, err := t.connectMethodForRequest(treq)
		if err != nil {
			req.closeBody()
			return nil, err
		}

                //使用连接池技术,获取连接对象 *persistConn
		pconn, err := t.getConn(treq, cm)
		if err != nil {
			t.setReqCanceler(cancelKey, nil)
			req.closeBody()
			return nil, err
		}

                //使用连接对象获取 响应内容 response
		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)
		}


		// Rewind the body if we're able to.
		req, err = rewindBody(req)
		if err != nil {
			return nil, err
		}
	}
}
复制代码

后续待补充 从 rt.RoundTrip() -> 调用连接池 -> 拨号 -> 获取请求内容部分

文章分类
后端
文章标签