这是我参与更文挑战的第10天,活动详情查看:更文挑战
如果❤️我的文章有帮助,欢迎点赞、关注。这是对我继续技术创作最大的鼓励。更多文章在我博客
初探 http 客户端源码 1
golang 源码基于 golang 1.16, 由于进入
RoundTripper发起请求, 到获取内容等篇幅太长。为了良好的阅读体验, 这里是第 1 篇。
golang cient 发起请求的流程
- 根据请求需要(连接池、重定向策略、cookie加载),构建
结构体 Client 结构体 Client调用请求方法Get、Post、Option, 最后会统一调用func (c *Client) do(req *Request) (retres *Response, reterr error)c.do()内部调用 方法send()发起请求,获取内容并返回func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error)步骤3中 方法send()中的参数rt RoundTripper是处理该请求往返的事务接口,实际调用方法为func (t *Transport) roundTrip(req *Request) (*Response, error)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() -> 调用连接池 -> 拨号 -> 获取请求内容部分