基于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 - 服务端
$ go run listener.go
network:tcp, listen on:[::]:2000...
2019/06/27 15:21:10 client 127.0.0.1:57389 coming..
2019/06/27 15:22:45 Client say:"hey,man!\n"
2019/06/27 15:22:45 server write to conn size: 23 bytes
2019/06/27 15:22:51 Client say:"1\n"
2019/06/27 15:22:51 server write to conn size: 16 bytes
2019/06/27 15:22:53 Client say:"2\n"
2019/06/27 15:22:53 server write to conn size: 16 bytes
^Csignal: interrupt
2.1.2. dial.go - 客户端
$ go run dial.go
using network tcp, local address 127.0.0.1:57389, connect to 127.0.0.1:2000...
hey,man!
2019/06/27 15:22:45 client write to conn total size: 9
1
2019/06/27 15:22:51 client write to conn total size: 2
2
2019/06/27 15:22:53 client write to conn total size: 2
2.2. 抓包分析
2.2.1. 客户端创建连接
$ sudo tcpdump -i lo0 -nn -S port 2000
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes
3次握手:[S]、[S.]、[.],点.
代表ACK标志
15:21:10.317146 IP 127.0.0.1.57389 > 127.0.0.1.2000: Flags [S], seq 3503385, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 1429491716 ecr 0,sackOK,eol], length 0
15:21:10.317204 IP 127.0.0.1.2000 > 127.0.0.1.57389: Flags [S.], seq 2869623410, ack 3503386, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 1429491716 ecr 1429491716,sackOK,eol], length 0
15:21:10.317217 IP 127.0.0.1.57389 > 127.0.0.1.2000: Flags [.], ack 2869623411, win 6379, options [nop,nop,TS val 1429491716 ecr 1429491716], length 0
15:21:10.317227 IP 127.0.0.1.2000 > 127.0.0.1.57389: Flags [.], ack 3503386, win 6379, options [nop,nop,TS val 1429491716 ecr 1429491716], length 0
2.2.2. Keepalive分析-1
客服端和服务端在未发送数据时候,Keepalive针对TCP连接进行检测,发现客户端(127.0.0.1.57569)会每间隔15s
发送一个ACK空包给到服务端(127.0.0.1.2000),该时间涉及到net.Dialer
包的源码:
15:34:00.301844 IP 127.0.0.1.57569 > 127.0.0.1.2000: Flags [.], ack 1245687546, win 6379, length 0
15:34:00.301895 IP 127.0.0.1.2000 > 127.0.0.1.57569: Flags [.], ack 2176488825, win 6379, options [nop,nop,TS val 1430259791 ecr 1430213787], length 0
15:34:15.435192 IP 127.0.0.1.57569 > 127.0.0.1.2000: Flags [.], ack 1245687546, win 6379, length 0
15:34:15.435239 IP 127.0.0.1.2000 > 127.0.0.1.57569: Flags [.], ack 2176488825, win 6379, options [nop,nop,TS val 1430274886 ecr 1430213787], length 0
15:34:30.481987 IP 127.0.0.1.57569 > 127.0.0.1.2000: Flags [.], ack 1245687546, win 6379, length 0
15:34:30.482035 IP 127.0.0.1.2000 > 127.0.0.1.57569: Flags [.], ack 2176488825, win 6379, options [nop,nop,TS val 1430289886 ecr 1430213787], length 0
15:34:45.645724 IP 127.0.0.1.57569 > 127.0.0.1.2000: Flags [.], ack 1245687546, win 6379, length 0
15:34:45.645765 IP 127.0.0.1.2000 > 127.0.0.1.57569: Flags [.], ack 2176488825, win 6379, options [nop,nop,TS val 1430305023 ecr 1430213787], length 0
15:35:00.676002 IP 127.0.0.1.57569 > 127.0.0.1.2000: Flags [.], ack 1245687546, win 6379, length 0
15:35:00.676053 IP 127.0.0.1.2000 > 127.0.0.1.57569: Flags [.], ack 2176488825, win 6379, options [nop,nop,TS val 1430320023 ecr 1430213787], length 0
15:35:15.760528 IP 127.0.0.1.57569 > 127.0.0.1.2000: Flags [.], ack 1245687546, win 6379, length 0
15:35:15.760588 IP 127.0.0.1.2000 > 127.0.0.1.57569: Flags [.], ack 2176488825, win 6379, options [nop,nop,TS val 1430335055 ecr 1430213787], length 0
2.2.3. Keepalive分析-2: net.Dialer的Keepalive设置
这块可以发现,默认情况,Dialer.KeepAlive=0
,因此被设置成15s了!
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
...
if tc, ok := c.(*TCPConn); ok && d.KeepAlive >= 0 {
setKeepAlive(tc.fd, true)
ka := d.KeepAlive
if d.KeepAlive == 0 {
ka = 15 * time.Second
}
setKeepAlivePeriod(tc.fd, ka)
testHookSetKeepAlive(ka)
}
...
}
func setKeepAlivePeriod(fd *netFD, d time.Duration) error {
// The kernel expects seconds so round to next highest second.
d += (time.Second - time.Nanosecond)
secs := int(d.Seconds())
if err := fd.pfd.SetsockoptInt(syscall.IPPROTO_TCP, sysTCP_KEEPINTVL, secs); err != nil {
return wrapSyscallError("setsockopt", err)
}
err := fd.pfd.SetsockoptInt(syscall.IPPROTO_TCP, syscall.TCP_KEEPALIVE, secs)
runtime.KeepAlive(fd)
return wrapSyscallError("setsockopt", err)
}
2.2.4. 客户端发送消息,发送PSH标志包
继续分析,客户端发送hey,man!
字符串到服务端,以及1
、2
两个字符串,实际传输中还会附带一个\n
回车字符!
可以看到ack包号:934698622
、934698624
、934698626
15:16:22.224881 IP 127.0.0.1.57312 > 127.0.0.1.2000: Flags [P.], seq 934698613:934698622, ack 713123462, win 6379, options [nop,nop,TS val 1429204284 ecr 1429204120], length 9
15:16:22.224914 IP 127.0.0.1.2000 > 127.0.0.1.57312: Flags [.], ack 934698622, win 6379, options [nop,nop,TS val 1429204284 ecr 1429204284], length 0
15:16:27.548259 IP 127.0.0.1.57312 > 127.0.0.1.2000: Flags [P.], seq 934698622:934698624, ack 713123462, win 6379, options [nop,nop,TS val 1429209599 ecr 1429204284], length 2
15:16:27.548328 IP 127.0.0.1.2000 > 127.0.0.1.57312: Flags [.], ack 934698624, win 6379, options [nop,nop,TS val 1429209599 ecr 1429209599], length 0
15:16:28.010360 IP 127.0.0.1.57312 > 127.0.0.1.2000: Flags [P.], seq 934698624:934698626, ack 713123462, win 6379, options [nop,nop,TS val 1429210060 ecr 1429209599], length 2
15:16:28.010392 IP 127.0.0.1.2000 > 127.0.0.1.57312: Flags [.], ack 934698626, win 6379, options [nop,nop,TS val 1429210060 ecr 1429210060], length 0
15:16:30.384172 IP 127.0.0.1.57312 > 127.0.0.1.2000: Flags [P.], seq 934698626:934698628, ack 713123462, win 6379, options [nop,nop,TS val 1429212430 ecr 1429210060], length 2
15:16:30.384213 IP 127.0.0.1.2000 > 127.0.0.1.57312: Flags [.], ack 934698628, win 6379, options [nop,nop,TS val 1429212430 ecr 1429212430], length 0
2.2.5. 服务端异常中断
在服务端基于Ctrl+C
发送中断信号给客户端进程后,服务端发送FIN
标志包给到客户端,进程(signal: interrupt)中断退出,至此,服务端引发了一个TCP Conn连接的半关闭(仅服务端=>客户端这半边关闭了):
15:23:00.063428 IP 127.0.0.1.2000 > 127.0.0.1.57389: Flags [F.], seq 2869623411, ack 3503399, win 6379, options [nop,nop,TS val 1429601142 ecr 1429594758], length 0
15:23:00.063479 IP 127.0.0.1.57389 > 127.0.0.1.2000: Flags [.], ack 2869623412, win 6379, options [nop,nop,TS val 1429601142 ecr 1429601142], length 0
2.2.6. 客户端对已关闭的连接发送信息
若挂起的客户端再发送任意一个字节,则会触发服务端的RST标志 - RESET重置包(这个包是由OS处理的,网卡接到该数据包,触发网络IO,引发OS的IO中断处理例程,交给网络层处理,网络层继续给到传输层,才知道对应TCP报文中的端口没有打开,则自动生成RST重置包给到请求主机)
15:23:06.283001 IP 127.0.0.1.57389 > 127.0.0.1.2000: Flags [P.], seq 3503399:3503401, ack 2869623412, win 6379, options [nop,nop,TS val 1429607350 ecr 1429601142], length 2
15:23:06.283044 IP 127.0.0.1.2000 > 127.0.0.1.57389: Flags [R], seq 2869623412, win 0, length 0
2.3. TCP连接状态
2.3.1. 客户端-服务端连接确定
连接建立后,Client和Server都处于ESTABLISHED
状态,另外Server端还有2000端口处理监听状态LISTEN
$ netstat -nap tcp|grep 2000
tcp4 0 0 127.0.0.1.2000 127.0.0.1.57054 ESTABLISHED
tcp4 0 0 127.0.0.1.57054 127.0.0.1.2000 ESTABLISHED
tcp46 0 0 *.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
状态(主动断开一方)
$ netstat -nap tcp|grep 2000
tcp4 0 0 127.0.0.1.2000 127.0.0.1.57054 FIN_WAIT_2
tcp4 0 0 127.0.0.1.57054 127.0.0.1.2000 CLOSE_WAIT
2.3.3. 若TCP半关闭状态一致持续,将持续多久?
$ sysctl net.inet.tcp | grep keep
net.inet.tcp.keepidle: 7200000 // 单位为ms,因此是2小时
net.inet.tcp.keepintvl: 75000 // 75s
net.inet.tcp.keepinit: 75000 // 75s
net.inet.tcp.keepcnt: 8
net.inet.tcp.always_keepalive: 0
- 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