TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP生命周期分为三个阶段:建立连接、传输数据、关闭连接。根据使用方法不同,可认为TCP连接分长连接和短连接。如果每次通信后就close掉连接,那TCP就相当于短连接。如果请求结束后保持TCP连接的状态不关闭,那TCP就是长连接。
一、为什么使用长连接
TCP连接的建立需要3次握手,断开需要4次挥手。使用长连接可以减少连接的建立次数,减少CPU及内存的使用
实现pipelining模式
实现服务端push数据给客户端
减少TCP请求,减少网络堵塞
减少后续请求的响应时间
减少了三次握手和四次挥手的时间
单线程1000次请求数据对比:
TCP短连接用时:4.6s
TCP长连接用时:2.5s
二、长连接会有什么问题
维护成本高,随着服务的运行,建立的连接会越来越多,最终可能导致服务器不堪重负。因此如何管理连接和清理死连接(不活跃)就显得尤为重要。
(1)心跳检测
TCP层:KeepAlive机制
应用层:心跳
TCP keepalive机制
tcpkeepalivetime:在链路上没有数据传送的情况下,tcpkeepalivetime秒后触发tcp心跳
tcpkeepaliveintvl:心跳每个tcpkeepaliveintvl秒发送一次
tcpkeepaliveprobes:发送tcpkeepaliveprobes次都无响应后将断开连接
KeepAlive本质
通过抓包数据来看,是TCP发送一个数据长度为0的空包,ack最后一个包的序号,这样对应用层无影响。
默认情况下,是关闭TCP的KeepAlive机制的。
go打开KeepAlive的方法:
conn.SetKeepAlive(true)
conn.SetKeepAlivePeriod(time.Minute)
(2)连接管理
目前主要有2种连接管理方式:
基于ip:port的连接池
ip:port为key,缓存一组连接
基于seq的连接池管理
需要应用层协议支持,客户端在请求包中加入seq,收到返回包后根据seq找到相应的回调方法。
连接池
bingo服务端使用的tcp层keepalive机制检查连接状态,同时服务端支持pipeline的方式处理请求。因此在客户端可以使用连接池来复用TCP长连接,避免客户端创建大量的连接。
常见实现方式:
map+chan
map+list
使用 map + chan的方式实现
connpool:负责管理connset
connset:一个ip:port拥有一个connset,负责申请连接、释放连接的具体实现,使用chann缓存连接
2个参数&2个方法
maxIdleSize:一个ip对应的最大空闲连接数。等于channel的大小
maxIdleTime:最大空闲时间,连接超过这个时间可认为是不活跃连接,可以直接关闭
Alloc:申请一个连接
Reclaim:把连接返回连接池,连接在使用过程中若发生error,则应该close掉连接,不应该把该连接返回连接池
申请连接时,会优先从连接池中获取链接,如果获取失败或尝试次数达到上限后才会创建新连接。
数据对比
(1)不使用连接池
单协程请求总数:100
请求间隔时间:0~50ms
协程数:500
成功:51636
失败:3364
端口占用:50000
(2)使用连接池
单协程请求总数:500
请求间隔时间:0~50ms
协程数:50
成功:50000
失败:0
端口占用:5~10
优点:
1.代码实现比较简单。
2.上层协议无关,任何协议都可以接入连接池。
缺点:
高并发下占用过多的fd。
如果请求间隔时间为0,占用端口数=协程数。
基于seq的连接池管理
这种方式通常用于异步rpc调用
客户端会缓存seq->channel(存放返回包、用于回调),
客户端发包在包头中填写seq值,服务端会将seq字段原封不动的带回来,
然后通过seq查找到对应的channel,将数据放进去。等待数据的协程就可以接着处理请求了。
这种方式管理连接,通常发送请求包和接受返回包是不同的进程(写成),通过map来共享数据,完成回调等操作。
jungle中返回包处理代码:
交互流程
如果conn本身是协程(进程)安全的,在alloc连接时,连接池可以不把此连接清除掉。这种情况下连接池的主要作用是根据策略做连接的负载均衡。
优点:
连接复用,理论上一个ip+port可以只对应一个连接。
基于seq复用,节省fd。
缺点:
实现复杂,异常状态多,超时、连接断开不可用等处理逻辑复杂。
需要应用层协议支持。