概述

在使用 Go 开发时几乎都会用到 net/http 标准库。但是,对库的内部实现不了解,仅限于会用。遇到问题容易懵,比如:

  • 长连接和短连接有什么区别?具体什么实现原理?
  • net/http 如何处理并发请求?
  • net/http 有用到缓存吗?缓存用来干什么?
  • ……

单个问题各个击破,不如深入梳理下 net/http 标准库一网打尽。本文将深入学习 net/http 标准库,力图做到知其然知其所以然。

服务端

源码走读

先看一段服务端示例代码:

 1func helloHandler(w http.ResponseWriter, r *http.Request) {  
 2    // 向客户端写入响应内容  
 3    fmt.Fprint(w, "Hello I am server 3")  
 4}  
 5  
 6func main() {  
 7    // 创建自定义的ServeMux实例  
 8    mux := http.NewServeMux()  
 9  
10    // 在mux上注册路由和处理函数  
11    mux.HandleFunc("/", helloHandler)  
12  
13    // 启动服务器并监听12302端口,使用自定义的mux作为handler  
14    fmt.Println("Server is listening on port 12302...")  
15    if err := http.ListenAndServe(":12302", mux); err != nil {  
16       // 错误处理  
17       fmt.Printf("Failed to start server: %v\n", err)  
18    }  
19}

示例中有几类概念需要介绍。

多路复用器 http.ServeMux

1type ServeMux struct {  
2  mu     sync.RWMutex  // 多路复用器锁
3  tree   routingNode   // 路由节点,用来存储路由 pattern 和对应的 handler
4  ... 
5}

路由处理器 handler

handler 是一个实现 func(ResponseWriter, *Request) 函数接口的函数,用来处理请求。

流程

服务端启动需要经过以下流程。

1) 创建多路复用器

创建自定义 http.ServeMux 多路复用器,如果不创建的话,则会使用默认 http.DefaultServeMux 多路复用器:

1// NewServeMux allocates and returns a new [ServeMux].  
2func NewServeMux() *ServeMux {  
3  return &ServeMux{}  
4}
5
6var DefaultServeMux = &defaultServeMux  
7  
8var defaultServeMux ServeMux

2) 注册路由处理器

调用多路复用器的 HandleFunc 方法注册路由处理器:

1func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {  
2  if use121 {  
3   mux.mux121.handleFunc(pattern, handler)  
4  } else {  
5   // 将 pattern 和路由处理器注册到多路复用器 
6   mux.register(pattern, HandlerFunc(handler))  
7  }  
8}

注册的过程实际是将 patternhandler 的映射关系写入 ServeMux.tree 中。可以根据请求的 patternServeMux.tree 中获取对应的 handler

3)监听服务

调用 http.ListenAndServe 监听服务:

 1func ListenAndServe(addr string, handler Handler) error {  
 2  server := &Server{Addr: addr, Handler: handler}  
 3  return server.ListenAndServe()  
 4}
 5
 6func (s *Server) ListenAndServe() error {  
 7  ...
 8  // 调用 net.Listen 获取 listener
 9  ln, err := net.Listen("tcp", addr)  
10  if err != nil {  
11   return err  
12  }  
13  return s.Serve(ln)  
14}
15
16func (s *Server) Serve(l net.Listener) error {
17  ...
18  for {  
19    // Accept waits for and returns the next connection to the listener.
20    rw, err := l.Accept()
21    ...
22    c := s.newConn(rw)  
23    // 异步启动协程处理请求
24    go c.serve(connCtx)  
25  }  
26}

监听服务监听到请求后会异步调用 conn.serve 启动协程处理请求:

 1func (c *conn) serve(ctx context.Context) {
 2  for {  
 3    // 读请求
 4    w, err := c.readRequest(ctx)
 5    ...
 6    // 调用多路复用器的 ServeHTTP 方法处理请求
 7    serverHandler{c.server}.ServeHTTP(w, w.req)
 8    ...
 9}
10
11func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {  
12  // 如果多路复用器为空,则用默认多路复用器
13  handler := sh.srv.Handler  
14  if handler == nil {  
15   handler = DefaultServeMux  
16  }  
17  ... 
18  
19  handler.ServeHTTP(rw, req)  
20}
21
22func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {  
23  ...
24  var h Handler  
25  if use121 {  
26   h, _ = mux.mux121.findHandler(r)  
27  } else {  
28   // 根据请求从多路复用器找到请求 pattern 对应的路由处理器
29   h, r.Pattern, r.pat, r.matches = mux.findHandler(r)  
30  }
31  
32  // 调用路由处理器的 ServeHTTP 方法处理请求  
33  h.ServeHTTP(w, r)  
34}

服务端的处理流程并不复杂,主要是注册路由处理器和回调路由处理器过程,流程图就不画了,接下来介绍客户端的处理逻辑,这是比较复杂的部分。

客户端

核心数据结构

Client

1type Client struct {
2  // 通信模块,负责和服务端建立通信
3  Transport RoundTripper
4  
5  // Cookie 模块,负责管理 Cookie
6  Jar CookieJar
7  
8  // 超时时间,这个时间是请求处理的总超时时间
9  Timeout time.Duration

RoundTripper

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

RoundTripper 是通信模块的 interface,需要实现方法 Roundtrip。通过传入请求 Request,与服务端交互后获得响应 Response

http.Transport

 1type Transport struct {  
 2  idleMu       sync.Mutex  
 3  ... 
 4  // 空闲连接,实现复用,每个连接只能被一个请求使用
 5  idleConn     map[connectMethodKey][]*persistConn 
 6  // 等待连接队列,需要等待的连接请求会放到 idleConnWait 中
 7  idleConnWait map[connectMethodKey]wantConnQueue  
 8  // 空闲连接 lru,结合 idleConn 根据连接时间管理连接
 9  idleLRU      connLRU
10  
11  // 建立长连接开关,如果为 true 则不复用连接
12  DisableKeepAlives bool

Transport 是实现了 RoundTripper 接口的方法,是用于通信的模块。

源码走读

客户端请求的示例如下:

 1func main() {
 2    resp, err := client.Get("http://localhost:12302/")  
 3    if err != nil {  
 4       // 错误处理  
 5       fmt.Printf("Failed to send request: %v\n", err)  
 6       return  
 7    }  
 8  
 9    // 读取响应体内容  
10    body, err := io.ReadAll(resp.Body)  
11    if err != nil {  
12       // 错误处理  
13       fmt.Printf("Failed to read response body: %v\n", err)  
14       return  
15    }  
16  
17    // 打印服务器返回的响应内容  
18    fmt.Printf("Server response: %s\n", body)  
19    if err := resp.Body.Close(); err != nil {  
20       fmt.Printf("Failed to close response body: %v\n", err)  
21    }  
22}

请求服务端

client.Get 调用 api 请求服务端,获取响应。

 1func (c *Client) Get(url string) (resp *Response, err error) {  
 2  // 构造请求体 request
 3  req, err := NewRequest("GET", url, nil)  
 4  if err != nil {  
 5   return nil, err  
 6  }  
 7  
 8  // 调用 client.Do(req) 获取响应
 9  return c.Do(req)  
10}
11
12func (c *Client) Do(req *Request) (*Response, error) {  
13  return c.do(req)  
14}
15
16func (c *Client) do(req *Request) (retres *Response, reterr error) {
17  ...
18  for {
19    ...
20    // 调用 client.send() 获取响应
21    if resp, didTimeout, err = c.send(req, deadline); err != nil {
22      ...
23    }
24    ...
25}

客户端调用 client.send() 发送请求到服务端,获取响应。

 1func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {  
 2  ...
 3  resp, didTimeout, err = send(req, c.transport(), deadline)  
 4  if err != nil {  
 5   return nil, didTimeout, err  
 6  }  
 7  ... 
 8  return resp, nil, nil  
 9}
10
11func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {  
12  ...
13  // 通信模块 Transport.RoundTrip 
14  resp, err = rt.RoundTrip(req)  
15  if err != nil {
16    ...
17  }
18  ...
19}

通信模块 Transport 开始接管通信过程。

 1func (t *Transport) RoundTrip(req *Request) (*Response, error) {  
 2  return t.roundTrip(req)  
 3}
 4
 5func (t *Transport) roundTrip(req *Request) (_ *Response, err error) {
 6  ...
 7  // 获取连接
 8  pconn, err := t.getConn(treq, cm)
 9  
10  var resp *Response  
11  if pconn.alt != nil {  
12    // HTTP/2 path.  
13    resp, err = pconn.alt.RoundTrip(req)  
14  } else {  
15    // 调用连接的 roundTrip 方法获取服务端响应
16    resp, err = pconn.roundTrip(treq)  
17  }
18  ...
19}

Transport.roundTrip 方法是这里的重点。主要包括两大逻辑:

  1. 调用 Transport.getConn 获取连接;
  2. 调用连接的 roundTrip 方法获取响应;

获取连接

 1func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (_ *persistConn, err error) {
 2  ...
 3  // 构造连接请求对象 wantConn
 4  w := &wantConn{  
 5    cm:         cm,  
 6    key:        cm.key(),  
 7    ctx:        dialCtx,  
 8    cancelCtx:  dialCancel,  
 9    result:     make(chan connOrError, 1),  
10    beforeDial: testHookPrePendingDial,  
11    afterDial:  testHookPostPendingDial,  
12  }  
13  defer func() {  
14    if err != nil {  
15      w.cancel(t, err)  
16    }  
17  }()
18  
19  // 获取连接
20  if delivered := t.queueForIdleConn(w); !delivered {  
21    t.queueForDial(w)  
22  }
23  
24  // 异步获取结果并处理 context
25  select {  
26    case r := <-w.result:
27      ...
28      return r.pc, r.err
29    case <-treq.ctx.Done():
30      ...
31  }
32}

Transport.getConn 首先构造连接请求对象 wantConn,然后根据 wantConn 调用 Transport.queueForIdleConn 获取连接。如果获取不到,调用 t.queueForDial 创建连接。

获取连接的过程是异步的,个人理解是创建连接的时间是不确定的,可以根据 context 上下文实现优雅退出,防止阻塞。

 1func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {  
 2  // 如果 disable keep alive 则返回
 3  if t.DisableKeepAlives {  
 4   return false  
 5  }
 6  
 7  ...
 8  // 判断连接是否存在于 Transport 中
 9  // 如果存在则复用连接,如果不存在则创建连接
10  if list, ok := t.idleConn[w.key]; ok {
11    stop := false  
12    delivered := false
13    for len(list) > 0 && !stop {
14      // 复用连接
15      pconn := list[len(list)-1]
16      ...
17      delivered = w.tryDeliver(pconn, nil, pconn.idleAt)
18      if delivered {
19	    ...
20      }
21    }
22    
23    // 更新 Transport.idleConn
24    // 同一个连接不能被多个请求使用,所以这里要删除 idleConn 中的连接
25    if len(list) > 0 {  
26      t.idleConn[w.key] = list  
27    } else {  
28      delete(t.idleConn, w.key)  
29    }  
30    if stop {  
31      return delivered  
32    }  
33  }
34  
35  // 连接不存在于 Transport.idleConn 中
36  // 将 wantConn 加入到 Transport.idleConnWait 队列中,等待连接
37  if t.idleConnWait == nil {  
38    t.idleConnWait = make(map[connectMethodKey]wantConnQueue)  
39  }  
40  q := t.idleConnWait[w.key]  
41  q.cleanFrontNotWaiting()  
42  q.pushBack(w)  
43  t.idleConnWait[w.key] = q  
44  return false
45}

Transport.queueForIdleConn 判断 Transport.idleConn 是否有空闲连接,如果有则调用 wantConn.tryDeliver 传递连接:

 1func (w *wantConn) tryDeliver(pc *persistConn, err error, idleAt time.Time) bool {  
 2  w.mu.Lock()  
 3  defer w.mu.Unlock()  
 4  ...
 5  // 实际是往 wantConn.result 通道中写 connOrError 对象,该对象中包括连接 pc
 6  w.result <- connOrError{pc: pc, err: err, idleAt: idleAt}  
 7  close(w.result)  
 8  
 9  return true  
10}

如果 Transport.idleConn 没有连接,则将 wantConn 加入等待队列 Transport.idleConnWait。然后调用 Transport.queueForDial 创建连接。

 1func (t *Transport) queueForDial(w *wantConn) {
 2  ...
 3  // 判断连接请求是否达到 Transport.connsPerHost 上限
 4  if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {  
 5    if t.connsPerHost == nil {  
 6      t.connsPerHost = make(map[connectMethodKey]int)  
 7    }  
 8    t.connsPerHost[w.key] = n + 1  
 9    // 如果没到请求上限,创建连接
10    t.startDialConnForLocked(w)  
11    return  
12  }
13  ...
14  // 如果达到请求上限,将请求加入等待队列
15  if t.connsPerHostWait == nil {  
16    t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)  
17  }  
18  q := t.connsPerHostWait[w.key]  
19  q.cleanFrontNotWaiting()  
20  q.pushBack(w)  
21  t.connsPerHostWait[w.key] = q
22}

Transport.queueForDial 判断连接是否达到 Transport.connsPerHost 上限。如果达到则将连接请求加入等待队列,如果未达到则调用 Transport.startDialConnForLocked 创建连接。

 1func (t *Transport) startDialConnForLocked(w *wantConn) {  
 2  ... 
 3  go func() {  
 4   // 调用 Transport.dialConnFor 创建连接
 5   t.dialConnFor(w)  
 6   t.connsPerHostMu.Lock()  
 7   defer t.connsPerHostMu.Unlock()  
 8   w.cancelCtx = nil  
 9  }()  
10}
11
12func (t *Transport) dialConnFor(w *wantConn) {  
13  ...
14  // 调用 Transport.dialConn 创建连接
15  pc, err := t.dialConn(ctx, w.cm)  
16  // deliver 创建的连接
17  delivered := w.tryDeliver(pc, err, time.Time{})  
18  ... 
19}

Transport.dialConnFor 调用 Transport.dialConn 创建连接,然后 deliver 该连接。

 1func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {  
 2  pconn = &persistConn{  
 3   t:             t,  
 4   cacheKey:      cm.key(),  
 5   reqch:         make(chan requestAndChan, 1),  
 6   writech:       make(chan writeRequest, 1),  
 7   closech:       make(chan struct{}),  
 8   writeErrCh:    make(chan error, 1),  
 9   writeLoopDone: make(chan struct{}),  
10  }  
11    
12  if cm.scheme() == "https" && t.hasCustomTLSDialer() {  
13   ... 
14  } else {  
15   // 调用 Transport.dial 创建连接
16   conn, err := t.dial(ctx, "tcp", cm.addr())  
17   ... 
18   // 将连接赋给 pconn
19   pconn.conn = conn    
20  } 
21  
22  // 将 Reader 和 Writter 赋给 pconn
23  // pconn.br 负责从连接中读服务端的响应,pconn.bw 负责写客户端请求到连接
24  pconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())  
25  pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())  
26  
27  // 异步启动两个伴生协程负责读写连接
28  go pconn.readLoop()  
29  go pconn.writeLoop()  
30  return pconn, nil  
31}

Transport.dialConn 这个方法很重要,它负责创建连接,创建连接的过程实际是和服务端进行三次握手建立 TCP 连接的过程。接着,启动两个伴生协程负责读写连接。

获取到连接后,会用这个连接获取服务端响应。

获取响应

 1func (t *Transport) roundTrip(req *Request) (_ *Response, err error) {
 2  ...
 3  for {
 4    // 获取连接
 5    pconn, err := t.getConn(treq, cm)
 6    ...
 7    
 8    var resp *Response  
 9    if pconn.alt != nil {  
10    // HTTP/2 path.  
11    resp, err = pconn.alt.RoundTrip(req)  
12    } else {  
13    // 通过连接获取响应
14    resp, err = pconn.roundTrip(treq)  
15    }
16  }
17  ...
18}
19
20func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
21  ...
22  // 写消息到连接的 writech 通道
23  pc.writech <- writeRequest{req, writeErrCh, continueCh}  
24  
25  resc := make(chan responseAndError)  
26  // 写消息到连接的 reqch 通道
27  pc.reqch <- requestAndChan{  
28    treq:       req,  
29    ch:         resc,  
30    addedGzip:  requestedGzip,  
31    continueCh: continueCh,  
32    callerGone: gone,  
33  }
34  
35  // 闭包函数处理响应
36  handleResponse := func(re responseAndError) (*Response, error) {  
37    ... 
38    return re.res, nil  
39  }
40  
41  for {
42    select {
43      ...
44      // 监听连接的 reqch.ch 通道
45      case re := <-resc:  
46        return handleResponse(re)
47    }
48  }
49}

在获取响应这里将 writeRequestrequestAndChan 写入通道 pc.writechpc.reqch 中,接着监听 pc.reqch.ch 通道,如果通道中有数据,则调用 handleResponse 处理响应。

那么,是谁在消费 pc.writechpc.reqch 呢?又是谁在往 pc.reqch.ch 写数据呢? 回答这个问题,需要看读写连接的伴生协程。

读写连接

写协程

 1func (pc *persistConn) writeLoop() {  
 2  defer close(pc.writeLoopDone)  
 3  for {  
 4   select {  
 5   // 监听 pc.writech 通道
 6   case wr := <-pc.writech:  
 7    startBytesWritten := pc.nwrite  
 8    // 写请求到连接,服务端会从连接中接受到该请求并返回响应
 9    err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))  
10    ...
11    pc.writeErrCh <- err // to the body reader, which might recycle us  
12    wr.ch <- err         // to the roundTrip function  
13    if err != nil {  
14     pc.close(err)  
15     return  
16    }  
17   // 如果收到关闭请求,则退出写协程 
18   case <-pc.closech:  
19    return  
20   }  
21  }  
22}

可以看到,写协程主要做了两件事:

  1. 作为消费者监听 pc.writech 通道,将请求写入连接;
  2. 阻塞等待 pc.closech,退出协程;

读协程

读协程负责从连接中读取服务端的响应数据。

 1func (pc *persistConn) readLoop() {  
 2  ...
 3  defer func() {  
 4   pc.close(closeErr)  
 5   pc.t.removeIdleConn(pc)  
 6  }()   
 7  ...
 8  alive := true  
 9  for alive {  
10   ...
11   // 阻塞请求通道
12   rc := <-pc.reqch  
13   trace := rc.treq.trace  
14  
15   var resp *Response  
16   if err == nil { 
17    // 从连接中读响应数据 
18    resp, err = pc.readResponse(rc, trace)  
19   } else {  
20    err = transportReadFromServerError{err}  
21    closeErr = err  
22   }    
23   ...
24   waitForBodyRead := make(chan bool, 2)  
25   // 构造 body
26   body := &bodyEOFSignal{  
27    body: resp.Body,  
28    ...  
29    fn: func(err error) error {  
30     isEOF := err == io.EOF  
31     waitForBodyRead <- isEOF  
32     if isEOF {  
33      <-eofc // see comment above eofc declaration  
34     } else if err != nil {  
35      if cerr := pc.canceled(); cerr != nil {  
36       return cerr  
37      }  
38     }  
39     return err  
40    },  
41   }  
42  
43   // 将构造的 body 赋值到 resp.Body
44   resp.Body = body  
45   ... 
46   select {  
47   // 将响应 resp 传递给 rc.ch 通道
48   case rc.ch <- responseAndError{res: resp}:  
49   case <-rc.callerGone:  
50    return  
51   }  
52  
53   select {  
54   // 阻塞等待
55   case bodyEOF := <-waitForBodyRead:  
56    alive = alive &&  
57     bodyEOF &&  
58     !pc.sawEOF &&  
59     pc.wroteRequest() &&  
60     tryPutIdleConn(rc.treq)  
61    if bodyEOF {  
62     eofc <- struct{}{}  
63    }  
64   case <-rc.treq.ctx.Done():  
65    alive = false  
66    pc.cancelRequest(context.Cause(rc.treq.ctx))  
67   // 退出协程
68   case <-pc.closech:  
69    alive = false  
70   }  
71  
72   rc.treq.cancel(errRequestDone)  
73   testHookReadLoopBeforeNextRead()  
74  }  
75}

读协程作为消费者读取 pc.reqch 通道,接着调用 persistConn.readResponse 读取响应。然后,构造响应 body,最后阻塞等待。

至此,基本过完了创建连接,读写连接的逻辑。下面介绍如何复用连接。

复用连接

将视线移到 persistConn.readLoop 构造 body 的逻辑:

 1   // 构造 body
 2   body := &bodyEOFSignal{  
 3    body: resp.Body,  
 4    ... 
 5    fn: func(err error) error {  
 6     isEOF := err == io.EOF  
 7     waitForBodyRead <- isEOF  
 8     if isEOF {  
 9      <-eofc // see comment above eofc declaration  
10     } else if err != nil {  
11      if cerr := pc.canceled(); cerr != nil {  
12       return cerr  
13      }  
14     }  
15     return err  
16    },  
17   }

body.fn 函数会写 isEOFwaitForBodyRead 通道,这个通道是读协程在消费:

 1func (pc *persistConn) readLoop() {
 2  ...
 3  for alive {
 4    select {
 5    // 接收 `waitForBodyRead`
 6    case bodyEOF := <-waitForBodyRead:  
 7      alive = alive &&  
 8       bodyEOF &&  
 9       !pc.sawEOF &&  
10       pc.wroteRequest() &&  
11       // 调用 tryPutIdleConn 将连接加入到空闲队列
12       tryPutIdleConn(rc.treq)  
13     if bodyEOF {  
14       eofc <- struct{}{}  
15      }  
16    ...
17  }
18  ...
19}

读协程在收到 waitForBodyRead 通道数据后,会根据一系列判断调用 tryPutIdleConn 将连接加入到 Transport 的空闲队列中。

  1func (pc *persistConn) readLoop() {  
  2  closeErr := errReadLoopExiting // default value, if not changed below  
  3  defer func() {  
  4   pc.close(closeErr)  
  5   pc.t.removeIdleConn(pc)  
  6  }()  
  7  
  8  tryPutIdleConn := func(treq *transportRequest) bool {  
  9   trace := treq.trace  
 10   // 调用 pc.t.tryPutIdleConn 添加连接
 11   if err := pc.t.tryPutIdleConn(pc); err != nil {  
 12    ... 
 13   }  
 14   ... 
 15   return true  
 16  }
 17  ...
 18}
 19
 20func (t *Transport) tryPutIdleConn(pconn *persistConn) error {  
 21  if t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 {  
 22   return errKeepAlivesDisabled  
 23  }  
 24  
 25  // 如果连接已经 broken 了,则返回 error
 26  if pconn.isBroken() {  
 27   return errConnBroken  
 28  }  
 29  
 30  // 标记连接是可复用的
 31  pconn.markReused()  
 32  
 33  // 加锁
 34  t.idleMu.Lock()  
 35  defer t.idleMu.Unlock()    
 36  
 37  // 获取连接的 key 
 38  key := pconn.cacheKey  
 39  // 判断连接的 key 是否在 Transport.idleConnWait 等待队列
 40  // 如果在等待队列中,则 remove 等待队列中的 wantConn
 41  if q, ok := t.idleConnWait[key]; ok {  
 42   // 如果连接在 Transport.idleConnWait 等待队列
 43   done := false  
 44   if pconn.alt == nil {  
 45    // HTTP/1.  
 46    // Loop over the waiting list until we find a w that isn't done already, and hand it pconn.   
 47    for q.len() > 0 {  
 48     w := q.popFront()  
 49     if w.tryDeliver(pconn, nil, time.Time{}) {  
 50      done = true  
 51      break  
 52     }  
 53    }  
 54   } else {  
 55    ... 
 56    }  
 57   }  
 58   if q.len() == 0 {  
 59    delete(t.idleConnWait, key)  
 60   } else {  
 61    t.idleConnWait[key] = q  
 62   }  
 63   if done {  
 64    return nil  
 65   }  
 66  }  
 67  
 68  // 如果空闲队列已经 close 了,退出
 69  if t.closeIdle {  
 70   return errCloseIdle  
 71  }  
 72  
 73  // 如果空闲队列为 nil,初始化空闲队列
 74  if t.idleConn == nil {  
 75   t.idleConn = make(map[connectMethodKey][]*persistConn)  
 76  }  
 77  
 78  // 建立请求和连接的映射存到空闲队列中
 79  idles := t.idleConn[key]  
 80  if len(idles) >= t.maxIdleConnsPerHost() {  
 81   return errTooManyIdleHost  
 82  }  
 83  
 84  for _, exist := range idles {  
 85   if exist == pconn {  
 86    log.Fatalf("dup idle pconn %p in freelist", pconn)  
 87   }  
 88  }  
 89  
 90  // 将连接加入到空闲队列中
 91  t.idleConn[key] = append(idles, pconn)  
 92  
 93  // 将连接加入到 lru 缓存,lru 缓存可以用来管理连接
 94  t.idleLRU.add(pconn)  
 95  if t.MaxIdleConns != 0 && t.idleLRU.len() > t.MaxIdleConns {  
 96   oldest := t.idleLRU.removeOldest()  
 97   oldest.close(errTooManyIdle)  
 98   t.removeIdleConnLocked(oldest)  
 99  }  
100  
101  ... 
102  return nil  
103}

Transport.tryPutIdleConn 中将连接加入到空闲队列和 LRU 缓存,后续的请求可以从空闲队列中复用连接。过期的连接会从空闲队列和 LRU 缓存移除。

基准测试

我们构造了三种场景用于测试性能:

  1. 连接池复用连接;
  2. 连接池不复用连接;
  3. 客户端复用连接,而服务端关闭连接;

具体代码实现在 这里

测试结果如下:

 1 go test -bench=. -benchmem -run=^$
 2goos: darwin
 3goarch: arm64
 4pkg: client
 5cpu: Apple M3
 6BenchmarkServerClosesConnection-8             21          52007139 ns/op           21505 B/op        139 allocs/op
 7BenchmarkWithConnectionPool-8                 51          21748487 ns/op           18985 B/op        127 allocs/op
 8BenchmarkWithoutConnectionPool-8              10         102032821 ns/op           23189 B/op        140 allocs/op
 9PASS
10ok      client  3.702s

可以看到,有复用的情况性能最好,无复用的情况性能最差,而客户端复用,服务端关闭连接的情况介于二者之间。个人猜测,虽然连接已经关闭了,但是还是有部分资源是可复用的,相比于无复用性能会好点。

小结

本文介绍了 net/http 标准库的服务端和客户端流程,相比于服务端,客户端要更复杂,大致画出客户端处理流程如下:

go-net-http.png

参考资料