http keep alive

写在开始:

Connection: keep-alive

keep-alive特性如何开启

Connection: keep-alivekeep-alive

服务端默认开启keep-alive的源码分析

这一点可以从golang源码中看到:

首先我们看作为服务端, 当启动一个server后, 首先listen, 然后发起server处理:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}
func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
        // 这里创建tcp链接
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
        // 这里开始接受tcp数据包, 进行处理
    return srv.Serve(ln)
}

之后进入for循环, 一个一个的请求都将启动新的协程去处理, 由此我们也可以发现, 每一个http的请求都是通过新的协程去处理

for {
        rw, err := l.Accept()
                // 此处省略N行代码
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(connCtx)
    }
servedoKeepAliveshttp keepalivehttp.Server.SetKeepAlivesEnabled(false)
keep-alive
net/http/client.go
if resp, didTimeout, err = c.send(req, deadline); err != nil {
resp, err = rt.RoundTrip(req)

最后queueForIdleConn方法可以得知, 我们的客户端在当前client结构体实例上是预先申请好连接的, 所以也就得到了keep-alive的目的.

// queueForIdleConn queues w to receive the next idle connection for w.cm.
// As an optimization hint to the caller, queueForIdleConn reports whether
// it successfully delivered an already-idle connection.
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
    if t.DisableKeepAlives {
        return false
    }

    t.idleMu.Lock()
    defer t.idleMu.Unlock()
TransportDisableKeepAlives

关于客户端连接池设计中的几个重要的参数

DisableKeepAlives

这个在上文中已经说过, 就是客户端Transport实例明确告诉服务端, 是否要开启长连接.

MaxConnsPerHost

代表每个客户端可以在一个host上创建最多几个连接
getConn
// 这里获取连接池中的空闲链接
if delivered := t.queueForIdleConn(w); delivered {
    pc := w.pc
    // Trace only for HTTP/1.
    // HTTP/2 calls trace.GotConn itself.
    if pc.alt == nil && trace != nil && trace.GotConn != nil {
        trace.GotConn(pc.gotIdleConnTrace(pc.idleAt))
    }
    // set request canceler to some non-nil function so we
    // can detect whether it was cleared between now and when
    // we enter roundTrip
    t.setReqCanceler(treq.cancelKey, func(error) {})
    return pc, nil
}

cancelc := make(chan error, 1)
t.setReqCanceler(treq.cancelKey, func(err error) { cancelc <- err })

// 如果当前连接池中无空闲链接, 则开始异步创建连接
t.queueForDial(w)

进入 创建连接代码

// queueForDial queues w to wait for permission to begin dialing.
// Once w receives permission to dial, it will do so in a separate goroutine.
func (t *Transport) queueForDial(w *wantConn) {
    w.beforeDial()
        // 如果MaxConnsPerHost不够, 创建
    if t.MaxConnsPerHost <= 0 {
        go t.dialConnFor(w)
        return
    }

    t.connsPerHostMu.Lock()
    defer t.connsPerHostMu.Unlock()

    if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {
        if t.connsPerHost == nil {
            t.connsPerHost = make(map[connectMethodKey]int)
        }
        t.connsPerHost[w.key] = n + 1
        go t.dialConnFor(w)
        return
    }

    if t.connsPerHostWait == nil {
        t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)
    }
    q := t.connsPerHostWait[w.key]
    q.cleanFront()
    q.pushBack(w)
    t.connsPerHostWait[w.key] = q
}

总结:

http 作为开发者是最最常用的类库, 其中有一些设计细节是有异于其他http类库的, 我们更需要keep-avlie的设计细节, 关注连接池的使用, 来达到更高的吞吐量, 这些地方优化好, 是可以整体提高我们程序的性能.