当TCP的连接建立完成后,我们就可以尽情的通过TCP连接所创立的会话来进行数据的传输了。当然,再有意思的话题也有说完的时候,所以,当数据传输完之后,TCP该如何善后呢?
TCP的四次挥手
TCP的连接断开需要经历4次数据包的交互才能完成,所以这个过程我们习惯性的称为“四次挥手”。当然,结束连接之后,主机中的"资源"(即缓存和变量)也就被释放掉了。
TCP的的断开也是双方均可以主动发起,我们上图以客户端主动发起为例,来说明这个过程。
- 首先,客户端发现自己需要发送的数据已经发送完毕,有了断开TCP连接的想法。它会发出一个TCP报文段,这个报文段中的FIN标记位将置1。(这里注意,和TCP的三次握手不同,FIN报文段是允许携带数据的,所以,它可能会包含整个数据流中最后一段数据。)
- 服务器收到对方的FIN断开请求报文段,则回复一个ACK报文段。
- 等待服务器把需要发送的数据都处理完毕之后,服务器也会发送一个FIN断开请求的报文段给客户端。
- 客户端收到后,也将回复一个ACK作为最后的确认。至此,服务器将关闭TCP的连接。而客户端也将在等待一段时间(图示的2MSL)后关闭。(这个等待时间及等待原因,我们后面再分析)
以上便是TCP断开的一个正常流程了。我们这个过程中只关注了交互的方式,并没有关注具体数据包中的序列号和确认序列号,那是因为这几个数据包是允许包含数据的,所以,这个序列号和确认序列号需要考虑携带数据的字节数。
断开连接过程中的状态变化
同样,这个图中,也标识出了断开过程中客户端以及服务器的状态变化。我们还是分别从客户端和服务器的视角来看一下这个状态变化。
我们还是先从客户端的角度出发,来观察下这个状态变化。
- 客户端一开始,还处于ESTABLISHED建立的状态,现在,他发现它数据传递完毕,于是发送了FIN断开请求报文段,之后,变进入到了一个新的状态 --- FIN_WAIT_1。
- 客户端再FIN_WAIT_1状态下,就是在等待服务器回复ACK,一旦等到之后,将进入到下一个状态。
- 客户端在收到服务器发送的ACK之后进入的状态被称为FIN_WAIT_2状态。这个状态主要就是等待服务器发送FIN请求。
- 客户端在收到服务器发送的FIN断开请求之后,将回复ACK进行确认。但注意,客户端并没有直接断开连接,进入到CLOSE(关闭)状态,而是进入到了一个TIME_WAIT状态继续等待。
- 在这个TIME_WAIT状态等待2MSL(这是一个时间),之后进入CLOSE(关闭)状态。进入到关闭状态后客户端将释放掉所有给这个TCP连接分配的资源。
以上便是客户端在断开连接过程中出现的状态变化。注意,服务器也可以主动发起断开,则服务器方的状态变化也将经历这几个过程。
下来我们再开看下服务器方的状态变化。
- 首先,服务器也还处在ESTABLISHED建立的状态。在收到客户端的FIN断开请求后,服务器会回复一个ACK进行确认,同时进入到CLOSED_WAIT状态。
- 这个状态随着服务器自身发送FIN断开请求报文段之后,将结束,并进入到一个新的状态 --- LSA_ACK状态。
- 从这个状态的名字就可以看出来,服务器在等待最后的一个ACK报文段。当服务器接收到这个ACK之后,则将进入最终的状态,也就是CLOSE(关闭)状态。之后,将给这个TCP连接分配的所有资源释放掉。
以上便是整个“四次挥手”断开过程的状态变化。需要注意的就是,这里的客户端和服务器是相对的,先发送FIN的需要经历“客户端”那一套,也就是需要在关闭前进行等待。而被动方则走的是“服务器”那一套,再收到ACK报文之后直接关闭。
为什么需要有TIME_WAIT状态
不出意外的话,再看过TCP断开的四个过程中的状态变化之后,大家可能都会有一个疑问,就是这个FIN的发起方为啥需要等待一段时间之后,才能关闭TCP连接?也就是这个TIME_WAIT状态存在的意义是什么?
其实这是很重要的一个等待,因为他是我们连接能够正常被关闭的保证。
我们先假设没有TIME_WAIT或者这个时间过短,看看会发生什么样的事情。
从上边的图中,我们其实就已经可以清楚的看出这个等待的必要性了。因为我们永远不能以最理想的眼光来看待网络通信。说不定,那个数据包就迷失在这网络数据之海中了。
这里假设客户端发送的最后一个ACK在网络中丢失了,而我们客户端不等待或者等待时间很短,则将自顾自的关闭了TCP连接。但是,服务端没有收到最后的确认,则一直停留在了LAST_ACK状态了。这样很不负责任。
服务器长时间接收不到ACK,则也将触发重传(TCP的可靠之处了),服务器会再发送一个FIN断开请求。而此时,客户端已经关闭了该连接通道,则将回复RST来终结连接,这就属于异常的断开。
TIME_WAIT的时间为什么是2MSL
从上面的分析,我们可以看出来这个等待是必要的,当然,只需要发起方等待就可以了,因为它最后发送的ACK报文可能会出现问题,需要等待反馈。
而且,这个时间的长短也很重要,不能太短,否则服务器方的反馈还没有回来,你就关闭了,那还不如不等。这里定义的时间长短是2MSL。
MSL --- Maximum Segment Lifetime。翻译过来叫做报文最大生存时间。这个时间指的是一个报文在网络上存在的最长时间,超过这个时间,则该数据报文将被丢弃。这个和IP头部中的TTL --- 生存时间还不一样,因为它是一个正儿八经的时间概念。
TTL是什么?
TTL被称为生存时间,是IP头部中定义的一个变量。它的用法是一个数据包,每经过一个路由器的转发,这个TTL值将减1,当一个数据包的TTL值被减为0的时候,则将不会被转发,而直接被丢弃。它并不是一个时间概念,反应的是经过路由的跳数。
所以,这个MSL的值应该大于等于TTL值消耗为0的值,以确保报文已被自然消亡。
那这里的2MSL指的就是2被的MSL时间。
TIME_WAIT的时间被设定为2MSL,也有有这方面的考虑的。这个数据包足够正常数据包的一个重传时间,比如如果被动关闭方没有收到断开连接的最后的ACK报文,就会触发超时重发Fin报文,另一方接收到FIN后(收到FIN断开请求后,客户端的TIME_WAIT状态会刷新,即等待时间重新计时2MSL),会重发ACK给被动关闭方,一来一去正好2个MSL。
Linux系统里,2MSL默认是60S
设置2MSL的TIME_WAIT时间还能应对一种情况,就是防止旧连接的数据包造成错乱。
这里也给大家一个场景。假设,服务器发送的一个数据报文在网络中被阻塞了,之后,客户端发起了断开连接的请求,同时请求的确认序列号应该是服务器之前丢失的那一个。此时,服务器将回复ACK并携带之前发送过的数据段进行重传。之后,TCP的连接正常断开。但注意,此时服务器之前被阻塞的数据段依旧在网络中慢悠悠的前进中。
恰巧,客户端有发起了一个新的连接。而之前这个数据包的序列号又恰好在新连接序号范围内,则将被客户端接收,造成数据错乱的后果。
所以,设计这2MSL的等待时间,足以让两个方向上的数据报都被丢弃,再出现新的连接时,不至于被历史报文造成数据错乱。
这里有一个点,需要大家注意一下。那就是客户端新建的连接使用的序列号问题。我们前面说过,客户端在发起连接时使用的随机的初始序列号ISN为了防止历史数据造成混乱,这个确实没错。但是,这个序列号并不是一个无限的值,其本质是由32位二进制构成的,所以其取值范围也是有限的。这个初始值在随机的时候,是基于时钟的,回滚一次需要大概4.55小时。但是,如果发送的数据量过大的话,是会加速这个回滚过程的。所以,是有可能出现上述场景的。
当然,这个时间也不能设置过长。时间太长必然导致内存资源的占用,并且,如果是连接发起方处在TIME_WAIT状态,则次状态下,还会导致其使用的端口资源被占用。(接收连接方也就是服务器方,理论上监听的同一个端口可以建立多个连接的,所以,不会造成端口占用的问题。当然,服务器如果TIME_WAIT状态的连接太多的话,也会大量的消耗系统资源,导致无法处理新的连接。)
为什么TCP的挥手需要四次而不能是三次
这是一个很经典的问题,不过其实也不难解释,如果认证看来前面断开的过程,其实就已经推理出一二了。
这个问题的核心其实在于"数据"上。理想的情况下,肯定是希望断开过程像连接建立过程一样,中间两步可以合成一步,变成“三次挥手”,这样,肯定是可以将资源节约下来的。但问题就是,你客户端(假设客户端是断开连接的发起方)数据发完了,我服务器(假设服务器是被动方)就也一定发完数据了吗?
所以,服务器发送FIN一定是取决于自己数据发送完毕的情况下,而不是为了赶给客户端回复ACK这个趟。
当然,也并不是说挥手就不能是三次。如果,恰好客户端发送FIN后,服务器回复的ACK中携带的是最后的数据报文段了,那同时将这个数据报文段中的FIN置1也未尝不可。那这样也就是三次挥手的情况了。
所以,这一切都在于“数据”是否被发送完毕。理论上的,我们还是要按照四次来进行理解记忆的。