TCP/IP连接知识

三次握手

img

解释:

客户端、服务端:CLOSED

初始(无连接)状态。

服务端:LISTEN

侦听状态,等待客户端的连接请求。

客户端:SYN_SEND

​ 在TCP三次握手期间,客户端发送了SYN包后,进入SYN_SEND状态,等待服务端的ACK包。

服务端:SYN_RECV

​ 在TCP三次握手期间,服务端收到SYN包后,进入SYN_RECV状态。

客户端、服务端: ESTABLISHED

​ 完成TCP三次握手后,服务端进入ESTABLISHED状态。此时,TCP连接已经建立,可以进行通信。

​ TCP/IP 协议是传输层的一个面向连接的安全可靠的一个传输协议,三次握手的机制是为了保证能建立一个安全可靠的连接,那么第一次握手是由客户端发起,客户端会向服务端发送一个报文,在报文里面:SYN标志位置为1,表示发起新的连接。当服务端收到这个报文之后就知道客户端要和我建立一个新的连接,于是服务端就向客户端发送一个确认消息包,在这个消息包里面:ack标志位置为1,表示确认客户端发起的第一次连接请求。以上两次握手之后,对于客户端而言:已经明确了我既能给服务端成功发消息,也能成功收到服务端的响应。但是对于服务端而言:两次握手是不够的,因为到目前为止,服务端只知道一件事,客户端发给我的消息我能收到,但是我响应给客户端的消息,客户端能不能收到我是不知道的。所以,还需要进行第三次握手,第三次握手就是当客户端收到服务端发送的确认响应报文之后,还要继续去给服务端进行回应,也是一个ack标志位置1的确认消息。通过以上三次连接,不管是客户端还是服务端,都知道我既能给对方发送消息,也能收到对方的响应。那么,这个连接就被安全的建了。

总结:

两次握手不行的原因:第三次握手保证了客户端和服务端既能给对方发送消息,又能响应对方

通俗易懂:

客户端:你在线吗? 第一次
服务器:我在,你在线吗? 第二次

为什么不是两次握手?
因为在这两次握手之后,对于客户端来说,发送和接收都没有问题,但是对于服务器,只知道接受没有问题,发送的消息客户端有没有收到,服务器是不知道的,因此需要第三次握手,也就是

客户端:我也在 第三次

此时三次握手之后,服务器知道了自己发送是没问题,因此连接是可靠的。

最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。

TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了
SYN-RCVD(同步收到)
状态。这个报文也不能携带数据,但是同样要消耗一个序号。
TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
三次握手主要目的是:信息对等和防止超时。防止超时导致脏连接。如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

四次挥手

img

解释:

​ 客户端: FIN_WAIT_1

​ 在TCP四次挥手时,客户端发送FIN包后,进入FIN_WAIT_1状态。

​ 客户端: FIN_WAIT_2

​ 在TCP四次挥手时,客户端收到ACK包后,进入FIN_WAIT_2状态。

 服务端:CLOSE_WAIT 

​ 在TCP四次挥手期间,服务端收到FIN包后,进入CLOSE_WAIT状态。

​ 客户端:TIME_WAIT

​ 在TCP四次挥手时,客户端发送了ACK包之后,进入TIME_WAIT状态,等待2个MSL时间,关闭该连接。

​ 服务端:LAST_ACK

​ 在TCP四次挥手时,服务端发送FIN包后,进入LAST_ACK状态,等待对方的ACK包。

总结:

​ 要等待客户端及服务端数据都要传输完成 ,由双方发起确认请求 ,对方回应为关闭

通俗解释:

1
2
3
4
5
6
7
8
客户端:我数据传完了,我要下线了
服务器:知道了,我还有点数据要传给你,你等下
一段时间后
服务器:我数据发完了,你可以下线了
客户端:好的

为啥不是三次挥手?
因为客户端要关闭连接的时候,无法保证服务器已经将数据发送完成,所以此时服务器只能告诉客户端我收到你的关闭请求,知道你的数据发送完成。在服务器把数据发送完成之后,告知客户端,数据发送完了,可以关闭连接。此时,客户端也要应答,同意关闭连接

TCP协议中的四次挥手

之前自己学习的网络都是浅尝辄止,最近被人反复问起 TCP 相关的挥手问题的相关问题,有必要整理下自身所学,以提供自己和别人查阅。

下图是 TCP 挥手的一个完整流程,这里引用了 tcpipguide 的流程图,更加直观的了解下挥手过程。

img

首先不要被这里的图给迷惑了,因为连接的主动断开是可以发生在客户端,也同样可以发生在服务端。

FIN_WAIT1

由图可知,当一方接受到来自应用断开连接的信号时候,就发送 FIN 数据报来进行主动断开,并且该连接进入 FIN_WAIT1 状态,连接处于半段开状态(可以接受、应答数据,当不能发送数据),并将连接的控制权托管给 Kernel,程序就不再进行处理。一般情况下,连接处理 FIN_WAIT1 的状态只是持续很短的一段时间。

我这里通过对数据包的拦截(不对 FIN 请求进行应答)来实现 FIN_WAIT1 状态,下图是主动断开一遍的 FIN 数据发送抓包记录。

undefined

在 18:12.43 的时间点,这台机器主动断开连接,并发送 FIN 请求,并且达到 RTO 后未收到响应后,一共重试了9次,每次重试时间是上一次的2倍,这条连接额外占用了 54 秒的时间。如果在服务中,这类连接数据一多就会消耗大量的服务器资源,我这里简单的提供 2 个参数来处理这个问题。

tcp_orphan_retries :Integer,这里系统参数默认为 9(文档里面默认值为7,和系统配置有关),就是近端丢弃 TCP 连接的时候,重试次数,在我的系统中。在刚刚那种情况,如果将该参数调整为 3 次,这类连接在系统中存活的时间就会大大减少,从而缓解这个问题。如果你的系统负载很大,有发现是因为 FIN_WAIT1 引起的,也可以适当的调整这个参数。

tcp_max_orphans:Integer,默认值 8096。系统所能处理不属于任何进程的 TCP sockets 最大数量。当超过这个值所有不属于任何进程的 TCP 连接(孤儿连接)都会被重置。这个参数仅仅是为了防御简单的 Dos ,不能依赖这个参数。

FIN_WAIT2

当主动断开一端的 FIN 请求发送出去后,并且成功够接受到相应的 ACK 请求后,就进入了 FIN_WAIT2 状态。其实 FIN_WAIT1 和 FIN_WAIT2 状态都是在等待对方的 FIN 数据报。当 TCP 一直保持这个状态的时候,对方就有可能永远都不断开连接,导致该连接一直保持着。

tcp_fin_timeout :Integer,默认 60,单位秒,不属于任何应用的孤儿连接保持 FIN_WAIT2 状态的最长时间,一当超过这个时间,就会被本地直接关闭,不会进入 TIME_WAIT 状态。
但是总体上来将处于 FIN_WAIT2 状态的 TCP 连接,威胁要比 FIN_WAIT1 的小,占用的资源也很小,通常不会有什么问题。

TIME_WAIT

当前面的步骤都顺利完成了,并且接受到了 被动关闭端 发送过来的 FIN 数据报后,系统做出 ACK 应答后,该连接就进入了尾声,也就是 TIME_WAIT 状态。内核会设定一个时间长度为 2MSL 的定时器,当定时器在到时间点后,内核就会将该连接关闭。反之,当连接尚未关闭的时候,又收到了对方发送过来的 FIN 请求(可能是我们发送出去的请求对方并未收到),或者收到 ICMP 请求(比如 ACK 数据报,在网络传输中出现了错误),该连接就会重新发送 ACK 请求,并重置定时器。

为什么要设置时间为 2MSL?

MSL 是Maximum Segment Lifetime,译为“报文最大生存时间”,任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

等待 2MSL 时间主要目的是怕最后一个 ACK 对方没收到,那么对方在超时后将重发第三次握手的 FIN ,主动关闭端接到重发的 FIN 包后,系统收到该分组后,可以再发一个 ACK 应答包。还有就是等来该连接在网络上的所有报文都传输完毕,所以处于 TIME_WAIT 状态时候,两端的端口都是不可用的,迟到的报文都会被废弃。

如果我们设置的时间少于 2MSL ,旧的连接刚刚关闭,这个时候有同样五元组的新连接进来了,而之前的连接还有残留报文在网络上,就会干扰新的连接的使用。

反之,如果连接处于 TIME_WAIT 过长,造成新 socket 无法复用这个端口,即使这个连接完全废弃(通常来说一个端口释放后会等待两分钟之后才能再被使用)。就像拉完屎还占着茅坑,可以尝试使用下SO_REUSEADDR(socket 参数),比如在服务停止后立即重启,这个时候可能会遇到原先的连接还处于 TIME_WAIT 状态,导致无法绑定原先的端口,就可以使用 SO_REUSEADDR。

tcp_timestamps: Boolean,默认1,表示tcp通讯的时候是否是否使用时间戳。如下图,在 TCP 头部信息的扩展头部字段中就附带了时间戳,数据长度为两个4字节。TSval是该数据报发送出来的时间,TSecr是回显时间戳(即该ack对应的data或者该data对应的上次 ack 中的 TSval 值)

undefined

tcp_tw_reuse:Boolean,默认0,只在客户端有效,就是 TCP TIME_WAIT 链路复用。比如,当客户端不断向服务端建立连接获取数据,当每次都是客户端自己关闭连接,导致服务端进入 TIME_WAIT,之后客户端又要不断重连对方继续拉取数据,这个时候就可以复用 TIME_WAIT 的连接。当连接复用后势必会有旧连接残留在网络上的数据报,那么这些数据报要怎么处理,才能不影响新的连接的使用呢。可以使用上面的参数,时间戳来判断,建立建立后将缓存的时间戳更新到现在,当早于这个时间戳的数据报进来就表明是老连接的数据,内核会直接废弃掉。

tcp_tw_recycle:Boolean,默认0,启动后能够更快地回收 TIME_WAIT 套接字。不再是2MSL,而是几个 RTO 内进行回收。所以在网络上同样会残存旧连接的数据报,内核同样可以通过时间戳的方式来判断、丢弃过时数据报。

在早期的网络通信中,开启这个参数会导致一个问题。当多个客户端通过NAT方式联网同时与服务端通信,对于服务端只收到一个IP就好像是一台客户端进行与其进行通讯,但是客户端之间会有时间戳差异,就会导致服务端会将认为过期的数据报丢弃。导致只允许一个客户端与其进行通讯。现在的 NAT 服务器已经将协议升级成了NAPT,可以采用多端口与服务端通讯就可以避免这件事情。

CLOSE_WAIT

当被动关闭端,也就是图中的服务端,接受到了对方发送过来的 FIN 请求,并且对请求做出应答后,该连接就进入了 CLOSE_WAIT ,当连接处于这个状态的时候,该连接可能有数据需要发送,或者一些其他事情要做,当这类连接过多的时候,就会导致网络性能下降,耗尽连接数,无法建立新的连接。

比如连接一直没得到释放,相应的资源一直被占用,一但达到句柄数的上限( linux 可以通过 ulimit -a 查看 open files 数值,默认1024 )后,新的请求就无法继续处理,就会返回大量的 Too Many Open Files 错误。

常见错误原因

1.代码层面上未对连接进行关闭,比如关闭代码未写在 finally 块关闭,如果程序中发生异常就会跳过关闭代码,自然未发出指令关闭,连接一直由程序托管,内核也无权处理,自然不会发出 FIN 请求,导致连接一直在 CLOSE_WAIT 。

2.程序响应过慢,比如双方进行通讯,当客户端请求服务端迟迟得不到响应,就断开连接,重新发起请求,导致服务端一直忙于业务处理,没空去关闭连接。这种情况也会导致这个问题。

缓解方案

1.修改 /etc/security/limits.conf 配置文件中参数,提高句柄数上限
2.修改 tcp 参数

参数名 默认值 优化值 说明
net.ipv4.tcp_keepalive_time 7200 1800 单位秒,默认为7200s,就是说一个异常的CLOSE_WAIT连接至少会维持2个小时
net.ipv4.tcp_keepalive_probes 9 3 在认定TCP连接失效之前,最多发送多少个keepalive探测消息。
tcp_keepalive_intvl 75 15 探测消息未获得响应时,重发该消息的间隔时间(秒)。

3.检查自己的代码,修改连接不规范的地方。

LAST_ACK

当被动关闭一段,发送出去了 FIN 数据报后,套接字就进入了 LAST_ACK 状态,并且等待对方进行发送 ACK 数据报。

1.收到了响应的ACK数据报后,连接进入CLOSED 状态,并释放相关资源
2.如果超时未收到响应,就触发了TCP的重传机制。

到此整个挥手流程就结束了。

相关文档

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

浅谈 TCP 四次挥手_yeweilei的博客-CSDN博客

TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)_青萍之末的博客-CSDN博客

TCP协议中的三次握手和四次挥手 图解、原因、状态码总结_zengrenyuan的专栏-CSDN博客 https://blog.csdn.net/zengrenyuan/article/details/80313449

TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)_青萍之末的博客-CSDN博客 https://blog.csdn.net/daaikuaichuan/article/details/83475809

青萍之末的博客_lx青萍之末_CSDN博客-剑指offer,C/C++基础知识,项目问题记录领域博主 https://blog.csdn.net/daaikuaichuan

TCP连接与释放——三次握手和四次挥手(详解+动图)_buknow的博客-CSDN博客 https://blog.csdn.net/buknow/article/details/81157002

TCP连接与释放——三次握手和四次挥手(详解+动图)_buknow的博客-CSDN博客

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

    TIME_WAIT:主动要求关闭的机器表示收到了对方的FIN报文,并发送出了ACK报文,进入TIME_WAIT状态,等2MSL后即可进入到CLOSED状态。如果FIN_WAIT_1状态下,同时收到待FIN标识和ACK标识的报文时,可以直接进入TIME_WAIT状态,而无需经过FIN_WAIT_2状态。

    CLOSE_WAIT:被动关闭的机器收到对方请求关闭连接的FIN报文,在第一次ACK应答后,马上进入CLOSE_WAIT状态。这种状态其实标识在等待关闭,并且通知应用发送剩余数据,处理现场信息,关闭相关资源。

为什么客户端最后还要等待2MSL?

MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失。站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。如果客户端收到服务端的FIN+ACK报文后,发送一个ACK给服务端之后就“自私”地立马进入CLOSED状态,可能会导致服务端无法确认收到最后的ACK指令,也就无法进入CLOSED状态,这是客户端不负责任的表现。第二,防止失效请求。防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

​ 在TIME_WAIT状态无法真正释放句柄资源,在此期间,Socket中使用的本地端口在默认情况下不能再被使用。该限制对于客户端机器来说是无所谓的,但对于高并发服务器来说,会极大地限制有效连接的创建数量,称为性能瓶颈。所以建议将高并发服务器TIME_WAIT超时时间调小。RFC793中规定MSL为2分钟。但是在当前的高速网络中,2分钟的等待时间会造成资源的极大浪费,在高并发服务器上通常会使用更小的值。
在服务器上通过变更/etc/sysctl.conf文件来修改该默认值net.ipv4.tcp_fin_timout=30(建议小30s)。修改完之后执行 /sbin/sysctl -p 让参数生效。
通过如下命令查看各连接状态的技术情况:

1
2
3
4
5
[root@node1 ~]# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

TIME_WAIT 63

ESTABLISHED 13

为什么建立连接是三次握手,关闭连接确是四次挥手呢?

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。 而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

netstat中的各种状态:

CLOSED

初始(无连接)状态。

​ LISTEN

侦听状态,等待远程机器的连接请求。

​ SYN_SEND

​ 在TCP三次握手期间,主动连接端发送了SYN包后,进入SYN_SEND状态,等待对方的ACK包。

​ SYN_RECV

​ 在TCP三次握手期间,主动连接端收到SYN包后,进入SYN_RECV状态。

​ ESTABLISHED

​ 完成TCP三次握手后,主动连接端进入ESTABLISHED状态。此时,TCP连接已经建立,可以进行通信。

​ FIN_WAIT_1

​ 在TCP四次挥手时,主动关闭端发送FIN包后,进入FIN_WAIT_1状态。

​ FIN_WAIT_2

​ 在TCP四次挥手时,主动关闭端收到ACK包后,进入FIN_WAIT_2状态。

​ TIME_WAIT

​ 在TCP四次挥手时,主动关闭端发送了ACK包之后,进入TIME_WAIT状态,等待最多MSL时间,让被动关闭端收到ACK包。

​ CLOSING

​ 在TCP四次挥手期间,主动关闭端发送了FIN包后,没有收到对应的ACK包,却收到对方的FIN包,此时,进入CLOSING状态。

​ CLOSE_WAIT

​ 在TCP四次挥手期间,被动关闭端收到FIN包后,进入CLOSE_WAIT状态。

​ LAST_ACK

​ 在TCP四次挥手时,被动关闭端发送FIN包后,进入LAST_ACK状态,等待对方的ACK包。

主动连接端可能的状态有:

​ CLOSED SYN_SEND ESTABLISHED。

主动关闭端可能的状态有:

​ FIN_WAIT_1 FIN_WAIT_2 TIME_WAIT。

被动连接端可能的状态有:

​ LISTEN SYN_RECV ESTABLISHED。

被动关闭端可能的状态有:

​ CLOSE_WAIT LAST_ACK CLOSED。

在Linux下,如果连接数比较大,可以使用效率更高的ss来替代netstat。

1
2
3
ss -s   统计连接数量
ss -t -a 查看tcp所有连接
ss -u -a 查看udp连接

查看netstat连接状态统计

1
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' 

存在close_wait的原因和解决办法

close_wait这个状态存在于服务端,当服务端发送FIN(之前客户端已经发送过fin),请求关闭连接之后进入close_wait,然而没有收到客户端的响应,可能由于客户端掉线了(如网络故障或者掉电),没有及时给予客户端回复造成问题。
或者由于客户端已经调用close(socket)退出,而服务端对其监测并断开连接,这种是服务端问题。

解决方法:一般是编程问题,可用keep_alive机制加以解决

存在FIN_WAIT2的原因和解决办法

这个状态存在于主动发起断开请求的一端,如果服务器存在大量的这个状态,那么这个服务器就充当客户端的角色,如网络爬虫,出现的原因是由于客户端发起FIN请求结束连接之后,收到了服务端的应答之后进入FIN_WAIT2,之后就没收到服务端发送的FIN信号导致。
解决方法:可以配置FIN_WAIT2的时长,当超过时长后自动断开加以解决(/proc/sys/net/ipv4/tcp_fin_timeout参数)

存在TIME_WAIT的原因和解决办法

为什么要经过TIME_WAIT状态后才真正关闭连接?
这个有两个原因:其一是响应服务端发送的FIN报文,保证服务端断开连接;其二是保证之前请求断开连接的请求,由于网络原因滞留在网络中,后续又到达了,导致后面重新建立的连接断开。

time_wait大量存在问题的原因:time_wait状态存在于主动关闭连接的一端(即客户端),如果是time_wait状态太多了,那肯定是客户端程序有问题,一般属于客户端频繁的往服务端发请求,如运行网络爬虫的机器上处于time_wait状态的socket会比较多
解决方法:可以通过修改系统配置和客户端程序解决

1 起因

线上服务器nginx日志运行一段时间后,会报如下错误:

1024 worker_connections are not enough

一般做法是修改worker_connections。
但实际上:该服务是用于时间比较短的连接里,并且一天最多才4000个请求。不可能会耗尽worker_connections。
除非每次连接都没有释放对应的连接。

shell>netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’
CLOSE_WAIT 802
ESTABLISHED 106
shell>lsof -n | grep “nginx对应的一个进程id”
MvLogServ 31125 mv 111u IPv4 76653578 0t0 TCP 10.1.138.60:8996->10.1.138.60:51977 (CLOSE_WAIT)
MvLogServ 31125 mv 112u IPv4 76659698 0t0 TCP 10.1.138.60:8996->10.1.138.60:52015 (CLOSE_WAIT)
MvLogServ 31125 mv 113u IPv4 76662836 0t0 TCP 10.1.138.60:8996->10.1.138.60:52042 (CLOSE_WAIT)
MvLogServ 31125 mv 114u IPv4 76663435 0t0 TCP 10.1.138.60:8996->10.1.138.60:52051 (CLOSE_WAIT)
MvLogServ 31125 mv 115u IPv4 76682134 0t0 TCP 10.1.138.60:8996->10.1.138.60:52136 (CLOSE_WAIT)
MvLogServ 31125 mv 116u IPv4 76685095 0t0 TCP 10.1.138.60:8996->10.1.138.60:52159 (CLOSE_WAIT)
……………….

2 解决

2.1 TIME_WAIT 通过优化系统内核参数可容易解决

TIME_WAIT大量产生很多通常都发生在实际应用环境中。
TIME_WAIT产生的原因:在通讯过程中A主动关闭造成的,
在A发送了最后一个FIN包后,系统会等待 Double时间
的MSL(Max Segment Lifetime)【注:按不同的操作系统有不同时间】用于等待接受B发送过来的FIN_ACK和FIN,
这段时间A的对应的socket的fd是不能够重新利用的,
这样在大量的短连接服务中,会出现TIME_WAIT过多的现象。

解决方案:
调整TIME_WAIT超时时间
vi /etc/sysctl.conf
#表示开启SYN Cookies。
#当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。
#允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
#表示如果套接字由本端要求关闭。
#这个参数决定了它保持在FIN-WAIT-2状态的时间
#生效,如下命令
/sbin/sysctl -p

-

  • 注:
    已经主动关闭连接了为啥还要保持资源一段时间呢?
    这个是TCP/IP的设计者规定的,主要出于以下两个方面的考虑:
  1. 防止上一次连接中的包,迷路后重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失)
    即:允许老的重复分节在网络中消逝。
  2. 可靠的关闭TCP连接。在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。
    另外这么设计TIME_WAIT 会定时的回收资源,并不会占用很大资源的,除非短时间内接受大量请求或者受到攻击。
    即:可靠地实现TCP全双工连接的终止。(确保最后的ACK能让被关闭方接收)

2.2 CLOSE_WAIT 需要从程序本身出发

LOSE_WAIT产生的原因是客户端B主动关闭,
服务器A收到FIN包,应用层却没有做出关闭操作引起的。
CLOSE_WAIT在Nginx上面的产生原因还是因为Nagle’s算法加Nginx本身EPOLL的ET触发模式导致。

ET出发模式在数据就绪的时候会触发一次回调操作,Nagle’s算法会累积TCP包,如果最后的数据包和

FIN包被Nagle’s算法合并,会导致EPOLL的ET模式只触发一次。
然而在应用层的SOCKET是读取返回0才代表链接关闭,
而读取这次合并的数据包时是不返回0的,
然后SOCKET以后都不会触发事件,
所以导致应用层没有关闭SOCKET,
从而产生大量的CLOSE_WAIT状态链接。
关闭TCP_NODELAY,在Nginx配置中加上

tcp_nodelay on;

3 总结

  • TIME_WAIT状态可以通过优化服务器参数得到解决。
    因为发生TIME_WAIT的情况是服务器自身可控的,
    要么就是对方连接的异常,要么就是自己没有迅速回收资源,
    总之不是由于自己程序错误导致的。
  • CLOSE_WAIT需要通过程序本身。
    如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程序自己没有进一步发出ack信号。
    即在对方连接关闭之后,程序里没有检测到,或者程序没有关闭连接,于是这个资源就一直被程序占着。
    服务器对于程序抢占的资源没有主动回收的功能。只能修改程序本身。
    代码需要判断socket,一旦读到0,断开连接,read返回负,
    检查一下errno,如果不是AGAIN,就断开连接。

参考来源:
【1】http://www.cnblogs.com/Bozh/p/3752476.html
作者联系方式:Email:zhangbolinux@sina.com QQ:513364476
【2】http://itindex.net/detail/50213-%E6%9C%8D%E5%8A%A1%E5%99%A8-time_wait-close_wait

知识:TCP连接状态

image-20210113114611544

TCP状态转移要点
TCP协议规定,对于已经建立的连接,网络双方要进行四次握手才能成功断开连接,如果缺少了其中某个步骤,将会使连接处于假死状态,连接本身占用的资源不会被释放。网络服务器程序要同时管理大量连接,所以很有必要保证无用连接完全断开,否则大量僵死的连接会浪费许多服务器资源。在众多TCP状态中,最值得注意的状态有两个:CLOSE_WAIT和TIME_WAIT。

1、LISTENING状态
  FTP服务启动后首先处于侦听(LISTENING)状态。

2、ESTABLISHED状态
  ESTABLISHED的意思是建立连接。表示两台机器正在通信。

*3、CLOSE_WAIT*

对方主动关闭连接或者网络异常导致连接中断,这时我方的状态会变成CLOSE_WAIT 此时我方要调用close()来使得连接正确关闭

*4、TIME_WAIT*

**我方主动调用close()断开连接,收到对方确认后状态变为TIME_WAIT**。**TCP协议规定TIME_WAIT状态会一直持续2MSL(即两倍的分段最大生存期),以此来确保旧的连接状态不会对新连接产生影响。处于TIME_WAIT状态的连接占用的资源不会被内核释放**,所以作为服务器,在可能的情况下,尽量不要主动断开连接,以减少TIME_WAIT状态造成的资源浪费。

目前有一种避免TIME_WAIT资源浪费的方法,就是关闭socket的LINGER选项。但这种做法是TCP协议不推荐使用的,在某些情况下这个操作可能会带来错误。

1. socket的状态

1.1 状态说明

CLOSED 没有使用这个套接字[netstat 无法显示closed状态]
LISTEN 套接字正在监听连接[调用listen后]
SYN_SENT 套接字正在试图主动建立连接[发送SYN后还没有收到ACK]
SYN_RECEIVED 正在处于连接的初始同步状态[收到对方的SYN,但还没收到自己发过去的SYN的ACK]
ESTABLISHED 连接已建立
CLOSE_WAIT 远程套接字已经关闭:正在等待关闭这个套接字[被动关闭的一方收到FIN]
FIN_WAIT_1 套接字已关闭,正在关闭连接[发送FIN,没有收到ACK也没有收到FIN]
CLOSING 套接字已关闭,远程套接字正在关闭,暂时挂起关闭确认[在FIN_WAIT_1状态下收到被动方的FIN]
LAST_ACK 远程套接字已关闭,正在等待本地套接字的关闭确认[被动方在CLOSE_WAIT状态下发送FIN]
FIN_WAIT_2 套接字已关闭,正在等待远程套接字关闭[在FIN_WAIT_1状态下收到发过去FIN对应的ACK]
TIME_WAIT 这个套接字已经关闭,正在等待远程套接字的关闭传送[FIN、ACK、FIN、ACK都完毕,这是主动方的最后一个状态,在过了2MSL时间后变为CLOSED状态]

1.2 状态变迁图

img

2.2 说明

2.2.1 connect返回-1

​ errno=110(ETIMEDOUT),当服务器端网线拔了的时候,客户端发送SYN过去就会收不到ACK,因此就会出现这个错误,1分钟内就会返 回这个错误。

​ errno=111(ECONNREFUSED),当服务器未listen时,就会报这个错

2.2.2 ESTABLISHED不一定真的establish

​ 会出现这种情况:client为ESTABLISHED状态而server为SYN_REVD状态。

​ ****这是因为LINUX不像其他操作系统在收到SYN为该连接立马分配一块内存空间用于存储相关的数据和结构,而是延迟到接收到client的ACK,即三次握手 真正完成后才分配空间,这是为了防范SYN flooding攻击****。 如果是这种情况,那么就会出现client端未ESTABLISHED状态,server为SYN_RECV状态。

​ 并且server的SYN_RECV状态在一定时间后会消失,client的established状态也会消失。这是因为server在SYN_RECV状态时,会像client发送多次的SYN+ACK(因为他以为自己的这个包对方没收到),发送的次数定义在/proc/sys/net/ipv4/tcp_synack_retries中,默认为5.在发送5次之后还没有收到ACK,就将其回收了,所以用netstat查看就看不到这个SYN_RECV状态了。并且会像client发送RST信号。这就会导致client的这种半连接最后也会消失。这个可以通过tcpdump抓包得到(最好知道src这样看到的包比较集中)。

TIME_WAIT处理方法

  实现的目标就是不要让处于TIME_WAIT的端口占满所有本地端口,导致没有新的本地端口用来创建新的客户端。
  1. 别让客户端的速率太快
  似乎上面的案例告诉我们别优化用力过猛,否则容易扯到蛋……将客户端请求的速率降下来就可以避免端时间占用大量的端口,吞吐量限制就是470tps或者235tps,具体根据系统TIME_WAIT默认时长决定,如果考虑到其他服务正常运行这个值还要保守一些才行;此外还需要注意,如果客户端和服务端增加了一层NAT或者L7负载均衡,那么这个限制可能会在负载均衡器上面;
  2. 客户端改成长连接的形式
  长连接效率高又不会产生大量TIME_WAIT端口。目前对我们来说还是不太现实的,虽然HTTP支持长连接,但是CGI调用应该是不可能的了,除非用之前的介绍的方式将CGI的请求转换成HTTP服务来实现。对于一般socket直连的程序来说,短连接改成长连接就需要额外的封装来标识完整请求在整个字节流中的起始位置,需要做一些额外的工作;
  3. SO_LINGER选项
  通常我们关闭socket的时候,即使该连接的缓冲区有数据要发送,close调用也会立即返回,TCP本身会尝试发送这些未发送出去的数据,只不过应用程序不知道也无法知道是否发送成功过了。如果我们将套接字设置SO_LINGER这个选项,并填写linger结构设置参数,就可以控制这种行为:
  如果linger结构的l_onoff==0,则linger选项就被关闭,其行为就和默认的close相同;如果打开,那么具体行为依据另外一个成员l_linger的值来确定:如果l_linger!=0,则内核会将当前close调用挂起,直到数据都发送完毕,或者设置的逗留时间超时返回,前者调用会返回0并且正常进入TIME_WAIT状态,后者调用会返回EWOULDBLOCK,所有未发送出去的数据可能会丢失(此处可能会向对端发送一个RST而快速关闭连接);如果l_linger==0,则直接将缓冲区中未发送的数据丢弃,且向对等实体发送一个RST,自己不经过TIME_WAIT状态立即关闭连接。
  我们都认为TIME_WAIT是TCP机制的正常组成部分,应用程序中不应该依赖设置l_linger=0这种机制避免TIME_WAIT。
  4. 修改系统参数
  (a). ****增加本地端口范围,修改net.ipv4.ip_local_port_range****,虽然不能解决根本问题但情况可以得到一定的缓解;
  (b). 缩短TIME_WAIT的时间。这个时长在书中描述到RFC推荐是2min,而BSD实现通常是30s,也就说明这个值是可以减小的,尤其我们用在内网通信的环境,数据包甚至都流不出路由器,所以根本不需要设置那么长的TIME_WAIT。这个很多资料说不允许修改,因为是写死在内核中的;也有说可以修改netfilter.ip_conntrack_tcp_timeout_time_wait(新版本nf_conntrack_tcp_timeout_time_wait)的,他们依赖于加载nf_conntract_ipv4模块,不过我试了一下好像不起作用。
  (c). 像之前在项目中推荐的,做出如下调整

1
2
3
4
5
net.ipv4.tcp_tw_reuse = 1

net.ipv4.tcp_timestamps=1

net.ipv4.tcp_tw_recycle=1

​ 很多文献说这种设置是不安全的,所以在测试环境以外就别尝试了,因为这些选项还涉及到timestamp特性,我还不清楚什么回事,后面有时间再看什么吧。
  我们在开发服务端的时候,通常都会设置SO_REUSEADDR这个选项。其实像上面描述到的,该选项也牵涉到侦听socket端口处于TIME_WAIT的情况,设置这个选项将允许处于TIME_WAIT的端口进行绑定

另外一文:

​ 记得以前面试的时候被面试官问起TIME_WAIT有什么痛点,当时只记得TCP三次握手、四次挥手之类的,至于其中的某个状态还真是记不起来,之前也没有过多关注过,还有对于拥塞控制的概念也比较模糊。

TCP报文格式

TCP大家都知道是什么东西,这个协议的具体报文格式如下:

img

标志位

  1. URG:指示报文中有紧急数据,应尽快传送(相当于高优先级的数据)。
  2. PSH:为1表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。
  3. RST:TCP连接中出现严重差错(如主机崩溃),必须释放连接,在重新建立连接。
  4. FIN:发送端已完成数据传输,请求释放连接。
  5. SYN:处于TCP连接建立过程。 (Synchronize Sequence Numbers)
  6. ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。

窗口

滑动窗口大小,这个字段是接收端用来告知发送端自己还有多少缓冲区可以接受数据。于是发送端可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。(以此控制发送端发送数据的速率,从而达到流量控制。)窗口大小时一个16bit字段,因而窗口大小最大为65535。

头部长度(首部长度)

由于TCP首部包含一个长度可变的选项和填充部分,所以需要这么一个值来指定这个TCP报文段到底有多长。或者可以这么理解:就是表示TCP报文段中数据部分在整个TCP报文段中的位置。该字段的单位是32位字,即:4个字节。TCP的滑动窗口大小实际上就是socket的接收缓冲区大小的字节数。

选项和填充部分

TCP报文的字段实现了TCP的功能,标识进程、对字节流拆分组装、差错控制、流量控制、建立和释放连接等。其最大长度可根据TCP首部长度进行推算。TCP首部长度用4位表示,那么选项部分最长为:(2^4-1)*(32/8)-20=40字节。


TCP/IP连接知识
https://www.tohmm.cn/20240525/T-TCPIP-tcpip连接知识/
作者
H.mm
发布于
2024年5月25日
许可协议