http协议是业务中使用最广泛,开发者接触最多的协议之一。最近笔者我也是因为业务中遇到一些问题,才深入阅读一些源码,带着问题来学习,能学习得更好,更有效果。
发起http的请求主流程如下,这里整理了发起一个http请求的主函数调用,函数所在的文件,以及函数功能说明。
下面我们来看不同的模块的代码:
1 Client对象
// a Client是一个HTTP的client
// Client Transport保存了tcp的连接请求,支持http详情的配置
type Client struct {
// 发送HTTP请求。
Transport RoundTripper
// CheckRedirect指定处理重定向的策略
// 默认是为nil
CheckRedirect func(req *Request, via []*Request) error
// Jar指定cookie 是保存cookie的对象
Jar CookieJar
// Timeout指定请求的时间限制
Timeout time.Duration
}
2 NewRequest对象
// NewRequest从新包装NewRequestWithContext。
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
// 组装请求信息,其实主要是Request{}结构,一些简单校验
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
if method == "" {
// 判断method为空,则赋值是GET
method = "GET"
}
// 验收method的字符是否是26个字母,如果是则报invalid method。
if !validMethod(method) {
return nil, fmt.Errorf("net/http: invalid method %q", method)
}
// 判断ctx是否是空
if ctx == nil {
return nil, errors.New("net/http: nil Context")
}
// 解析url是否是正常的
u, err := urlpkg.Parse(url)
if err != nil {
return nil, err
}
// 断言body的类型,如果失败,则给rc重新赋值
rc, ok := body.(io.ReadCloser)
if !ok && body != nil {
rc = ioutil.NopCloser(body)
}
// 处理u.Host可能带有端口
u.Host = removeEmptyPort(u.Host)
req := &Request{
ctx: ctx,
Method: method,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(Header),
Body: rc,
Host: u.Host,
}
if body != nil {
// 断言body的类型
switch v := body.(type) {
case *bytes.Buffer:
req.ContentLength = int64(v.Len())
buf := v.Bytes()
req.GetBody = func() (io.ReadCloser, error) {
r := bytes.NewReader(buf)
return ioutil.NopCloser(r), nil
}
case *bytes.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return ioutil.NopCloser(&r), nil
}
case *strings.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return ioutil.NopCloser(&r), nil
}
default:
// body的类型断言失败,不处理
}
// 对于body如果不等于nil,但是content-length又是0,则对req.Body和req.GetBody进行重新赋值。
if req.GetBody != nil && req.ContentLength == 0 {
req.Body = NoBody
req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
}
}
return req, nil
}
3 Do(req *Request)对象
// 调用do函数
func (c *Client) Do(req *Request) (*Response, error) {
return c.do(req)
}
func (c *Client) do(req *Request) (retres *Response, reterr error) {
// 如果有测试,则函数结束后调用测试
if testHookClientDoResult != nil {
defer func() { testHookClientDoResult(retres, reterr) }()
}
//req.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 {
// 判断reqs是否为空,不为空则进行下面流程判断
if len(reqs) > 0 {
// 首先获取 请求头的Location
loc := resp.Header.Get("Location")
if loc == "" {
resp.closeBody()
return nil, uerr(fmt.Errorf("%d response missing Location header", resp.StatusCode))
}
// 解析请求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 := ""
// 如果请求的host 不等于请求的url的host
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,
}
// 状态码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
}
// 在设置Referer之前复制原始头,以防用户在第一次请求时设置Referer; 如果他们真的想重写,他们可以在CheckRedirect函数中完成
copyHeaders(req)
// 将Referer头从最近的请求URL添加到新的请求URL,如果不是https->http
if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {
req.Header.Set("Referer", ref)
}
// 检查重定向
err = c.checkRedirect(req, reqs)
// Sentinel error允许用户选择上一个响应,而不关闭其 响应体. See Issue 10069.
if err == ErrUseLastResponse {
return resp, nil
}
// 检查响应体长度。 关闭上一个响应体。但至少要读一些正文,这样如果它很小,底层的TCP连接就会被重用。无需检查错误:如果失败,Transport无论如何也不会重用它
const maxBodySlurpSize = 2 << 10
if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
// 从 响应体 复制maxBodySlurpSize 个字节,它返回复制的字节
io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
}
// 关闭响应体
resp.Body.Close()
if err != nil {
// 兼容性的特殊情况:如果CheckRedirect函数失败,则返回响应和错误。
ue := uerr(err)
ue.(*url.Error).URL = loc
return resp, ue
}
}
// 把req追加到reqs
reqs = append(reqs, req)
var err error
var didTimeout func() bool
// 第一个请求:调用内部发送请求方法
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)
}
// 判断是否冲行动
var shouldRedirect bool
redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
if !shouldRedirect {
return resp, nil
}
// 关闭请求体
req.closeBody()
}
}
4 client.send方法,发送请求
// 客户机send()方法:客户端 发送请求获取响应 内部方法 只有在出现错误时,didTimeout才是非nil
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
// 如果客户端的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
}
// 如果客户端cookie不为空,则会把cookie设置请求的cookie
if c.Jar != nil {
if rc := resp.Cookies(); len(rc) > 0 {
c.Jar.SetCookies(req.URL, rc)
}
}
return resp, nil, nil
}
5 send方法,调用发送HTTP request
// send() 方法发出一个HTTP请求。 调用方 在读完响应体时应关闭request请求
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
// 如果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")
}
// 请求的URI不能被设置在客户端请求上
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
}
}
// 如果请求头是空的,则调用forkReq函数(将req分叉到ireq的浅克隆中),然后初始化请求头
if req.Header == nil {
forkReq()
req.Header = make(Header)
}
// 取出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))
}
// 如果 截止时间 有值,给 再次调用forkReq函数(将req分叉到ireq的浅克隆中)
if !deadline.IsZero() {
forkReq()
}
stopTimer, didTimeout := setRequestCancel(req, rt, deadline)
// 执行请求,拿到响应
resp, err = rt.RoundTrip(req)
if err != nil {
// 调用 停止计时器
stopTimer()
if resp != nil {
log.Printf("RoundTripper returned a response & error; ignoring response")
}
// 处理错误
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
}
// 响应结果判断
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 {
// 处理响应body为nil的情况,处理一些默认赋值
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(""))
}
// 如果截止时间有值,则响应体 是带有 截止时间器的 cancelTimerBody
if !deadline.IsZero() {
resp.Body = &cancelTimerBody{
stop: stopTimer,
rc: resp.Body,
reqDidTimeout: didTimeout,
}
}
//这里是最终的结果正确响应的返回。
return resp, nil, nil
}
关于发起http的client.go的源码就分享到这里。关于上面的源码阅读,从一定程度上解决了笔者的问题,开头笔者说过业务中遇到了一些问题。也从阅读源码中了解switch case 可以对interface进行断言,对http发送client请求的整个流程更加熟悉了,如何使用http.Client也是一种技巧。
总结:
( 1 )对interface进行断言,可以通过switch case,这样代码更加优雅(下面贴个示例)。
package test
import (
"fmt"
"testing"
)
func Test_Interface(t *testing.T) {
checkSwitchInterface(10)
}
// 通过switch case 断言判断arr的类型
func checkSwitchInterface(arr interface{}) {
switch t := arr.(type) {
case int:
fmt.Println("int:", t)
case string:
fmt.Println("string:", t)
default:
fmt.Println("未知")
}
}
( 2 )客户端的Timeout实现是通过context,通过setRequestCancel方法起一个开启一个定时器,然后起一个协程来监听定时器timer的结果,最后调用到transport的CancelRequest取消请求的方法。
( 3 )http.Client在大量发送http请求的时候最好声明一个全局变量,尽量不要每次发送一个请求声明一个http.Client,方便统一申明transport来控制复用tcp请求。
注意:
1、http请求第一篇文章,是个总括,第二章会更加详细讲解tcp的复用,参数控制的一些注意事项。 2、笔者本着严谨的态度,流程中的很多细节并未详细提及或讲述,请读者酌情参考。
参考文档
https://blog.csdn.net/yyj18085644/article/details/112994499