转自
Golang http之server源码详解-蒲公英云
仅做个人备份
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事务的接口,必须是并发安全的。它的相关源码我们在前面已经介绍过了,可以参考以下地址
https://blog.csdn.net/skh2015java/article/details/89340215
我们通过一些常用的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客户端请求的大致流程及相关方法和结构体的介绍,如有问题还请指正