17、TCP协议之四次挥手
在 TCP 中,既然有握手来建立连接,就有挥手跟你拜拜,断开连接。为什么要断开连接呢?如果没有挥手,那么就会无限期保持连接,我们要知道,TCP 的连接是要占用系统资源的,比如端口号和内存,大家都占着茅坑不拉屎的话,很快厕所就满了。在 TCP 的世界里,挥手是冷冰冰的指令,而在我们的生活中,挥手往往显得那么沉重!
想起吴奇隆的《祝你一路顺风》:
当你背上行囊 卸下那份荣耀
我只能让眼泪留在心底
面带着微微笑 用力的挥挥手
祝你一路顺风
一、前言
挥手,即告别的意思,让我想起昨天看的金刚川这部电影。
没错,张译和吴京把我拉回了电影院,钦佩两人自然而细腻的演技,尤其是张译那略带羞赧的表演。此外,个人喜欢战争题材的电影,因为残酷的背景更让我愿意去相信这群人的信仰。当代人距离那场战争已越来越遥远了,和平的年代让很多人忘记了对生命的尊重和对和平的珍惜,实际上是对这个时代最大福报的忽视。
这部电影让我泪目、印象最深的是老关(吴京)与张飞(张译)的对话:
老关:要不,把你炮弹匀我十发,也算你心疼我?
张飞:能成!
这两句话在电影中说出时,我相信了他们之间表面互相嫌弃、埋怨,实际上极为亲近的战友关系,两个惺惺相惜的生命开始熠熠生辉起来了。随后,老关便先牺牲了,一瞬间身体直接被炮弹打碎,他两从此分别!
下面言归正传。转向今天的主角:TCP四次挥手。
二、天下没有不散的宴席
stephen
:收到(ACK
),不过我刚才说的话还没说完,你下面就听我把最后两句话说完哈,巴拉巴拉。。。
这里用到了上篇文章中说的FIN
标志位。
该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。
上图主要包含以下几个步骤:
①客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入
FIN_WAIT_1
状态。(FIN 段是可以携带数据的,比如客户端可以在它最后要发送的数据块可以“捎带” FIN 段。当然也可以不携带数据。客户端发送 FIN 包以后不能再发送数据给服务端,但是还可以接受服务端发送的数据。这个状态就是所谓的「半关闭(half-close)」)②服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSED_WAIT
状态。③客户端收到服务端的
ACK
应答报文后,之后客户端进入FIN_WAIT_2
状态。④等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态。⑤客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后客户端进入TIME_WAIT
状态。⑥服务器收到了
ACK
应答报文后,服务端进入了CLOSE
状态,至此服务端已经完成连接的关闭。⑦客户端在经过
2MSL
一段时间后,客户端自动进入CLOSE
状态,至此客户端也完成连接的关闭。
提一句,里面的序列号和确认号就不多说了,都是基于开始确定的数进行累加,所以这里图上就忽略了对这两个数字的说明。
我们可以从上面流程中看到,每个方向都需要一个 FIN
和一个 ACK
,因此通常被称为四次挥手。这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT
状态。
为什么要挥手四次?为什么不能是3次呢?原因是:
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了,但是客户端还能接收数据。
服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
可以看到,一般情况下,主动发起断开的一方肯定是因为它已经没有更多数据要发送了,不过服务端呢?往往还是有数据没发完的,还要继续发给客户端。这个情况下,如果先不回复ACK
的话,客户端有可能会误以为服务端没收到而重发不必要的FIN
包,如下图所示。所以服务端先发ACK
,等数据发完了再发FIN
。服务端的这个逻辑是不是很像HTTP异步回执处理,先回个OK我收到,然后我慢慢处理,处理完毕再把结果返回给你。
如果服务端确定没有什么数据需要发给客户端,那么当然是可以把 FIN
和 ACK
合并成一个包,四次挥手的过程就成了三次。一个实际的抓包截图:
此时就可以变成三次挥手:
上图中左边的四次挥手过程中,编号为 (1) 和 (3) 的步骤中,数据包中除了 FIN
这个标志位被设置,一般也设置了 ACK
标志位,用于回复上一个收到的数据包。这里为了不引起大家的混淆,所以我没有写成 FIN
+ ACK
的形式。很多教材上编号为 (1) 和 (3) 的步骤上写的是 FIN
+ ACK
。
三、为什么 TIME_WAIT 等待的时间是 2MSL?
这个问题包含两个问题:
为什么需要
TIME_WAIT
状态?为什么要等待2MSL?
四次挥手中最不好理解的就是为什么发起断开的一方需要有TIME_WAIT
这个状态,且为什么要等到2MSL
?
我们先来看看 MSL
是啥。
MSL 是
Maximum Segment Lifetime
英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
假设主动断开的一侧为A,被动断开的一侧为B。
第一个消息:A发FIN
第二个消息:B回复ACK
第三个消息:B发出FIN
此时,双方其实都达成共识,即双方都同意关闭连接了,但是此时B发出了FIN
就可以直接释放TCP
连接所要占的资源了吗?不能,B一定要确保A收到自己的ACK
、FIN
。
如何确认呢?那就是B要等A发回最后的一个ACK
报文。这个ACK
就是对B的FIN
报文的确认,B收到这个ACK
后,就可以安心释放此TCP
连接了!所以被动关闭的B无需任何wait time
,直接释放资源。
不过A呢?A不知道B有没有收到自己的ACK
呀!其实A是这么想的:
如果B没有收到自己的
ACK
,会超时重传FIN
如果B收到自己的
ACK
,也不会再发任何消息,包括ACK
无论是1还是2,A都需要等待,1比较好理解,如果B确实没收到ACK
,A还关闭了,那么B不就不能正常关闭了吗?所以第1种情况A一定要等待的。
对于第2种情况,需要反过来想,为什么也要等待呢?如果不等待或者等待时间太短会发生什么呢?
有可能下次连接还是用的一样的端口,那么上次的延时数据包可能会在新的连接中出现:
这时有相同端口的 TCP 连接被复用后,被延迟的
SEQ = 300
抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。
因此A必须要等待,那么这里等待时间如何取值呢?要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:
去向ACK
消息最大存活时间(MSL
) + 来向FIN消息的最大存活时间(MSL
)。这恰恰就是2MSL
。
等待2MSL
时间,A就可以放心地释放TCP占用的资源、端口号,此时可以使用该端口号连接任何服务器,因为经过这个时间的等待,经过 2MSL
这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
好,其实已经解释了为什么要等2MSL
,但是不知道读者是不是有一些疑问,至少我是!
①疑问1:2MSL一般是多久?
在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
②疑问2:如果B一直收不到随后一个ACK,是不是会一直处于last ack?
在A关闭连接后,B重传FIN的次数有上限,所以超过了上限B就会reset连接,所以我们要尽可能地正常关闭,而不是这种异常关闭,毕竟异常关闭是兜底操作,比较浪费时间,且这个过程中,服务端一直处于
LAST_ACK
状态。
③疑问3:定时重传是等多久?
一般超时重传只有0.5秒、1秒、2秒…16秒,所以不可能发生等A结束了还能收到来自B的FIN,即使B的FIN是存活了MSL才到,A等待2MSL也会等到它。此外,可以看到,2MSL是可以保证B收到最后的ACK并关闭连接,如果一直收不到,参照疑问2。
④疑问4:那我最坏情况下,比如16秒后的重发FIN
,这个时候TIME_WAIT
会重置吗?
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
四、TIME_WAIT 过多有什么危害?
时间太短不行,太长呢?实际上也不行,如果服务器有处于 TIME-WAIT
状态的 TCP,则说明是由服务器方主动发起的断开请求。
过多的 TIME-WAIT
状态主要的危害有两种:
第一是内存资源占用;
第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口;
第二个危害是会造成严重的后果的,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000.这个是可以配置的,不过资源一定要珍惜,长期占着茅坑不拉屎,当连接数起来了,就会不够用。