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客户端请求的大致流程及相关方法和结构体的介绍,如有问题还请指正