基于Go的net包,对TCP的半关闭状态以及Keepalive的进一步透析
1. 调试背景
- 基于Go的net包进行编写C/S模式网络服务模拟,代码:https://github.com/tkstorm/go-example/tree/master/net/dial
- Server端中断退出,没有Close客户端的网络连接,只做了Server->Clent的TCP半关闭
- Client不知情,一直挂着等待,直至Keepalive超时
- 基于
netstat
查看到主动断开一方一直处于FIN_WAIT_2
,被动断开一方一直处于CLOSE_WAIT
- 直至最终超时,或者客户端重新发送包检测到连接已断开(不做操作,则4中状态一直持续)
2. 模拟复现
2.1. C/S程序运行
2.1.1. listener.go - 服务端
|
|
2.1.2. dial.go - 客户端
|
|
2.2. 抓包分析
2.2.1. 客户端创建连接
|
|
3次握手:[S]、[S.]、[.],点.
代表ACK标志
|
|
2.2.2. Keepalive分析-1
客服端和服务端在未发送数据时候,Keepalive针对TCP连接进行检测,发现客户端(127.0.0.1.57569)会每间隔15s
发送一个ACK空包给到服务端(127.0.0.1.2000),该时间涉及到net.Dialer
包的源码:
|
|
2.2.3. Keepalive分析-2: net.Dialer的Keepalive设置
这块可以发现,默认情况,Dialer.KeepAlive=0
,因此被设置成15s了!
|
|
2.2.4. 客户端发送消息,发送PSH标志包
继续分析,客户端发送hey,man!
字符串到服务端,以及1
、2
两个字符串,实际传输中还会附带一个\n
回车字符!
可以看到ack包号:934698622
、934698624
、934698626
|
|
2.2.5. 服务端异常中断
在服务端基于Ctrl+C
发送中断信号给客户端进程后,服务端发送FIN
标志包给到客户端,进程(signal: interrupt)中断退出,至此,服务端引发了一个TCP Conn连接的半关闭(仅服务端=>客户端这半边关闭了):
|
|
2.2.6. 客户端对已关闭的连接发送信息
若挂起的客户端再发送任意一个字节,则会触发服务端的RST标志 - RESET重置包(这个包是由OS处理的,网卡接到该数据包,触发网络IO,引发OS的IO中断处理例程,交给网络层处理,网络层继续给到传输层,才知道对应TCP报文中的端口没有打开,则自动生成RST重置包给到请求主机)
|
|
2.3. TCP连接状态
2.3.1. 客户端-服务端连接确定
连接建立后,Client和Server都处于ESTABLISHED
状态,另外Server端还有2000端口处理监听状态LISTEN
|
|
2.3.2. 服务端异常退出
查看状态,会发现:
- 客户端(127.0.0.1.57054=>127.0.0.1.2000)一直处于
CLOSE_WAIT
状态(被动断开一方) - 服务端(127.0.0.1.2000=>127.0.0.1.57054)一直处理
FIN_WAIT_2
状态(主动断开一方)
|
|
2.3.3. 若TCP半关闭状态一致持续,将持续多久?
|
|
- net.inet.tcp.keepidle : 发送keepalive探测(如果已启用)之前(TCP)连接必须处于空闲状态的时间(以毫秒为单位)。
- net.inet.tcp.keepintvl : 发送到远程计算机的keepalive探测之间的间隔(以毫秒为单位),发送TCPTV_KEEPCNT(默认为8)探测器后,如果没有响应,则删除(TCP)连接。(在GO中被设置成了15s)
- net.inet.tcp.always_keepalive: 假设在所有TCP连接上设置了SO_KEEPALIVE ,内核将定期向远程主机发送数据包以验证连接是否仍在运行。(GO的
net.Dialer
已开启)
这些参数都可以被
keepalive参数参考:1
2.4. TCP的状态机图示
更多TCP连接可以参见 2
相关说明:
- 粗的实线箭头表示正常的客户端状态变迁,用粗的虚线箭头表示正常的服务器状态变迁。
- 两个导致进入ESTABLISHED状态的变迁对应打开一个连接,而两个导致从ESTABLISHED状态离开的变迁主动打开对应关闭一个连接,ESTABLISHED状态是连接双方能够进行双向数据传递的状态。
- 左下角4个状态放在一个虚线框内,并标为“主动关闭”
- 右下两个状态(CLOSE_WAIT和LAST_ACK)也用虚线框住,并标为“被动关闭”
- CLOSED状态不是一个真正的状态,而是这个状态图的假想起点和终点,11个状态的名称(CLOSED,LISTEN,SYN_SENT等)是有意与netstat命令显示的状态名称一致。
- 从LISTEN到SYN_SENT的变迁是正确的,但伯克利版的TCP软件并不支持它。
- 当SYN_RCVD状态是从LISTEN状态(正常情况),而不是从SYN_SENT状态(同时打开)进入时,从SYN_RCVD回到LISTEN的状态变迁才是有效的。这意味着如果我们执行被动关闭(进入LISTEN),收到一个SYN,发送一个带ACK的SYN(进入SYN_RCVD),然后收到一个RST,而不是一个ACK,便又回到LISTEN状态并等待另一个连接请求的到来。
2.5. 结论
- 到此应该知道为何会通过netstat查看到那两个状态一直持续了(服务端主动断开后,只关闭了一半连接,另一半连接没有关闭);
- 网络服务进程异常关闭引起的TCP连接断开(
conn.Close()
),需要在异常情况下,也需要完整地完成TCP连接的断开,否则容易导致TCP连接资源异常,不会被释放; - 主Groutine在处理过程中,中断的消息没有广播给到当前还在处理的子Goroutine,仅仅做了主Goroutine的退出,没有考虑到子Goroutine的连接情况,可以通过将消息广播给连接池中所有的子Goroutine将其Accept接收到的连接关闭(主Goroutine退出之前,必须先做好善后工作)
进一步阅读学习
- TCP连接状态RFC793([Page 22]): https://www.ietf.org/rfc/rfc793.txt