背景
TCP 的三次握手四次挥手,估计大家都听过。但是真的能把每一步说明白的人比较少。我还记得在之前面试的时候被面试官一顿问,然后一脸懵 B...
都是大学没好好上课 😢,这篇文章就跟大家讲讲到底这三握四挥是在搞什么飞机。
三次握手
握手是指的双方进行连接的操作。
每一次握手到底是传输了什么?
想知道除了 ip 和端口号,每次传输的信息是什么,我们首先得知道 TCP 传输信息的结构。
第一次握手
- 首先会将 TCP 的 header 部分的控制位(上图的 flags)中的 SYN 置为 1。表示希望建立连接。
- 还有一个重要的数据,seq,即序号。这个东西是干嘛用的呢?这个涉及到 TCP 安全性和可靠性,与 ack 即确认号紧密相关。TCP 在传输数据时,如果数据比较大,会进行拆分操作,将大数据拆成一个个小的数据包。序号就是在这个时候用的。我们必须要知道这个包的顺序是什么,才能把真正的数据在服务端还原。seq 的顺序并不是从 0 或者 1 开始的,而是一个随机值。因为如果序号从 1 开始,那么整个通信的过程非常容易被预测。正因为是随机的,所以对方不知道你的 seq,所以我们需要在开始收发数据之前,将 seq 先发送给对方。序号的初始值的传递就是通过 SYN=1 的操作传递的。
客户端此时处于同步状态,即可以建立连接。服务端处于监听状态。
第二次握手
- 标志位。因为已经收到了客户端的建立连接请求。所以必须要发送 ack,以告知客户端自己已经收到消息。所以 ACK 的标志位要置为 1,且 SYN 也是 1,因为还未建立连接。
- seq。同第一次握手一样,也是一个随机值(TCP 为全双工,所以双方都需要保留 seq 方便处理数据包)。
- ack。是对客户端发过来的序列号进行计算得到的。
服务端处于 SYN 接收状态。
第三次握手
- 标志位。此时只需要 ACK=1。SYN 已经不需要了,双方已经同步完 seq 等信息。
- seq。可以说是第二次握手收到的 ack。
- ack。是对第二次握手收到的序列号进行计算得到的。用以告知已收到二次握手信息。
客户端处于连接建立状态,服务端收到信息之后也会进入连接建立状态,双方可以进行通信。
四次挥手
TCP 连接在数据传输完成之后需要关闭,不然会一直占用系统资源。TCP 的连接关闭需要四次通信才行,分手也是个麻烦事儿。
为什么四次才能断开连接呢?
分手这件事情,两边都说明白,分别断开即可。但是想要确认对方都要断开,那么一次两次是不够的。
我们还是拿 A、B 来举例。假设 A 要主动断开和 B 的连接。
- A 发送断开请求。(需要等待 B 的回复,不然 B 未收到消息 就单方面的断开有点不负责任。超时重传)
- B 收到请求并回复 A。(B 收到请求后,很伤心,但是没有办法,只能断开连接,但是 B 是被动的接收断开,所以需要通知其应用程序做关闭准备)
- B 这边准备完了,通知 A 可以断开了。
- A 回复 B,我收到消息了,断开连接吧。
请求发送的信息是什么?
下面是抓包看到的数据
关闭由哪方开始?
答案是哪边都可以,无论是客户端还是服务器端,都可以主动发起关闭请求。
发起关闭请求的前提是数据发送完毕,不一定非得等待对方确认完成。
第一次挥手
假设客户端发起关闭请求。那么客户端发完消息后,会进入 FIN_WAIT 阶段。此处已经无法再发送应用程序消息,只能处理关闭相关信息。
第二次挥手
服务端收到第一次挥手消息后,返回收到消息的 ack。服务端会进入 CLOSE_WAIT 阶段。
这个阶段是等待关闭阶段,通知应用程序发送剩余数据,处理现场信息,关闭相应资源。
第三次挥手
你应该有点好奇为什么第三次挥手和第二次挥手都是同一个服务。第二次主要是一个 ack 响应,第三次主要是一个服务端关闭通知消息。两者目的不同。
等到三次挥手完毕,那么服务端会进入 LAST_ACK 状态,即等待最后客户端(主动发起关闭的一方)的 ack 确认。
第四次挥手
第四次挥手是主动发起关闭的一方 A,对被动关闭一方 B 的 FIN 消息的确认。
第四次信息发送完成后,A 会进入 TIME_WAIT 阶段,而不是直接删除套接字。具体原因我们在下面讲。
收到第四次挥手信息后,B 会直接进行关闭操作。
为什么会有 TIME_WAIT?
还是那句话,网络并不是一个理想世界,任何异常情况都有可能发生。
为了保证 TCP 连接能够正常关闭,主动发起关闭方不能直接删除套接字,而是需要经过一段时间等待。这个时间一般是 2MSL(Maxumun Segment Lifetime 最大报文生存时间)。
原因如下
- 确认被动关闭方能够正常进入关闭状态。假设 B 未收到 A 最后的一个 ack,会再次发送 FIN 消息(第三次挥手),如果 A 已经 CLOSE 了,那么 B 就会一直重试发送。所以 A 必须要进行一段时间的等待。
- 防止失效请求。假设 A 关闭了此次连接,又重新在原来的端口号上开启了新的连接。原来在网络上发送的一些包(已失效但未超过 ttl)到来之后,无法进行区分是否是正常的包,导致数据混乱。
举个例子来证明没有 TIME_WAIT 的情况。
- A 断开了,并且删除了套接字。
- B 没有收到 A 的最后一次 ack,导致 FIN 重发。
- A 重新开启了新的套接字新的连接,但是这个套接字和之前删除的套接字拥有相同的端口号。
- B 后来重发的 FIN 会错误的跑到新的套接字,导致 A 开始执行断开操作。
所以 TIME_WAIT 也是为了防止上面的误删除。
总结
在并发量很高的时候,熟悉 TCP 的原理和参数变得尤为重要。
我们关注的不仅仅是几次握手 几次挥手,也应该关注一些细节,细节决定成败。更应该多问问为什么如此设计,能解决什么问题。
后续
后续主要是与大家分享讨论一下对 TCP 的滑动窗口机制和可靠性保障的理解。如果有时间,可以跟大家分享一下 TCP 的参数等配置相关内容。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于