前段时间参加一个部门内部的一个关于socket通信的培训,回来之后,将学习到的只是做了一下记录,防止时间过长,淡化了记忆;
我们使用工具对网络抓包进行分析的时候,会发现有时候会出现大量的close_wait或者time_wait的抓包,如果我们是为了对应用层协议进行分析,一般都会忽略这些包,而去关注与某种特定的引用层协议包(sip、http、rtsp等);其实这些包对于我们分析当前网络状态非常有意义的。
我们都知道tcp是可靠的通信协议,在tcp协议中,从发起网络连接到连接的完全建立,是存在一个状态机的,用以标明当前连接的状态;一条连接,在每一个时期,都有一个状态,我们在抓包中看到的time_wait或者是close_wait的抓包,其实是tcp中维护的一个连接的状态,是状态机在特定时期的一种体现,接下来我们来看看网络状态在通信过程中的一个变化;
从上图可以看出来服务端在进行端口初始化后,调用listen方法,到这个时候,没有任何状态,那是由于这个时候客户端不能连接,并且状态机是针对于每一个连接来说的,所以说无连接就自然谈不上状态机;在收发双方的协议栈之中都会针对于这个连接创建一个对象,用以关联整个这个连接上产生的数据以及状态的变化。
客户端调用connect方法后,客户端所处的机器的协议栈通过五元祖检测到这是一个新的连接,会产生一个连接对象,并发送一个syn消息给服务端,客户端协议栈在发送syn消息之后,其内部标记为syn_send状态,说明我已经发送完syn消息;
当消息通过网络到达server,server检测到这个是个新的连接,也会创建一个对象,当检测该请求是合法请求之后,会立即回复ack+syn消息给客户端(这个过程是协议栈自动回复的,不需要业务层干预);然后在服务端内部维护的这条连接的状态机中,将状态机设置为syn_recv状态;
当客户端收到对端的ack+sync消息之后,将状态机设置为establish,这个时候,对于客户端来说连接已经建立完成,但是服务端还没有建立,那是由于服务端在响应ack的时候,报文中携带了自己的syn,客户端需要针对于这个syn进行响应,所以客户端在收到syn+ack消息之后,会自动响应ack消息;
服务端收到ack消息之后,也将这条连接的双方的状态设置为establish,这样的话,整个连接都已经建立完成。
好了,上面这个是tcp三次握手建立连接的过程,经过上面的流程,客户端和服务端之间可以基于已经建立的连接进行数据可靠传输。
在基于某一条已经建立的连接进行数据传输结束之后,需要断开连接,否则连接会占用资源,导致资源泄漏;
在上面的图中,可以看到client和server都调用了close方法,client先调用了close方法,其实在实际变成过程中,close方法可以由client和server的任何一方先调用,没有明确的硬性规定,在接下来的介绍中,我们称先调用的一方为“主动断开方”,后调用的一方为“被动断开方”
在通信双方进行close调用之前,双方看到这条连接的状态都为establish;主动调用方调用close方法之后,协议展会发送FIN消息,这个时候,主动调用方的协议栈会将这条连接的状态设置成fin_wait1状态;
被动调用方在收到FIN消息之后,会马上响应ack消息给主动调用方,并将自己维护的状态改成close_wait;
主动调用方在收到被动调用方响应的ack消息之后,会将连接的状态吗设置成fin_wait2状态。此时连接并没有断开,而是出于断开的过程中;
接下来被动调用方也应该调用close方法,发送fin消息给主动调用方,发送完fin消息之后,将自己维护的这条连接的状态码设置成last_ack状态;
主动调用方在收到fin消息之后,马上响应ack消息给被动断开方,并将自己维护的状态码设置成time_wait状态;这条连接在维护time_wai状态两个RTT时间之后会被系统回收掉;
被动断开方在收到主动调用方响应的ack消息后,会将这条连接的资源回收;
到此,tcp的四次挥手过程也已经完成,整个连接从建立到结束的状态改变,上面的流程中,除了connect和close需要业务成调用,其他过程都是自动完成的;
在抓包的过程中,我们有时候还能看到一个RST包,他也是tcp协议栈中的一个状态;他是说明基于传输数据的这条连接不存在或者产生异常;产生RST消息的情况有很多种,例如:基于一条正在断开或已经断开的连接,连接一个服务器没有监听的端口等等;
在上面四次挥手过程中,我们看到time_wait状态会维护两个RTT时间,大家有想过这是为什么吗?
RTT是指一条消息从client到server所耗费的时间,两个RTT是因为当网络出现拥塞,我们响应的数据包丢失,对端会重新发送fin消息,而对端需要感知到消息丢失,需要耗费一个RTT时间,对端重发消息,在网络中传输,需要一个RTT时间;所以需要两个RTT时间;
还有一种场景是为了防止网络延时,基于这条连接的数据到达的比较慢,延迟到达的数据对系统后续的影响;
close_wait和time_wait状态
在上图和上面的描述中,我们都看到了close_wait和time_wait状态,并且通过分析状态变化的过程,我们知道了他们产生的原因,那么针对于这种大量的这两种状态,会对系统产生什么样的状态呢?又该怎么处理呢?
我们知道,对于操作系统来说,网络描述符是一种特殊的文件句柄,而句柄资源又是珍贵的,在系统中,句柄数量是有限制的;每一条连接在通信的一端,只能保持六中状态中的一种,如果说系统中存在大量的time_wait和close_wait的状态,说明会占用大量的句柄;所以说,如果不进行处理的,会导致句柄泄漏,影响后续对句柄的申请;导致运行在本机器上的其他程序出现异常;
如何进行处理呢?
针对于close_wait状态,那是由于被动接收方在收到对端关闭连接之后,没能立即调用close方法导致的,所以我们在编程的时候,需要实时监控通信端口,根据对端的动作做出及时的响应,在感知到对端在关闭连接之后,立即调用了close方法,断开连接;同时也可以使用setsockopt 函数,设置SO_KEEPALIVE;利用协议栈内部的保活机制;同时在业务成也可以实现一个定时机制,在检测到长时间没有数据传输的情况下,主动断开连接;等等;
针对于time_wait状态,它是由于主动关闭方在接收到被动挂壁方发的fin消息之后的状态;由于西药保留2个RTT时间后系统才会回收,如果处于快速连接断开的服务,则会缠身非常的多;在linux系统下,针对于这种情况,可以通过修改配置文件的方式来减小影响:
修改配置文件 /etc/sysctl.conf中的以下几个配置项:
net.ipv4.tcp_syncookies= 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_tw_reuse= 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle= 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系統默认的TIMEOUT时间
listen方法参数理解
我们在网络通信的过程中,服务端会调用lisetn函数,来监听客户端的连接,listen方法有两个参数,第一个参数为监听句柄,第二个参数为可以同时连接的最大连接数量;在实际的实现过程中,业务层在调用listen方法后,协议栈内部会创建两个队列,用来保存客户端主动过来的连接请求,第一个列表用于保存在三次握手过程中的请求,第二个列表用于保存已经完成三次握手的列表;应用调用accept方法,将会从已完成三次握手的队列中将网络句柄移除;listen中第二个参数指的是两个队列的瞬间总值,如果这两个队列当前的值超过设置数值的1.2倍,协议栈将对发起连接的客户端不进行处理。该值并不是指整个程序可以保持连接的最大数,仅仅指瞬间可接受的连接请求的最大值