tcp连接一端在进行完三次握手以后进入ESTABLISHED状态,如果连接的对端在某一时刻在网络中消失,而本端没有感知到,还是处于ESTABLISHED状态,那么本端的连接就被称为半打开连接(Half Open)。
连接的对端在网络中消失的情况有好多:
例如对端主机突然断电,tcp连接来不及发送任何信息就消失啦。
还有,连接路径上的某个nat设备aging-time过期,并且nat port被重用,虽然tcp连接的两端都还处于ESTABLISHED状态,可实际上两端的连接已经无法正常通信,此时这两端的连接都是半打开连接。(这种情况是我的猜测,还没有得到实践的检验。如果结论错误,会修改掉!)
还有,listen socket的accept调用缓慢导致积压队列满,client端连接会成为半打开连接。这种情况是本次讨论的主题。
首先说下tcp的三次握手
server端的tcp连接在三次握手阶段会经历SYN_RECV状态到ESTABLISHED状态的变迁,其中SYN_RECV状态到连接存放于listen socket积压队列的半连接队列中,当连接由SYN_RECV状态变为ESTABLISHED状态,连接会被从半连接队列中移到已连接队列中。系统调用accept的作用就是从listen socket的已连接队列中取走一个连接,然后将该连接与进程绑定。
但是,如果listen socket的积压队列(半连接队列与连接队列)全部满后,对于新来的client连接会如何处理呢。答案是,linux不同版本的实现不同。
当前的实验环境:
1 | zuchunlei@ubuntu14:~$ uname -a |
服务端代码:
1 | In [1]: from socket import * |
为了简单,我将listen的backlog设置为1,并且不调用sock.accept方法。这样所有的ESTABLISHED状态的连接都存在积压队列中,并且没有和进程绑定起来。
使用netstat查看10000端口的状态:
1 | Every 1.0s: sudo netstat -tnpoa|sed -n -e 2p -e /10000/p Sat Dec 16 20:23:03 2017 |
使用ss查看10000端口的状态:
1 | Every 1.0s: ss -tnpoa|sed -n -e 1p -e /10000/p Sat Dec 16 20:25:18 2017 |
解析一下,ss命令输出的State=Listen状态的数据时,其中Send-Q的大小表示该listen socket积压队列的长度,Recv-Q代表已完成三次握手,ESTABLISHED状态的连接个数。这样的连接存在于listen socket的已连接队列中。
用nc localhost 10000进行2次连接后,使用netstat查看10000端口的状态:
1 | Every 1.0s: sudo netstat -tnpoa|sed -n -e 2p -e /10000/p Sat Dec 16 20:32:45 2017 |
netstat显示当前客户端程序nc连接已经建立完成,服务端的2个连接也处于ESTABLISHED状态,但因为当前没有accept调用,所以服务端的两个连接的进程PID显示为-,表示当前连接没有和进程绑定起来。
使用ss查看10000端口的状态:
1 | Every 1.0s: ss -tnpoa|sed -n -e 1p -e /10000/p Sat Dec 16 20:36:10 2017 |
通过ss可以看到,当前LISTEN状态的RECV-Q值为2,表示有2个ESTABLISHED状态的连接在已连接队列中等待应用层调用accept取走。
用nc localhost 10000进行第三次连接后,netstat查看10000端口的状态:
1 | Every 1.0s: sudo netstat -tnpoa|sed -n -e 2p -e /10000/p Sat Dec 16 20:41:18 2017 |
可以看到对于第三个客户端nc,连接状态为ESTABLISHED,表示3次握手已经正确完成。而对于服务端,当前的连接状态为SYN_RECV,表示半连接状态,因为当前积压队列已经满,没有空间再存放ESTABLISHED连接,所以该连接无法从SYN_RECV状态变为ESTABLISHED状态,虽然能正确接收到nc端的第三个ACK段。
此时使用tcpdump进行抓包:
1 | zuchunlei@ubuntu14:~$ sudo tcpdump -i any tcp port 10000 -nn |
对于SYN_RECV状态的连接,linux会启动定时器进行重传三次握手的第二段[S.],在4次重传后,如果当前listen socket已连接队列中依然没有空间,则将SYN_RECV状态的连接丢弃。
等待4次重传后,使用netstat查看10000端口状态:
1 | Every 1.0s: sudo netstat -tnpoa|sed -n -e 2p -e /10000/p Sat Dec 16 20:58:20 2017 |
server端将SYN_RECV状态的连接丢弃后,此时第三个nc客户端连接就已经成为了半打开连接。
对半打开连接进行send/recv操作时的影响:
如果此时,第三个nc客户端发送数据,则因为连接对对端不存在,对端会回复RST段,本端收到RST段后也会将连接重置。
如果第三个nc客户端只接收数据的话,则这个客户端永远阻塞在recv调用中无法返回。为了有效解决这种问题,客户端可以启动tcp的keepalive,因为默认tcp发送keepalive probe的间隔时间较长,应用可以通过设置socket option(TCP_KEEPDILE/TCP_KEEPINTVL/TCP_KEEPCNT)将发送keepalive probe的时间设短些。
今早我测试了一下最新版ubuntu16.04的实现,发现如果listen socket的积压队列满后,新来客户端的连接不再成为ESTABLISHED状态,而是在SYN_SENT状态进行进行SYN段的超时重传,而服务端不返回任何tcp段。
新版的测试环境:
1 | zuchunlei@box:~$ uname -a |
与之前的测试场景一样,当前只关注第三个nc客户端连接的状态。
使用netstat查看10000端口的状态:
1 | Every 1.0s: sudo netstat -tnpoa|sed -n -e 2p -e /10000/p Sat Dec 16 21:21:57 2017 |
此时,第三个nc客户端连接状态为SYN_SENT,进行超时重传SYN段。
使用tcpdump抓去第三个nc客户端的tcp包:
1 | zuchunlei@box:~$ sudo tcpdump -i any tcp port 10000 -nn |
可以看到客户端在进行超时重传SYN段的过程中,服务端没有发送一个包。
在客户端SYN_SENT超时后,使用netstat查看10000端口状态:
1 | Every 1.0s: sudo netstat -tnpoa|sed -n -e 2p -e /10000/p Sat Dec 16 21:27:36 2017 |
客户端连接消失。
在当前新版当linux实现中,由于listen socket积压队列满时,新的客户端连接并不会成为半打开连接,而是在connect调用时进行重传SYN段,如果达到了SYN_SENT状态的阈值后,tcp连接消失,应用层connect调用返回timeout异常!