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 响应。