您现在的位置是:网站首页> 编程资料编程资料
快速掌握Go 语言 HTTP 标准库的实现方法_Golang_
2023-05-26
436人已围观
简介 快速掌握Go 语言 HTTP 标准库的实现方法_Golang_
本篇文章来分析一下 Go 语言 HTTP 标准库是如何实现的。
本文使用的go的源码1.15.7
基于HTTP构建的服务标准模型包括两个端,客户端(Client)和服务端(Server)。HTTP 请求从客户端发出,服务端接受到请求后进行处理然后将响应返回给客户端。所以http服务器的工作就在于如何接受来自客户端的请求,并向客户端返回响应。
一个典型的 HTTP 服务应该如图所示:

HTTP client
在 Go 中可以直接通过 HTTP 包的 Get 方法来发起相关请求数据,一个简单例子:
func main() { resp, err := http.Get("http://httpbin.org/get?name=luozhiyun&age=27") if err != nil { fmt.Println(err) return } defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body)) }我们下面通过这个例子来进行分析。
HTTP 的 Get 方法会调用到 DefaultClient 的 Get 方法,DefaultClient 是 Client 的一个空实例,所以最后会调用到 Client 的 Get 方法:

Client 结构体
type Client struct { Transport RoundTripper CheckRedirect func(req *Request, via []*Request) error Jar CookieJar Timeout time.Duration }Client 结构体总共由四个字段组成:
Transport:表示 HTTP 事务,用于处理客户端的请求连接并等待服务端的响应;
CheckRedirect:用于指定处理重定向的策略;
Jar:用于管理和存储请求中的 cookie;
Timeout:指定客户端请求的最大超时时间,该超时时间包括连接、任何的重定向以及读取相应的时间;
初始化请求
func (c *Client) Get(url string) (resp *Response, err error) { // 根据方法名、URL 和请求体构建请求 req, err := NewRequest("GET", url, nil) if err != nil { return nil, err } // 执行请求 return c.Do(req) }我们要发起一个请求首先需要根据请求类型构建一个完整的请求头、请求体、请求参数。然后才是根据请求的完整结构来执行请求。
NewRequest 初始化请求
NewRequest 会调用到 NewRequestWithContext 函数上。这个函数会根据请求返回一个 Request 结构体,它里面包含了一个 HTTP 请求所有信息。
Request
Request 结构体有很多字段,我这里列举几个大家比较熟悉的字段:

NewRequestWithContext
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) { ... // parse url u, err := urlpkg.Parse(url) if err != nil { return nil, err } rc, ok := body.(io.ReadCloser) if !ok && body != nil { rc = ioutil.NopCloser(body) } 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, } ... return req, nil }NewRequestWithContext 函数会将请求封装成一个 Request 结构体并返回。
准备 http 发送请求

如上图所示,Client 调用 Do 方法处理发送请求最后会调用到 send 函数中。
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { resp, didTimeout, err = send(req, c.transport(), deadline) if err != nil { return nil, didTimeout, err } ... return resp, nil, nil }Transport
Client 的 send 方法在调用 send 函数进行下一步的处理前会先调用 transport 方法获取 DefaultTransport 实例,该实例如下:
var DefaultTransport RoundTripper = &Transport{ // 定义 HTTP 代理策略 Proxy: ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, ForceAttemptHTTP2: true, // 最大空闲连接数 MaxIdleConns: 100, // 空闲连接超时时间 IdleConnTimeout: 90 * time.Second, // TLS 握手超时时间 TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }
Transport 实现 RoundTripper 接口,该结构体会发送 http 请求并等待响应。
type RoundTripper interface { RoundTrip(*Request) (*Response, error) }从 RoundTripper 接口我们也可以看出,该接口定义的 RoundTrip 方法会具体的处理请求,处理完毕之后会响应 Response。
回到我们上面的 Client 的 send 方法中,它会调用 send 函数,这个函数主要逻辑都交给 Transport 的 RoundTrip 方法来执行。

RoundTrip 会调用到 roundTrip 方法中:
func (t *Transport) roundTrip(req *Request) (*Response, error) { t.nextProtoOnce.Do(t.onceSetNextProtoDefaults) ctx := req.Context() trace := httptrace.ContextClientTrace(ctx) ... for { select { case <-ctx.Done(): req.closeBody() return nil, ctx.Err() default: } // 封装请求 treq := &transportRequest{Request: req, trace: trace, cancelKey: cancelKey} cm, err := t.connectMethodForRequest(treq) if err != nil { req.closeBody() return nil, err } // 获取连接 pconn, err := t.getConn(treq, cm) if err != nil { t.setReqCanceler(cancelKey, nil) req.closeBody() return nil, err } // 等待响应结果 var resp *Response if pconn.alt != nil { // HTTP/2 path. t.setReqCanceler(cancelKey, nil) // not cancelable with CancelRequest resp, err = pconn.alt.RoundTrip(req) } else { resp, err = pconn.roundTrip(treq) } if err == nil { resp.Request = origReq return resp, nil } ... } }roundTrip 方法会做两件事情:
- 调用 Transport 的 getConn 方法获取连接;
- 在获取到连接后,调用 persistConn 的 roundTrip 方法等待请求响应结果;获取连接 getConn
getConn 有两个阶段:
调用 queueForIdleConn 获取空闲 connection;调用 queueForDial 等待创建新的 connection;

func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) { req := treq.Request trace := treq.trace ctx := req.Context() if trace != nil && trace.GetConn != nil { trace.GetConn(cm.addr()) } // 将请求封装成 wantConn 结构体 w := &wantConn{ cm: cm, key: cm.key(), ctx: ctx, ready: make(chan struct{}, 1), beforeDial: testHookPrePendingDial, afterDial: testHookPostPendingDial, } defer func() { if err != nil { w.cancel(t, err) } }() // 获取空闲连接 if delivered := t.queueForIdleConn(w); delivered { pc := w.pc ... t.setReqCanceler(treq.cancelKey, func(error) {}) return pc, nil } // 创建连接 t.queueForDial(w) select { // 获取到连接后进入该分支 case <-w.ready: ... return w.pc, w.err ... } 获取空闲连接 queueForIdleConn
成功获取到空闲 connection:

成功获取 connection 分为如下几步:
- 根据当前的请求的地址去空闲 connection 字典中查看存不存在空闲的 connection 列表;
- 如果能获取到空闲的 connection 列表,那么获取到列表的最后一个 connection;
- 返回;
获取不到空闲 connection:

当获取不到空闲 connection 时:
- 根据当前的请求的地址去空闲 connection 字典中查看存不存在空闲的 connection 列表;
- 不存在该请求的 connection 列表,那么将该 wantConn 加入到 等待获取空闲 connection 字典中;
从上面的图解应该就很能看出这一步会怎么操作了,这里简要的分析一下代码,让大家更清楚里面的逻辑:
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) { if t.DisableKeepAlives { return false } t.idleMu.Lock() defer t.idleMu.Unlock() t.closeIdle = false if w == nil { return false } // 计算空闲连接超时时间 var oldTime time.Time if t.IdleConnTimeout > 0 { oldTime = time.Now().Add(-t.IdleConnTimeout) } // Look for most recently-used idle connection. // 找到key相同的 connection 列表 if list, ok := t.idleConn[w.key]; ok { stop := false delivered := false for len(list) > 0 && !stop { // 找到connection列表最后一个 pconn := list[len(list)-1] // 检查这个 connection 是不是等待太久了 tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime) if tooOld { go pconn.closeConnIfStillIdle() } // 该 connection 被标记为 broken 或 闲置太久 continue if pconn.isBroken() || tooOld { list = list[:len(list)-1] continue } // 尝试将该 connection 写入到 w 中 delivered = w.tryDeliver(pconn, nil) if delivered { // 操作成功,需要将 connection 从空闲列表中移除 if pconn.alt != nil { } else { t.idleLRU.remove(pconn) list = list[:len(list)-1] } } stop = true } if len(list) > 0 { t.idleConn[w.key] = list } else { // 如果该 key 对应的空闲列表不存在,那么将该key从字典中移除 delete(t.idleConn, w.key) } if stop { return delivered } } // 如果找不到空闲的 connection if t.idleConnWait == nil { t.idleConnWait = make(map[connectMethodKey]wantConnQueue) } // 将该 wantConn 加入到 等待获取空闲 connection 字典中 q := t.idleConnWait[w.key] q.cleanFront() q.pushBack(w) t.idleConnWait[w.key] = q return false }上面的注释已经很清楚了,我这里就不再解释了。
建立连接 queueForDial

在获取不到空闲连接之后,会尝试去建立连接,从上面的图大致可以看到,总共分为以下几个步骤:
- 在调用 queueForDial 方法的时候会校验 MaxConnsPerHost 是否未设置或已达上限;
- 检验不通过则将当前的请求放入到 connsPerHostWait 等待字典中;
- 如果校验通过那么会异步的调用 dialConnFor 方法创建连接;
dialConnFor 方法首先会调用 dialConn 方法创建 TCP 连接,然后启动两个异步线程来处理读写数据,然后调用 tryDeliver 将连接绑定到
相关内容
- go单体日志采集zincsearch方案实现_Golang_
- golang构建工具Makefile使用详解_Golang_
- Makefile构建Golang项目示例详解_Golang_
- Golang中的包及包管理工具go mod详解_Golang_
- go mode tidy出现报错go: warning: “all“ matched no packages的解决方法_Golang_
- go Antlr重构脚本解释器实现示例_Golang_
- Go Excelize API源码阅读Close及NewSheet方法示例解析_Golang_
- Golang实现HTTP编程请求和响应_Golang_
- Golang Http请求返回结果处理_Golang_
- golang NewRequest/gorequest实现http请求的示例代码_Golang_
