写点什么

为什么 TCP 建连接要 3 次,断连接却要 4 次呢?

发布于: 2021 年 05 月 18 日

大家好,今天聊聊传输层通信协议 TCP 的经典问题:建连接与断连接


网络上的传输是没有连接的,包括 TCP 也是一样的。

而 TCP 所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP 的状态变换是非常重要的。


很多人会问,为什么建链接要 3 次握手,断链接需要 4 次挥手?

  • 对于建链接的 3 次握手,主要是要初始化 Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的 Sequence Number(缩写为 ISN:Inital Sequence Number)——所以叫 SYN,全称 Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP 会用这个序号来拼接数据)。

  • 对于 4 次挥手,其实你仔细看是 2 次,因为 TCP 是全双工的,所以,发送方和接收方都需要 Fin 和 Ack。只不过,有一方是被动的,所以看上去就成了所谓的 4 次挥手。如果两边同时断连接,那就会就进入到 CLOSING 状态,然后到达 TIME_WAIT 状态。下图是双方同时断连接的示意图(你同样可以对照着 TCP 状态机看):


 

另外,有几个事情需要注意一下:

  • 关于建连接时 SYN 超时。试想一下,如果 server 端接到了 clien 发的 SYN 后回了 SYN-ACK 后 client 掉线了,server 端没有收到 client 回来的 ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server 端如果在一定时间内没有收到的 TCP 会重发 SYN-ACK。在 Linux 下,默认重试次数为 5 次,重试的间隔时间从 1s 开始每次都翻售,5 次的重试时间间隔为 1s, 2s, 4s, 8s, 16s,总共 31s,第 5 次发出后还要等 32s 都知道第 5 次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP 才会把断开这个连接。

  • 关于 SYN Flood 攻击。一些恶意的人就为此制造了 SYN Flood 攻击——给服务器发了一个 SYN 后,就下线了,于是服务器需要默认等 63s 才会断开连接,这样,攻击者就可以把服务器的 syn 连接的队列耗尽,让正常的连接请求不能处理。于是,Linux 下给了一个叫 tcp_syncookies 的参数来应对这个事——当 SYN 队列满了后,TCP 会通过源地址端口、目标地址端口和时间戳打造出一个特别的 Sequence Number 发回去(又叫 cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie 发回来,然后服务端可以通过 cookie 建连接(即使你不在 SYN 队列中)。请注意,请先千万别用 tcp_syncookies 来处理正常的大负载的连接的情况。因为,synccookies 是妥协版的 TCP 协议,并不严谨。对于正常的请求,你应该调整三个 TCP 参数可供你选择,第一个是:tcp_synack_retries 可以用他来减少重试次数;第二个是:tcp_max_syn_backlog,可以增大 SYN 连接数;第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。

  • 关于 ISN 的初始化。ISN 是不能 hard code 的,不然会出问题的——比如:如果连接建好后始终用 1 来做 ISN,如果 client 发了 30 个 segment 过去,但是网络断了,于是 client 重连,又用了 1 做 ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client 的 Sequence Number 可能是 3,而 Server 端认为 client 端的这个号是 30 了。全乱了。RFC793 中说,ISN 会和一个假的时钟绑在一起,这个时钟会在每 4 微秒对 ISN 做加一操作,直到超过 2^32,又从 0 开始。这样,一个 ISN 的周期大约是 4.55 个小时。因为,我们假设我们的 TCP Segment 在网络上的存活时间不会超过 Maximum Segment Lifetime(缩写为 MSL – Wikipedia 语条),所以,只要 MSL 的值小于 4.55 小时,那么,我们就不会重用到 ISN。

  • 关于 MSL 和 TIME_WAIT。通过上面的 ISN 的描述,相信你也知道 MSL 是怎么来的了。我们注意到,在 TCP 的状态图中,从 TIME_WAIT 状态到 CLOSED 状态,有一个超时设置,这个超时设置是 2*MSL(RFC793 定义了 MSL 为 2 分钟,Linux 设置成了 30s)为什么要这有 TIME_WAIT?为什么不直接给转成 CLOSED 状态呢?主要有两个原因:1)TIME_WAIT 确保有足够的时间让对端收到了 ACK,如果被动关闭的那方没有收到 Ack,就会触发被动端重发 Fin,一来一去正好 2 个 MSL,2)有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存 IP 数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。

  • 关于 TIME_WAIT 数量太多。从上面的描述我们可以知道,TIME_WAIT 是个很重要的状态,但是如果在大并发的短链接下,TIME_WAIT 就会太多,这也会消耗很多系统资源。只要搜一下,你就会发现,十有八九的处理方式都是教你设置两个参数,一个叫 tcp_tw_reuse,另一个叫 tcp_tw_recycle 的参数,这两个参数默认值都是被关闭的,后者 recyle 比前者 resue 更为激进,resue 要温柔一些。另外,如果使用 tcp_tw_reuse,必需设置 tcp_timestamps=1,否则无效。这里,你一定要注意,打开这两个参数会有比较大的坑——可能会让 TCP 连接出一些诡异的问题(因为如上述一样,如果不等待超时重用连接的话,新的连接可能会建不上。正如官方文档上说的一样“It should not be changed without advice/request of technical experts”)。


·················· END ··················


作者:架构精进之路,十年研发风雨路,大厂架构师,CSDN 博客专家,专注架构技术沉淀学习及分享,职业与认知升级,坚持分享接地气儿的干货文章,期待与你一起成长

关注并私信我回复“01”,送你一份程序员成长进阶大礼包,欢迎勾搭。



Thanks for reading!

发布于: 2021 年 05 月 18 日阅读数: 21
用户头像

坚持分享接地气儿的架构技术文章! 2018.02.26 加入

同名微信公众号「架构精进之路」,专注软件架构研究,技术学习与职业成长!坚持原创总结、沉淀和分享,希望能带给大家一些引导和启发,感谢各位的支持(关注、点赞、分享)!

评论

发布
暂无评论
为什么TCP 建连接要3次,断连接却要4次呢?