http客户端介绍

         http 是典型的 C/S 架构,客户端向服务端发送请求(request),服务端做出应答(response)。本文章主要介绍Golang中http客户端的相关源码,源码主要在net/http/client.go中。源码版本号为1.10.3

一个简单的http 客户端请求例子:

package main
import (
	"log"
	"fmt"
	"net/http"
	"io/ioutil"
)

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的操作例子

package main

import (
	"fmt"
	"net/http"
	"log"
	"io/ioutil"
)

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))
}

      

源码分析

     Client结构体

 type Client struct {
	
	Transport RoundTripper

	CheckRedirect func(req *Request, via []*Request) error

	
	Jar CookieJar

	
	Timeout time.Duration
}

我们看到Client的结构体非常简单,只有几个字段。Client表示一个HTTP客户端,它的默认值是DefaultClient会默认使用DefaultTransport的可用客户端。

Transport表示HTTP事务,用于处理客户端的请求并等待服务端的响应

CheckRedirect 用于指定处理重定向的策略

Jar指定cookie的jar

Timeout 指定客户端请求的最大超时时间,该超时时间包括连接,任何的重定向以及读取响

应体的时间。如果服务器已经返回响应信息该计时器仍然运行,并将终端Response.Body的读取。如果Timeout为0则意味着没有超时

RoundTripper接口

type RoundTripper interface {
	RoundTrip(*Request) (*Response, error)
}

  RoundTripper表示执行单个HTTP事务的接口,必须是并发安全的。它的相关源码我们在前面已经介绍过了,可以参考以下地址

 

我们通过一些常用的http包对外提供的方法来窥探http客户端的请求流程和机制。

Get方法

func Get(url string) (resp *Response, err error) {

       return DefaultClient.Get(url)

}

var DefaultClient = &Client{}

 Get方法使用的是http包对外提供的默认客户端DefaultClient

 DefaultClient的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)
}

看到首先创建了Request结构体req,指定了请求的Method和url。

func (c *Client) Do(req *Request) (*Response, error) {
	//如果请求的URL为nil,则关闭请求体,并且返回error
	if req.URL == nil {
		req.closeBody()
		return nil, errors.New("http: nil Request.URL")
	}

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

		// Redirect behavior:
		redirectMethod string
		includeBody    bool
	)
	//错误处理函数
	uerr := func(err error) error {
		// the body may have been closed already by c.send()
		if !reqBodyClosed {
			req.closeBody()
		}
		method := valueOrDefault(reqs[0].Method, "GET")
		var urlStr string
		if resp != nil && resp.Request != nil {
			urlStr = resp.Request.URL.String()
		} else {
			urlStr = req.URL.String()
		}
		return &url.Error{
			Op:  method[:1] + strings.ToLower(method[1:]),
			URL: urlStr,
			Err: err,
		}
	}

	for {
		// For all but the first request, create the next
		// request hop and replace req.
		/*
		对于非第一个请求 ,创建下一个hop并且替换req
		*/
		if len(reqs) > 0 { //有重定向的情况
			loc := resp.Header.Get("Location")  //获取响应resp的Header中的Location对应的值
			if loc == "" {
				resp.closeBody()
				return nil, uerr(fmt.Errorf("%d response missing Location header", resp.StatusCode))
			}
			u, err := req.URL.Parse(loc) //将loc解析成URL
			if err != nil {
				resp.closeBody()
				return nil, uerr(fmt.Errorf("failed to parse Location header %q: %v", loc, err))
			}
			host := ""
			if req.Host != "" && req.Host != req.URL.Host {  //解析host
				// If the caller specified a custom Host header and the
				// redirect location is relative, preserve the Host header
				// through the redirect. See issue #22233.
				if u, _ := url.Parse(loc); u != nil && !u.IsAbs() {
					host = req.Host
				}
			}
			ireq := reqs[0]
			//获取重定向的Request  req
			req = &Request{
				Method:   redirectMethod,
				Response: resp,
				URL:      u,
				Header:   make(Header),
				Host:     host,
				Cancel:   ireq.Cancel,
				ctx:      ireq.ctx,
			}
			if includeBody && ireq.GetBody != nil {
				req.Body, err = ireq.GetBody()
				if err != nil {
					resp.closeBody()
					return nil, uerr(err)
				}
				req.ContentLength = ireq.ContentLength
			}

			// Copy original headers before setting the Referer,
			// in case the user set Referer on their first request.
			// If they really want to override, they can do it in
			// their CheckRedirect func.
			copyHeaders(req)

			// Add the Referer header from the most recent
			// request URL to the new one, if it's not https->http:
			/*
			如果不是https-> http,请将最新请求URL中的Referer标头添加到新标头中:
			*/
			if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {
				req.Header.Set("Referer", ref)
			}
			err = c.checkRedirect(req, reqs)

			// Sentinel error to let users select the
			// previous response, without closing its
			// body. See Issue 10069.
			if err == ErrUseLastResponse {
				return resp, nil
			}

			// Close the previous response's body. But
			// read at least some of the body so if it's
			// small the underlying TCP connection will be
			// re-used. No need to check for errors: if it
			// fails, the Transport won't reuse it anyway.
			const maxBodySlurpSize = 2 << 10
			if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
				io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
			}
			resp.Body.Close()

			if err != nil {
				// Special case for Go 1 compatibility: return both the response
				// and an error if the CheckRedirect function failed.
				// See https://golang.org/issue/3795
				// The resp.Body has already been closed.
				ue := uerr(err)
				ue.(*url.Error).URL = loc
				return resp, ue
			}
		}

		reqs = append(reqs, req)  //将req写入到reps中
		var err error
		var didTimeout func() bool
		//发送请求到服务端,并获取响应信息resp
		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)
		}

		var shouldRedirect bool
		//根据请求的Method,响应的消息resp和 第一次请求req获取是否需要重定向,重定向的方法,重定向时是否包含body
		redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
		if !shouldRedirect {  //不用重发,则返回
			return resp, nil
		}

		req.closeBody()
	}
}

Do发送一个HTTP请求并且返回一个客户端响应,遵循客户端上配置的策略(例如redirects,cookie,auth),如果服务端回复的Body非空,则该Body需要客户端关闭,否则会对”keep-alive”请求中的持久化TCP连接可能不会被复用。

如果服务端回复一个重定向,客户端首先会调用CheckRedirect函数来确定是否遵循重定向。

如果允许,一个301,302或者303重定向会导致后续请求使用HTTP的GET方法。

该方法的大致流程如下:

  1.首先会进行相关参数的校验

  2.参数校验通过后,会调用send方法来发送客户端请求,并获取服务端的响应信息。

  3.如果服务端回复的不需要重定向,则将该响应resp返回

  4.如果服务端回复的需要重定向,则获取重定向的Request,并进行重定向校验

  5.重定向校验通过后,会继续调用send方法来发送重定向的请求。

  6.不需要重定向时返回从服务端响应的结果resp。

 

send方法

func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
	//如果Jar不为nil,则将Jar中的Cookie添加到请求中
	if c.Jar != nil {
		for _, cookie := range c.Jar.Cookies(req.URL) {
			req.AddCookie(cookie)
		}
	}
	//发送请求到服务端,并返回从服务端读取到的response信息resp
	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函数将请求发送到Transport中,并返回response。

    如果Jar不为nil,则将Jar中的Cookie添加到请求中,并调用send函数将请求发送到服务端,并返回从服务端读取到的response信息resp。需要注意的是如果用户自定义了Transport则用用户自定义的,如果没有则用默认的DefaultTransport。

 

send函数

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  req是原始的请求或者一个拷贝

	/*
	条件判断:
	如果RoundTripper为nil 或者请求的URL为nil 或者 请求的RequestURI为空,则关闭请求体,返回error
	*/
	.......

	// forkReq forks req into a shallow clone of ireq the first
	// time it's called.
	/*
	第一次调用时,将req转换为ireq的拷贝
	*/
	forkReq := func() {
		if ireq == req {
			req = new(Request)
			*req = *ireq // shallow clone
		}
	}

	// Most the callers of send (Get, Post, et al) don't need
	// Headers, leaving it uninitialized. We guarantee to the
	// Transport that this has been initialized, though.
	/*
	由于大多数的调用(Get,Post等)都不需要Headers,而是将其保留成为初始化状态。不过我们传输到Transport需要保证其被初始化,所以这里将
	没有Header为nil的进行初始化
	如果请求头为nil
	*/
	if req.Header == nil {
		forkReq()
		req.Header = make(Header)
	}

	/*
	如果URL中协议用户和密码信息,并且请求头的Authorization为空,我们需要设置Header的Authorization
	*/
	if u := req.URL.User; u != nil && req.Header.Get("Authorization") == "" {
		username := u.Username()
		password, _ := u.Password()
		forkReq()
		req.Header = cloneHeader(ireq.Header)
		req.Header.Set("Authorization", "Basic "+basicAuth(username, password))
	}

	//如果设置了超时时间,则需要调用forkReq,来确保req是ireq的拷贝,而不是执行同一地址的指针
	if !deadline.IsZero() {
		forkReq()
	}

	//根据deadline设置超时
	stopTimer, didTimeout := setRequestCancel(req, rt, deadline)

	//调用RoundTrip完成一个HTTP事务,并返回一个resp
	resp, err = rt.RoundTrip(req)
	if err != nil { //如果err不为nil
		stopTimer() //取消监听超时
		if resp != nil {
			log.Printf("RoundTripper returned a response & error; ignoring response")
		}
		if tlsErr, ok := err.(tls.RecordHeaderError); ok {
			// If we get a bad TLS record header, check to see if the
			// response looks like HTTP and give a more helpful error.
			// See golang.org/issue/11111.
			if string(tlsErr.RecordHeader[:]) == "HTTP/" {
				err = errors.New("http: server gave HTTP response to HTTPS client")
			}
		}
		return nil, didTimeout, err
	}
	if !deadline.IsZero() { //如果设置了超时,则将Body转成cancelTimerBody
		resp.Body = &cancelTimerBody{
			stop:          stopTimer,
			rc:            resp.Body,
			reqDidTimeout: didTimeout,
		}
	}
	return resp, nil, nil
}

  该函数的主要作用是调用RoundTrip完成一个HTTP事务,并返回一个resp。

 

Post和PostForm方法

func Post(url string, contentType string, body io.Reader) (resp *Response, err error) {
	return DefaultClient.Post(url, contentType, body)
}

func (c *Client) Post(url string, 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)
}


func PostForm(url string, data url.Values) (resp *Response, err error) {
	return DefaultClient.PostForm(url, data)
}

func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error) {
	return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}

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

 

Request结构体和如何设置Header

     

type Request struct {
	
	//HTTP的方法(GET、POST、PUT等)
	Method string


	/*
	请求解析后的url
	*/
	URL *url.URL

	Proto      string // "HTTP/1.0" 协议版本
	ProtoMajor int    // 1  主版本号
	ProtoMinor int    // 0  次版本号

	//请求到服务器携带的请求头信息,用map存储
	Header Header

	
	//请求的消息体,HTTP客户端的Transport负责调用Close方法关闭
	Body io.ReadCloser

	
	/*
	GetBody 定义一个可选的方法用来返回Body的副本
	当客户端的请求被多次重定向的时候,会用到该函数
	*/
	GetBody func() (io.ReadCloser, error)

	
	/*
	ContentLength  存储消息体的字节长度
	如果为 -1 则表示消息长度未知
	*/
	ContentLength int64

	
	/*
	TransferEncoding列出从最外层到最内层的传输编码。
	空列表表示“identity”编码。 TransferEncoding通常可以忽略; 发送和接收请求时,会根据需要自动添加和删除分块编码。
	*/
	TransferEncoding []string

	
	/*
	Close对于服务端是 回复此请求后是否关闭连接
	对于客户端发送此请求并读取服务端的响应后是否关闭连接
	对于服务器请求,HTTP服务器自动处理此请求,处理程序不需要此字段。
	 对于客户端请求,设置此字段可防止在对相同主机的请求之间重复使用TCP连接,就像设置了Transport.DisableKeepAlives一样。
	*/
	Close bool

	/*
	主机地址
	对于服务器请求,Host指定要在其上查找URL的主机
	 对于客户端请求,Host可以选择覆盖要发送的Host头。 如果为空,则Request.Write方法使用URL.Host的值。 主机可能包含国际域名。
	*/
	Host string

	
	/*
	储存解析后的表单数据,包括URL字段查询的参数和 POST或PUT表单数据
	该字段仅在调用ParseForm后可用。 HTTP客户端忽略Form并使用Body。
	*/
	Form url.Values

	
	/*
	PostForm储存了 从POST,PATCH,PUT解析后表单数据
	该字段仅在调用ParseForm后可用,HTTP客户端会忽略PostForm而是使用Body
	*/
	PostForm url.Values

	
	/*
	MultipartForm是解析的多部分表单,包括文件上载。 该字段仅在调用ParseMultipartForm后可用。
	HTTP客户端忽略MultipartForm而使用Body
	*/
	MultipartForm *multipart.Form

	
	
	/*
	指定请求体发送之后发送的额外请求头
	*/
	Trailer Header

	
	/*
	RemoteAddr允许HTTP服务器和其他软件记录发送请求的网络地址,通常用于记录。 ReadRequest未填写此字段,并且没有已定义的格式。
	在调用处理程序之前,此程序包中的HTTP服务器将RemoteAddr设置为“IP:port”地址。 HTTP客户端忽略此字段。
	*/
	RemoteAddr string

	
	/*
	RequestURI是客户端发送到服务端的未经解析的Request-URI
	*/
	RequestURI string

	
	/*

TLS允许HTTP服务器和其他软件记录有关收到请求的TLS连接的信息。 ReadRequest未填写此字段。
	此包中的HTTP服务器在调用处理程序之前为启用TLS的连接设置字段; 否则它会离开现场零。 HTTP客户端忽略此字段。
	*/
	TLS *tls.ConnectionState

	
	/*
	用于通知客户端的请求应该被取消.不是所有的RoundTripper都支持取消
	此字段不使用服务端的请求
	*/
	Cancel <-chan struct{}

	
	/*
	重定向时使用该字段
	*/
	Response *Response

	/*
	客户端或者服务端的上下文。只有通过WithContext来改变该上下文
	*/
	ctx context.Context   //请求的上下文
}

Request表示一个HTTP的请求,用于客户端发送,服务端接收。

Method表示HTTP的方法(常用的如:GET、POST、PUT、DELETE等)

Header请求到服务器时携带的请求头信息,用map存储。

 

Header的格式如下:

type Header map[string][]string

Header用 key-value键值对 表示HTTP header

操作Header的常用方法如下:

//添加value到指定key,如果value已存在,则追加,因为value是[]string的格式。即一个// key可以对应多个value
func (h Header) Add(key, value string) {
	textproto.MIMEHeader(h).Add(key, value)
}

//设置key的value,value会替换key对应的已存在的value
func (h Header) Set(key, value string) {
	textproto.MIMEHeader(h).Set(key, value)
}

//获取与key关联的第一个value值,如果没有则返回""(空字符串),
func (h Header) Get(key string) string {
	return textproto.MIMEHeader(h).Get(key)
}


//删除Header中指定的key
func (h Header) Del(key string) {
	textproto.MIMEHeader(h).Del(key)
}

以上就是http客户端请求的大致流程及相关方法和结构体的介绍,如有问题还请指正