写在开始:
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的设计细节, 关注连接池的使用, 来达到更高的吞吐量, 这些地方优化好, 是可以整体提高我们程序的性能.