本博客主要概述了TCP的各个阶段的状态所代表的意义,以及各个异常场景的描述以及原因。
线上大量CLOSE_WAIT的原因深入分析 浅谈CLOSE_WAIT
1 工具
- 查看TCP各个状态的连接数量
netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' LISTEN 5 TIME_WAIT 3 ESTABLISHED 5
- perf火焰图 如果对业务代码不熟悉,可以直接使用perf把所有的调用关系使用火焰图给绘制出来。
2 TCP各个阶段的状态
- CLOSED 表示socket连接没被使用。
- LISTENING 表示正在监听进入的连接。
- SYN_SENT 表示客户端发送SYN包,还未收到服务端的响应。
- SYN_RECEIVED 表示服务端收到了客户端的SYN包,并发送SYN+ACK,还未收到客户端的ACK报文。
- ESTABLISHED 表示连接已被建立。
- 客户端表示已经收到了服务端的SYN+ACK报文,并发送了ACK报文。
- 服务端表示收到了客户端的ACK报文。
- CLOSE_WAIT 表示服务端收到了客户端的FIN包,并返回了ACK报文,但是自己还未发送FIN包。
- FIN_WAIT_1 表示客户端发送了FIN包,但是未收到服务端的ACK报文。
- LAST_ACK 表示服务端发送了FIN包之后,等待客户端的ACK报文。
- FIN_WAIT_2 表示客户端收到了服务端的ACK报文,等待服务端继续发送FIN包。
- TIME_WAIT 表示客户端返回ACK报文之后,还要等待2MLS时间。
3 TCP异常场景
3.1 服务端先关闭连接
正常的TCP链路,在服务端先关闭连接之后,服务端的状态变为了FIN_WAIT2,而客户端变成了CLOSE_WAIT状态。客户端主动发送FIN包之后,客户端状态从CLOSE_WAIT走出。
- 连接正常建立
- 服务端异常关闭
- 再关闭客户端
- 抓包
在服务端关闭长时间之后
在服务端关闭之后,立即关闭客户端
原因
主动关闭连接的一方在进入到FIN_WAIT_2状态之后,在n秒的时间内没有收到对端的FIN包,则主动关闭一方会强制将socket关闭,状态直接进入到CLOSED。可以看到超时时间为60s。
- tcp的close
调用close之后,并不是立即关闭这个文件描述符,而是先发送fin报文,之后完成4次挥手或者异常超时之后,才会关闭该文件描述符。close背后的连接终止过程
- tcp的close和shutdown的区别 TCP连接关闭—close和shutdown
①:close函数函数会关闭套接字,如果由其他进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写。
②:shutdown会切断进程共享的套接字的所有连接,不管引用计数是否为0,由第二个参数选择断连的方式。
如果在服务端关闭之后,客户端继续向服务端发送数据,会收到RST报文。之后再发送的话,客户端会被SIGPIPE信号终止。 如果客户端此时还要向服务端发送数据,将诱发服务端TCP向服务端发送SIGPIPE信号,因为向接收到RST的套接口写数据都会收到此信号.
3.2 服务端fd泄露,导致accept返回失败,此时客户端主动close之后
- accept失败,即accept不再调用
- 客户端主动close之后,服务端由于无法close导致出现CLOSE_WAIT状态
- 如果客户端主动close,服务端在出现CLOSE_WAIT状态之后,再accept,close的话。 服务端未accept的时候,客户端的报文正常发送到服务端,其报文存放在缓冲区中。服务端accept之后,从缓冲区中读取数据,并能够正常将其的socket close掉,CLOSE_WAIT状态消失。
3.3 listen函数的backlog的含义
结论:全连接队列中的能存放的最大连接数为backlog+1
如下图所示,LISTEN的backlog设置为5,而其全连接队列中有6个连接存在。客户端尝试建立第7个连接,服务端一直没有发会ACK报文,导致客户端一直以1,2,4,8,16,32的时间间隔重发。
3.3.1 tcp全连接队列满的情况下
- ubuntu主机下的情况,core-9.20170808ubuntu1-noarch:security-9.20170808ubuntu1-noarch
- centos7主机
Centos7主机的表现符合网上的表述,即全连接队列满了之后,3次握手的时候服务端直接丢弃ACK,因此服务端的TCP的协议栈持续重传SYN+ACK报文,重传5次。Ubuntu的表现怀疑半连接队列已经满了。
- 服务端close_wait情况下lsof的表现
- 如果由于未accpet导致的close_wait,那么lsof只能看到listen的fd的情况,而不能看到close_wait的连接,因为close_wait的连接并未被分配文件描述符,所以无法通过lsof查看。
- 如果是由于服务端获取得到了文件描述符了,而未close导致的问题,那么lsof是能看到这条错误的连接的。
3.4 send中参数MSG_NOSIGNAL
3.4.1 概述
linux下当连接断开,还发数据的时候,不仅send()的返回值会有反映,而且还会向系统发送一个异常消息,如果不作处理,系统会出BrokePipe,程序会退出,这对于服务器提供稳定的服务将造成巨大的灾难。
为此,send()函数的最后一个参数可以设MSG_NOSIGNAL,禁止send()函数向系统发送异常消息。
其实第一次send的时候应该会收到RST返回的错误码,当再次执行send的时候会发送管道破裂信号(ESPIPE)错误码,同时系统会发送信号(SIGPIPE
)给进程,然后进程收到后执行退出操作。其实MSG_NOSIGNAL的作用应该是告诉进程忽略信号(SIGPIPE
)。
3.4.2 实验
- 客户端的send(2)函数不加入MSG_NOSIGNAL
客户端send(2)持续向服务端发送信息,
ctrl+c
关闭服务端之后,客户端过一会儿也自动退出了。
- 客户端的send(2)函数加入MSG_NOSIGNAL
客户端send(2)持续向服务端发送信息,
ctrl+c
关闭服务端之后,客户端不会退出,但是send(2)接口报错。
3.4 非阻塞情况下send(2)返回值为0时
目前在网络上查找,发现有三种说法:
- send返回0,说明连接关闭
- send返回0,说明缓冲区满
- snnd返回0,等于0一般只有你len=0的时候,你要copy的缓冲区为0,才返回0 目前解释3是可信的。 对于1,send返回-1,报错Broken pipe; 对于2,send返回-1,非阻塞情况下返回EAGAIN;
3.5 Server端调用Close发送的是RST报文,而不是FIN包
代码里面写得很清楚,如果你的接收缓冲区中还有数据,协议栈就会发送RST而不是FIN。
3.5.1 实验
//TODO
3.5 断开网线,客户端调用send(2)接口
send会正常返回,但是此时tcp已经断开了, 无法发送报文到对端。因此不能够通过判断send的返回值来确认tcp是否断开。
3.6 SO_REUSEADDR的使用
- 服务器启动后,有客户端连接并已建立,如果服务器主动关闭,那么和客户端的连接会处于TIME_WAIT状态,此时再次启动服务器,就会bind不成功,报:Address already in use。
- 服务器父进程监听客户端,当和客户端建立链接后,fork一个子进程专门处理客户端的请求,如果父进程停止,因为子进程还和客户端有连接,所以再次启动父进程,也会报Address already in use。