TCP各个阶段的状态

本博客主要概述了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背后的连接终止过程

①: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的表现
    1. 如果由于未accpet导致的close_wait,那么lsof只能看到listen的fd的情况,而不能看到close_wait的连接,因为close_wait的连接并未被分配文件描述符,所以无法通过lsof查看。

  1. 如果是由于服务端获取得到了文件描述符了,而未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时

目前在网络上查找,发现有三种说法:

  1. send返回0,说明连接关闭
  2. send返回0,说明缓冲区满
  3. snnd返回0,等于0一般只有你len=0的时候,你要copy的缓冲区为0,才返回0 目前解释3是可信的。 对于1,send返回-1,报错Broken pipe; 对于2,send返回-1,非阻塞情况下返回EAGAIN;

3.5 Server端调用Close发送的是RST报文,而不是FIN包

tcp 关闭socket 不发 FIN(RST)

代码里面写得很清楚,如果你的接收缓冲区中还有数据,协议栈就会发送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。
Tags: TCP/IP协议
Share: X (Twitter) Facebook LinkedIn