长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。
TCP/IP是个协议组,可分为三个层次:网络层、传输层和应用层。
在网络层有IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。
在传输层中有TCP协议与UDP协议。
在应用层有FTP、HTTP、TELNET、SMTP、DNS等协议。
比如http的,只是连接、请求、关闭,过程时间较短,服务器若是一段时间内没有收到请求即可关闭连接。
其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。
通常的短连接操作步骤是: 连接→数据传输→关闭连接;
而长连接通常就是: 连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
这就要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态,短连接在没有数据传输时直接关闭就行了。
长连接
短连接
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三次握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,再次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
像WEB网站的http服务一般都用短连接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连接好。
参考文章:
详解golang net之transport:https://www.cnblogs.com/charlieroro/p/11409153.html
作为一名Golang开发者,你可能会遇到线上环境遇到连接数暴增问题。
纠其原因,Golang作为常驻进程,请求第三方服务或者资源完毕后,需要手动关闭连接,否则连接会一直存在。而很多时候,开发者不一定记得关闭这个连接。
那么你可能会问,我可以在程序中defer主动关闭连接啊!需要知道的是,连接相对于其他对象,创建成本较高,资源也有限。如果没有连接池,在高并发场景下,连接关闭又新建,很快就会因为过多的TIME_WAIT(连接主动关闭方)导致无法创建更多连接了,程序被压死。
那么这样是不是很麻烦很头疼?于是有了连接池。顾名思义,连接池就是管理连接的;我们从连接池获取连接,请求完毕后再将连接还给连接池;连接池帮我们做了连接的建立、复用以及回收工作。
在设计与实现连接池时,我们通常需要考虑以下几个问题:
Transport:为http.RoundTripper接口,定义功能为负责http的请求分发。实际功能由结构体net/http/transport.go中的Transport struct继承并实现,除了请求发分还实现了对空闲连接的管理。如果创建client时不定义,就用系统默认配置。
Transport结构定义如下:
type Transport struct {
//操作空闲连接需要获取锁
idleMu sync.Mutex
//空闲连接池,key为协议目标地址等组合
idleConn map[connectMethodKey][]*persistConn // most recently used at end
//等待空闲连接的队列,基于切片实现,队列大小无限制
idleConnWait map[connectMethodKey]wantConnQueue // waiting getConns
//排队等待建立连接需要获取锁
connsPerHostMu sync.Mutex
//每个host建立的连接数
connsPerHost map[connectMethodKey]int
//等待建立连接的队列,同样基于切片实现,队列大小无限制
connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns
//tls client用于tls协商的配置
TLSClientConfig *tls.Config
//tls协商的超时时间
TLSHandshakeTimeout time.Duration
//是否取消长连接,默认使用长连接
DisableKeepAlives bool
//是否取消HTTP压缩
DisableCompression bool
//所有host的连接池最大连接数量,默认无穷大
MaxIdleConns int
//每个目标host最大空闲连接数;默认为2(注意默认值)
MaxIdleConnsPerHost int
//对每个host可建立的最大连接数量,0表示不限制
MaxConnsPerHost int
//连接多少时间没有使用则被关闭
IdleConnTimeout time.Duration
//发送完request后等待serve response的时间
ResponseHeaderTimeout time.Duration
//限制客户端在发送一个包含:100-continue的http报文头后,等待收到一个go-ahead响应报文所用的时间。
ExpectContinueTimeout time.Duration
//在tls协商带NPN/ALPN的扩展后,transport如何切换到其他协议。指tls之上的协议(next指的就是tls之上的意思)
TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper
//在CONNECT请求时,配置request的首部信息,可选
ProxyConnectHeader Header
//指定server响应首部的最大字节数
MaxResponseHeaderBytes int64
//写bufffer的大小,默认为4096。
WriteBufferSize int
//读bufffer的大小,默认为4096。
ReadBufferSize int
//是否启用HTTP/2,默认为启用
ForceAttemptHTTP2 bool
}
需要特别注意的是,MaxIdleConnsPerHost
默认等于2,即与目标主机最多只维护两个空闲连接。这会导致什么呢?
如果遇到突发流量,瞬间建立大量连接,但是回收连接时,由于最大空闲连接数的限制,该联机不能进入空闲连接池,只能直接关闭。结果是,一直新建大量连接,又关闭大量连,业务机器的TIME_WAIT连接数随之突增。
最后,Transport也提供了配置DisableKeepAlives,禁用长连接,使用短连接访问第三方资源或者服务。
Transport结构提供下面两个方法实现连接的获取与回收操作。
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {}
func (t *Transport) tryPutIdleConn(pconn *persistConn) error {}
连接的获取主要分为两步走: 1)尝试获取空闲连接; 2)尝试新建连接:
//getConn方法内部实现
if delivered := t.queueForIdleConn(w); delivered {
return pc, nil
}
t.queueForDial(w)
当然,可能获取不到连接而需要排队,此时怎么办呢?当前会阻塞当前协程了,直到获取连接为止,或者httpclient超时取消请求:
select {
case <-w.ready:
return w.pc, w.err
//超时被取消
case <-req.Cancel:
return nil, errRequestCanceledConn
……
}
var errRequestCanceledConn = errors.New("net/http: request canceled while waiting for connection") // TODO: unify?
排队等待空闲连接的逻辑如下:
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
//如果配置了空闲超时时间,获取到连接需要检测,超时则关闭连接
if t.IdleConnTimeout > 0 {
oldTime = time.Now().Add(-t.IdleConnTimeout)
}
if list, ok := t.idleConn[w.key]; ok {
for len(list) > 0 && !stop {
pconn := list[len(list)-1]
tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
//超时了,关闭连接
if tooOld {
go pconn.closeConnIfStillIdle()
}
//分发连接到wantConn
delivered = w.tryDeliver(pconn, nil)
}
}
//排队等待空闲连接
q := t.idleConnWait[w.key]
q.pushBack(w)
t.idleConnWait[w.key] = q
}
排队等待新建连接的逻辑如下:
func (t *Transport) queueForDial(w *wantConn) {
//如果没有限制最大连接数,直接建立连接
if t.MaxConnsPerHost <= 0 {
go t.dialConnFor(w)
return
}
//如果没超过连接数限制,直接建立连接
if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {
go t.dialConnFor(w)
return
}
//排队等待连接建立
q := t.connsPerHostWait[w.key]
q.pushBack(w)
t.connsPerHostWait[w.key] = q
}
连接建立完成后,同样会调用tryDeliver分发连接到wantConn,同时关闭通道w.ready,这样主协程就接触阻塞了。
func (w *wantConn) tryDeliver(pc *persistConn, err error) bool {
w.pc = pc
close(w.ready)
}
请求处理完成后,通过tryPutIdleConn将连接放回连接池;这时候如果存在等待空闲连接的协程,则需要分发复用该连接。另外,在回收连接时,还需要校验空闲连接数目是否超过限制:
func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
//禁用长连接;或者最大空闲连接数不合法
if t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 {
return errKeepAlivesDisabled...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!