vlambda博客
学习文章列表

17、TCP协议之四次挥手

    在 TCP 中,既然有握手来建立连接,就有挥手跟你拜拜,断开连接。为什么要断开连接呢?如果没有挥手,那么就会无限期保持连接,我们要知道,TCP 的连接是要占用系统资源的,比如端口号和内存,大家都占着茅坑不拉屎的话,很快厕所就满了。在 TCP 的世界里,挥手是冷冰冰的指令,而在我们的生活中,挥手往往显得那么沉重!

想起吴奇隆的《祝你一路顺风》:

当你背上行囊 卸下那份荣耀

我只能让眼泪留在心底

面带着微微笑 用力的挥挥手

祝你一路顺风

一、前言

    挥手,即告别的意思,让我想起昨天看的金刚川这部电影。

    没错,张译和吴京把我拉回了电影院,钦佩两人自然而细腻的演技,尤其是张译那略带羞赧的表演。此外,个人喜欢战争题材的电影,因为残酷的背景更让我愿意去相信这群人的信仰。当代人距离那场战争已越来越遥远了,和平的年代让很多人忘记了对生命的尊重和对和平的珍惜,实际上是对这个时代最大福报的忽视。

    这部电影让我泪目、印象最深的是老关(吴京)与张飞(张译)的对话:

  • 老关:要不,把你炮弹匀我十发,也算你心疼我?

  • 张飞:能成!

    这两句话在电影中说出时,我相信了他们之间表面互相嫌弃、埋怨,实际上极为亲近的战友关系,两个惺惺相惜的生命开始熠熠生辉起来了。随后,老关便先牺牲了,一瞬间身体直接被炮弹打碎,他两从此分别!

    下面言归正传。转向今天的主角:TCP四次挥手。

二、天下没有不散的宴席

  • stephen:收到(ACK),不过我刚才说的话还没说完,你下面就听我把最后两句话说完哈,巴拉巴拉。。。

17、TCP协议之四次挥手

    这里用到了上篇文章中说的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我收到,然后我慢慢处理,处理完毕再把结果返回给你。

17、TCP协议之四次挥手

    如果服务端确定没有什么数据需要发给客户端,那么当然是可以把 FIN 和 ACK 合并成一个包,四次挥手的过程就成了三次。一个实际的抓包截图:

17、TCP协议之四次挥手

    此时就可以变成三次挥手:

    上图中左边的四次挥手过程中,编号为 (1) 和 (3) 的步骤中,数据包中除了 FIN 这个标志位被设置,一般也设置了 ACK 标志位,用于回复上一个收到的数据包。这里为了不引起大家的混淆,所以我没有写成 FIN + ACK 的形式。很多教材上编号为 (1) 和 (3) 的步骤上写的是 FIN + ACK

三、为什么 TIME_WAIT 等待的时间是 2MSL?

    这个问题包含两个问题:

  1. 为什么需要 TIME_WAIT 状态?

  2. 为什么要等待2MSL?

    四次挥手中最不好理解的就是为什么发起断开的一方需要有TIME_WAIT这个状态,且为什么要等到2MSL

我们先来看看 MSL 是啥。

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

假设主动断开的一侧为A,被动断开的一侧为B。

  • 第一个消息:A发FIN

  • 第二个消息:B回复ACK

  • 第三个消息:B发出FIN

    此时,双方其实都达成共识,即双方都同意关闭连接了,但是此时B发出了FIN就可以直接释放TCP连接所要占的资源了吗?不能,B一定要确保A收到自己的ACKFIN

    如何确认呢?那就是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.这个是可以配置的,不过资源一定要珍惜,长期占着茅坑不拉屎,当连接数起来了,就会不够用。