golang源码解读之http.client

公良浩邈
2023-12-01

client.go 文件 内容总括
① 首先 定义了客户端对象, 以及客户端的send发送请求获取响应的方法(调用了内部send方法),获取截止时间方法、获取往返处理器方法;
② 然后 内部send方法实现,主要是判断请求内容,以及使用 RoundTripper 发送请求获取响应, 判断响应并返回响应;
③ 然后 内部setRequestCancel 设置请求体取消方法实现;
④ 然后 重定向检查,以及如果重定向 怎么复制请求头 和cookie进行二次请求的实现;
⑤ 然后 发送get、post、header、postForm方式请求方法的实现,都是对内部 默认客户端的请求方法的封装;
⑥ 然后自定义客户端 发送请求的do方法的实现(url检查、重定向检查、请求体检查、发送第一个请求、发送重定向请求,陆续返回相应)
⑦ 以及涉及的一些复制请求头、检查重定向的内部方法

源码解读
①client结构体

type Client struct {
	// Transport是一个接口,表示HTTP事务,用于处理客户端的请求并等待服务端的响应。
	// 如果Transport为nil,则使用DefaultTransport。
	Transport RoundTripper

	// CheckRedirect指定处理重定向的策略(函数类型)
	// 如果CheckRedirect不为nil,客户端会在执行重定向之前调用本函数字段。
	// 参数req和via是将要执行的请求和已经执行的请求(切片,越新的请求越靠后)。
	// 如果CheckRedirect返回一个错误,本类型的Get方法不会发送请求req,
	// 而是返回之前得到的最后一个回复和该错误。(包装进url.Error类型里)
	// 如果CheckRedirect为nil,会采用默认策略:连续10此请求后停止。
	// 函数指定处理重定向的策略。当使用 HTTP Client 的Get()或者是Head()方法发送 HTTP 请求时,若响应返回的状态码为 30x (比如 301 / 302 / 303 / 307), HTTP Client会在遵循跳转规则之前先调用这个CheckRedirect函数
	CheckRedirect func(req *Request, via []*Request) error

	// Jar指定cookie管理器:将相关cookie插入到每个出站请求中,并用每个入站响应的cookie值进行更新
	// 如果Jar为nil,请求中不会发送cookie,回复中的cookie会被忽略。(只有在请求中显式设置了cookie时,才会发送cookie)
	Jar CookieJar

	// Timeout指定本类型的值执行请求的时间限制。
	// 该超时限制包括连接时间、重定向和读取回复主体的时间。
	// 计时器会在Head、Get、Post或Do方法返回后继续运作并在超时后中断回复主体的读取。
	// Timeout为零值表示不设置超时。
	Timeout time.Duration
}

一个Client实例 就是一个HTTP客户端。它的零值(DefaultClient)是使用DefaultTransport的可用客户端。
客户机的传输通常具有内部状态(缓存的TCP连接),因此应该重新用客户机,而不是根据需要创建客户机。多个goroutine并发使用客户端是安全的。客户机的级别比往返程序(如传输)更高,另外还处理诸如cookies和重定向之类的HTTP细节。

send() 方法
客户端的send发送请求获取响应的方法

// 客户机send()方法:客户端 发送请求获取响应 内部方法
// 只有在出现错误时,didTimeout才是非nil
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
	// 1、如果 客户机的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
	}
	if c.Jar != nil {
		if rc := resp.Cookies(); len(rc) > 0 {
			c.Jar.SetCookies(req.URL, rc)
		}
	}
	return resp, nil, nil
}

②内部send()方法实现

// send() 方法发出一个HTTP请求。 调用方 在读完响应体时应关闭它
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
	// 1、如果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")
	}

	// 2、如果 Request.RequestURI有值也返回,因为其不能再客户机请求体内设置
	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
		}
	}

	// 3、如果请求头是空的,则调用forkReq函数(将req分叉到ireq的浅克隆中),然后初始化请求头
	// send的大多数调用方(Get、Post等)不需要头文件,使其处于未初始化状态。不过,我们向传输部门保证这已经初始化
	if req.Header == nil {
		forkReq()
		req.Header = make(Header)
	}

	// 4、取出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))
	}

	// 5、如果 截止时间 有值,给 再次调用forkReq函数(将req分叉到ireq的浅克隆中)
	if !deadline.IsZero() {
		forkReq()
	}

	// 6、(使用请求、往返器、截止时间)设置请求取消操作
	// 设置操作(会新增一个截止时间上下文给请求),然后返回:停止器函数、和一个是否超时判断函数
	stopTimer, didTimeout := setRequestCancel(req, rt, deadline)

	// 7、 执行请求,拿到响应
	resp, err = rt.RoundTrip(req)
	if err != nil {
		// 8、调用 停止计时器
		stopTimer()
		if resp != nil {
			log.Printf("RoundTripper returned a response & error; ignoring response")
		}
		// 9、处理错误
		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
	}

	// 8、 响应结果判断
	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 {
		// 如果ContentLength允许正文为空,请在此处填写一个空的,以确保它不是nil。
		// ody字段上的文档说“http客户机和传输保证Body始终是非nil的,即使在没有Body的响应或具有零长度Body的响应上也是如此。”
		//不幸的是,我们没有为任意RoundTripper实现记录相同的约束,并且RoundTripper实现在野外(主要是在测试中)假设它们可以使用nil体来表示空body类似于Request.Body)
		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(""))
	}

	// 9、如果截止时间有值,则响应体 是带有 截止时间器的 cancelTimerBody
	if !deadline.IsZero() {
		resp.Body = &cancelTimerBody{
			stop:          stopTimer,
			rc:            resp.Body,
			reqDidTimeout: didTimeout,
		}
	}
	// 10、返回响应结果
	return resp, nil, nil
}

③内部send()方法,调用了 设置请求取消操作setRequestCancel()函数

// setRequestCancel 设置req.Cancel然后新增一个截止时间上下文给请求。
// 入参:RoundTripper的类型用于确定是否应该使用传统的CancelRequest行为。
// 反参:返回一个 停止器函数、和一个是否超时判断函数
// 作为背景,有三种方法可以取消请求: (不赞成)① Transport.CancelRequest. ② Request.Cancel.③ Request.Context。此函数填充第二个和第三个
func setRequestCancel(req *Request, rt RoundTripper, deadline time.Time) (stopTimer func(), didTimeout func() bool) {
	// 1、如果截止时间为0,则直接返回nop空函数和 false
	if deadline.IsZero() {
		return nop, alwaysFalse
	}
	// 2、检查是否使用备用协议
	knownTransport := knownRoundTripperImpl(rt, req)

	// 3、取出 原始上下文
	oldCtx := req.Context()

	// 4、如果 请求的 Cancel通道没有关闭,且使用了 备用协议
	if req.Cancel == nil && knownTransport {
		// 且 如果 已经存在 上下文 快过期了,直接返回
		if !timeBeforeContextDeadline(deadline, oldCtx) {
			return nop, alwaysFalse
		}

		var cancelCtx func()
		// 5、如果上下文截止时间 比 deadline晚,则使用传入的截止时间 设置进上下文(方法③)
		// 然后 返回 取消的空函数,以及 入参的截止时间是否 已经超时
		req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
		return cancelCtx, func() bool { return time.Now().After(deadline) }
	}

	// 6、取出 请求 的Cancel通道  (一般是用户的原始请求,方法②)
	initialReqCancel := req.Cancel // the user's original Request.Cancel, if any

	var cancelCtx func()
	// 7、 取出上下文,判断 此时的上下文和 传入deadline时间哪个更早
	// 如果传入的时间更早, 则仍然设置进入 上下文
	if oldCtx := req.Context(); timeBeforeContextDeadline(deadline, oldCtx) {
		req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
	}

	// 8、给请求的取消通道重新 赋值一个 通道
	cancel := make(chan struct{})
	req.Cancel = cancel

	// doCancel() : 取消 操作 函数
	doCancel := func() {
		// 关闭 取消通道(方法二:② Request.Cancel)
		close(cancel)
		// 第一种方法,仅用于Go 1.5或Go 1.6之前编写的往返程序实现
		type canceler interface{ CancelRequest(*Request) }
		if v, ok := rt.(canceler); ok {
			v.CancelRequest(req)
		}
	}

	// 停止计时器 通道
	stopTimerCh := make(chan struct{})
	var once sync.Once
	// 停止计时器 函数:只停止一次
	stopTimer = func() {
		once.Do(func() {
			close(stopTimerCh)
			if cancelCtx != nil {
				cancelCtx()
			}
		})
	}

	// 9、使用入参截止时间 创建一个新的计时器
	timer := time.NewTimer(time.Until(deadline))
	var timedOut atomicBool

	// 10、开启协程
	go func() {
		select {
		// 如果是req.cancel,则调用doCancel() : 执行取消
		case <-initialReqCancel:
			doCancel()
			timer.Stop()
		// 如果是 截止时间的计时器 到点,则 设置超时,再执行取消
		case <-timer.C:
			timedOut.setTrue()
			doCancel()
		// 如果 是停止计时器的 通道关闭,则调用 截止时间的计时器停止
		case <-stopTimerCh:
			timer.Stop()
		}
	}()

	// 11、返回 停止计时器函数,和判断是否设置了超时函数
	return stopTimer, timedOut.isSet
}

⑤ Get() 使用get方式发送请求方法, 是客户端调用Get()方法的封装
client.Get() 方法调用client.Do() 实现

// Get请求:是DefaultClient.Get的封装。 任何返回的错误都属于 *url.Error.
func Get(url string) (resp *Response, err error) {
	return DefaultClient.Get(url)
}

// 客户端 对象 发Get请求方法
// 如果 要使用自定义标头发出请求,请使用 NewRequest and Client.Do.
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)
}

⑥ client.Do() 源码解读

// Do 发送一个HTTP请求并返回一个HTTP响应
// 307或308重定向保留原始HTTP方法和主体,前提是Request.GetBody函数已定义
func (c *Client) Do(req *Request) (*Response, error) {
	return c.do(req)
}
// Do的内部实现
func (c *Client) do(req *Request) (retres *Response, reterr error) {
	// 1、如果有测试,则函数结束后调用测试
	if testHookClientDoResult != nil {
		defer func() { testHookClientDoResult(retres, reterr) }()
	}

	// 2、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 {
		// 3、 判断请求 :如果有多个请求: 设置为下一个请求
		if len(reqs) > 0 {
			// 3.1 首先获取 请求头的Location
			loc := resp.Header.Get("Location")
			if loc == "" {
				resp.closeBody()
				return nil, uerr(fmt.Errorf("%d response missing Location header", resp.StatusCode))
			}
			// 3.2 解析请求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 := ""
			// 3.3 如果 请求诸暨 不等于 请求url的主机(如果调用者指定了自定义主机头并且重定向位置是相对的,则通过重定向保留主机头)
			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,
			}
			// 3.4  状态码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
			}

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

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

			// 3.7 检查重定向
			err = c.checkRedirect(req, reqs)

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

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

			// 3.9 关闭响应体
			resp.Body.Close()

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

		// 4、把所有的 请求 追加
		reqs = append(reqs, req)
		var err error
		var didTimeout func() bool

		// 5、 第一个请求:调用内部发送请求方法
		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)
		}

		// 6、下一个请求(重定向)
		var shouldRedirect bool
		redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
		if !shouldRedirect {
			return resp, nil
		}

		// 7、关闭请求体
		req.closeBody()
	}
}

其他方法请求,如Post和PostForm最终都是调用默认客户端DefaultClient的Post方法,然后调用Do来处理请求并获得相应信息。

// 客户端 对象内部实现发送Post请求的方法
func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	req, err := NewRequest("POST", url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", contentType)
	return c.Do(req)
}

//  发送 PostForm请求, DefaultClient.PostForm 的封装
func PostForm(url string, data url.Values) (resp *Response, err error) {
	return DefaultClient.PostForm(url, data)
}

实际应用示例


func main() {
	resp,err := http.Get("http://www.baidu.com")
	if err != nil {
		log.Fatal(err)
	}
 
	d,err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	resp.Body.Close()
 
	fmt.Println(string(d))
}

http客户端直接调用http包的Get获取相关请求数据。如果客户端在请求时需要设置header,则需要使用NewRequest和 DefaultClient.Do。如下:设置Header的操作例子

func main() {
	req,err := http.NewRequest("GET","http://www.baidu.com",nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("key","value")
	resp ,err :=  http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
 
	byts,err := ioutil.ReadAll(resp.Body)
	defer resp.Body.Close()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(byts))

综上示例讲解可以看到,Go语言标准库提供的HTTP Client 是相当优雅的。一方面提供了极其简单的使用方式,另一方面又具备极大的灵活性。

Go语言标准库提供的HTTP Client被设计成上下两层结构。一层是上述提到的 http.Client类及其封装的基础方法,我们不妨将其称为“业务层”。之所以称为业务层,是因为调用方通常只需要关心请求的业务逻辑本身,而无需关心非业务相关的技术细节,这些细节包括:

HTTP 底层传输细节
HTTP 代理
gzip 压缩
连接池及其管理
认证(SSL或其他认证方式)

之所以HTTP Client可以做到这么好的封装性,是因为HTTP Client在底层抽象了http.RoundTripper 接口,而http.Transport实现了该接口,从而能够处理更多的细节,我们将其称为“传输层”。HTTP Client在业务层初始化HTTP Method、目标URL、请求参数、请求内容等重要信息后,经过“传输层”,“传输层”在业务层处理的基础上补充其他细节,然后再发起 HTTP请求,接收服务端返回的 HTTP 响应。

 类似资料: