文章目录
- 概要&序論
- 一、深入理解三次握手与四次挥手
- 1.1 三次握手与系统底层的关系
- 1.2 为什么一定要进行三次握手?(面试高频)
- 1.2.1 谋求共识与全双工验证
- 1.2.2 握手的本质与“四次合并”逻辑
- 1.3 四次挥手与半关闭状态
- 1.3.1 拆分机制与单向信道断开
- 1.3.2 延迟确认与谋求共识的区别
- 1.4 CLOSE_WAIT 状态与文件描述符泄漏
- 1.4.1 CLOSE_WAIT 的产生与危害
- 1.4.2 系统解耦与 shutdown 接口
- 1.5 TIME_WAIT 状态与地址复用(SO_REUSEADDR)
- 1.5.1 TIME_WAIT 的困境与生产痛点
- 1.5.2 解决方案:SO_REUSEADDR 端口复用
- 1.6 深入理解 TIME_WAIT 的本质与 2MSL 等待
- 1.6.1 2MSL 时间的定义与作用
- 1.6.2 网络“流弹”场景深度剖析
- 1.6.3TIME_WAIT状态保护的保护作用
- 1.6.4既要解决网络流弹又不想等待TIME_WAIT的辅助方案(TCP随机序列号ISN机制)
- 二、连接状态
- 2.1 服务端状态转化
- 2.2 客户端状态转化
概要&序論
Hello大家好,我是此方。上文开始我们进入TCP的深入理解阶段,本文继续讲解,在前文的基础上深入理解三次握手与四次挥手的详细内容。
一、深入理解三次握手与四次挥手
1.1 三次握手与系统底层的关系
在深入分析握手过程之前,我们需要先思考一个问题:连接,要不要被管理??要,先描述在组织!建立连接是有成本的,包含时间成本与空间成本(例如数据结构struct Link)。
三次握手的具体过程有客户端和服务器的操作系统自动完成,而connect只是负责发起三次握手,accept不参与三次握手。accept负责把已经建立好的连接拿上来!
1.2 为什么一定要进行三次握手?(面试高频)
1.2.1 谋求共识与全双工验证
男女双方要达成夫妻的条件是什么?
- 双方谋求共识
- 双方父母等外部条件得到满足
TCP 建立连接选择三次握手,主要有两个核心理由:
第一,以最小成本,100%确认双方通信意愿。
第二,是以最短的方式,进行验证全双工!本质是验证:我们两个所处的网络是通畅的,能够支持全双工!
1.2.2 握手的本质与“四次合并”逻辑
三次握手,真的只是三次握手吗??错误的!三次握手实际上是四次握手,只不过对端携带应答,把第一次握手的应答连同第二次握手的报文一块儿发过去了。
为什么“三次握手”本质上是三次,而不是“四次合并”?
- 建立连接时,双方的诉求是一致的(即双方都要确认对方的接收与发送能力)。服务器收到客户端的 SYN 后,由于自己也需要发起建立连接,并且顺便确认客户端的请求(发送 ACK),这两件事在逻辑上是同时发生的。因此,TCP 协议从底层规范上就把它们设计在同一个报头文段里(SYN+ACK 标志位同时置1),这本身就是单次原子操作,而不是被迫把两次通信“拼凑”在一起。
1.3 四次挥手与半关闭状态
1.3.1 拆分机制与单向信道断开
在断开连接时,客户端和服务器断开连接需要进行四次挥手。
为什么“四次挥手”通常不能合并成三次?
- 半关闭状态(Half-Close):TCP 是全双工通信。当客户端发送 FIN 断开连接时,只代表客户端不再发送数据了;但此时服务器可能还有没发完的数据要继续传给客户端。
- 拆分机制:服务器必须先立即回复一个 ACK(告诉客户端:“你的断开请求我收到了”),随后继续传输剩余数据。等服务器把所有数据发完后,才会单独再发一个 FIN 给客户端。因此,挥手过程天然被拆成了 4 个阶段(FIN -> ACK -> FIN -> ACK)。
1.3.2 延迟确认与谋求共识的区别
只有在极极其特殊且刚好没有残留数据要发的情况下,TCP 才会开启延迟确认机制将中途的 ACK 和 FIN 偶尔地合并发送,但这并不是常态。
总的来说:为什么四次挥手不能合并?因为三次握手的时候“更加容易谋求共识”,而四次挥手的时候客户端和服务器断开的时间往往不能谋求一致,第二次挥手和第三次挥手无法实现捎带应答。
1.4 CLOSE_WAIT 状态与文件描述符泄漏
1.4.1 CLOSE_WAIT 的产生与危害
在四次挥手过程中,如果客户端主动发起断开请求(发送 FIN),服务端在收到后会回应 ACK,此时服务端就会进入CLOSE_WAIT状态。
如果客户端已经退出或者关闭,而服务器端就是不关闭(代码中没有显式调用close(sockfd)),服务器端就会一直处于 CLOSE_WAIT 的状态!
这种情况下,连接并没有被彻底释放,会引发文件描述符的资源泄漏。
1.4.2 系统解耦与 shutdown 接口
当然,实际开发中并不需要这么麻烦!我们的应用层和操作系统是解耦的。我们直接在两端调用close正常关闭描述符就可以了。至于这种问题交给操作系统,操作系统会帮我们完成,不用担心服务器的剩余发送数据被丢弃的情况。
我们的
write接口没有直接把报文发送到网络中的能力,它是将报文从应用层拷贝给缓冲区。有操作系统将缓冲区中的内容发送到网络。于是操作系统就可以执行流量控制和超时重传等操作!这也是一种解耦。
此外,系统还提供了一个系统调用shutdown,它可以根据你传递的选项自由选择关闭文件描述符的读端、写端或者是全部关闭。例如设置为SHUT_WR时,客户端可以关闭写的单向信道,保留读端读取服务器发送过来的剩余消息,即将全双工变成半双工。
1.5 TIME_WAIT 状态与地址复用(SO_REUSEADDR)
1.5.1 TIME_WAIT 的困境与生产痛点
主动断开连接的一方,在完成四次挥手后,要进入一个状态,叫做TIME_WAIT。即便四次挥手完成,主动关闭方也不会立刻回到 CLOSED 状态。
从应用角度看,既要 TIME_WAIT,又要让服务器立即重启!
但是 TIME_WAIT 解决问题的同时自身也有很大的问题,在一些场景中,如果我们的服务器突然挂掉了,这个时候必须以“相同端口”的方式高度重构重启。比如在双11期间,大量客户涌入服务器,服务器炸掉了,这个时候如果等待 TIME_WAIT 面而不立刻重启会引发巨大的经济损失。
1.5.2 解决方案:SO_REUSEADDR 端口复用
我们有解决方案:使用SO_REUSEADDR选项!它允许套接字强制绑定并重用处于 TIME_WAIT 状态的已有 IP 和端口(支持服务器挂掉后以完全相同的 IP/端口立即重启):
intopt=1;setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));1.6 深入理解 TIME_WAIT 的本质与 2MSL 等待
1.6.1 2MSL 时间的定义与作用
TCP 协议规定,主动关闭连接的一方要处于 TIME_WAIT 状态,等待两个 MSL(Maximum Segment Lifetime,最大报文生存时间)的时间后才能回到 CLOSED 状态。
MSL 是 TCP 报文的最大生存时间,因此 TIME_WAIT 持续存在 2MSL 的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到数据,导致数据混乱)。
1.6.2 网络“流弹”场景深度剖析
如何理解 TIME_WAIT?TIME_WAIT 解决的是一种可能性非常低的 bug。我们客户端和服务器在建立通信正常交流的时候,可能存在这样一个报文,这个报文有以下性质:
- 没有到达 MSL,依然在网络中保持存活状态。
- 可能阻塞在网络中的任何一个路由器的阻塞队列中。
- 超时引发发送端的超时重传机制。
- 通信两端全部关闭。
- 阻塞时间确实很久但是没有达到 MSL。
- 两台主机关闭后立即重启,重启使用的通信 IP 和端口号全部和上一次使用的 IP 与端口一致。
- 就在两台主机重启后建立连接的某一次握手,刚好到达了目标主机。干扰了三次握手,导致三次握手失败。
所以我们需要在上次连接断开之前先等待 2MSL 的时间,让这种流弹保证在上一次连接断开后到达目标主机后被丢弃或者在网络中由于到达 MSL 被丢弃。这种情况发生的可能性非常低,但是只要不是 0 就会在每天数以亿计的世界网络通信中天天发生。
1.6.3TIME_WAIT状态保护的保护作用
主动断开的一方处于 TIME_WAIT 的状态的时候,是不能在以同样的端口号+IP地址绑定重启的!一定会发生 bind error。
其实 TIME_WAIT 实际上也倒逼着我们的用户在重启的时候不使用原来的端口号。如果我们使用新的端口号重新启动,“陈旧报文”到来的时候,接收方会去判断它的四元组(源端口 源IP 目的端口 目的IP),如果有一个不一样!就会直接丢弃报文。
TIME_WAIT 还有一个功能,这是比较容易想到的:如果发起方发送的最后的那个 ACK 没有到达对端,那么对端就可以发送一个 FIN。一来一回,2MSL 刚好可以覆盖这个过程。——确保我发出去的 ACK 可以被正常接收到。
换句话说,我在 TIME_WAIT 的期间。没有收到消息,就是好消息。没有收到消息就代表我发出的 ACK 到达了。
1.6.4既要解决网络流弹又不想等待TIME_WAIT的辅助方案(TCP随机序列号ISN机制)
那么先前的问题难道不解决了吗?当然解决。我们采用另外一种辅助方案:
客户端和服务器通信的时候,报文的起始序号,起始时非固定的!比如我这一次启动,客户端发送的报文的起始序号是1000,下一次起始序号就是1428。如果接收方发现接收到的报文的序号与当前通信过程中的报文序号存在巨大出入,那么就会丢弃它。
比如我的缓冲区大小是 5000,我和对面协商好它从 1000 开始发送,但是这个时候突然出来一个 7000 序号的报文,于是这个时候这个报文必须丢弃!
连接管理机制我们正式讲完了。
其实上面的各种连接状态,我只讲解 了最重要的两个,剩下的状态统一展出:
二、连接状态
2.1 服务端状态转化
在 TCP 通信的整个生命周期中,服务端的状态演变如下:
- [CLOSED -> LISTEN]服务器端调用 listen 后进入 LISTEN 状态,等待客户端连接;
- [LISTEN -> SYN_RCVD]一旦监听收到连接请求(同步报文段),就会将该连接放入内核等待队列中,并向客户端发送 SYN 确认报文。
- [SYN_RCVD -> ESTABLISHED]服务端一旦收到客户端的确认报文,就进入 ESTABLISHED 状态,可以进行读写数据了。
- [ESTABLISHED -> CLOSE_WAIT]当客户端主动关闭连接(调用 close),服务器会收到结束报文段,服务器返回确认报文段并进入 CLOSE_WAIT;
- [CLOSE_WAIT -> LAST_ACK]进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用 close 关闭连接时,会向客户端发送 FIN,此时服务器进入 LAST_ACK 状态,等待最后一个 ACK 到来(这个 ACK 是客户端确认收到了 FIN)
- [LAST_ACK -> CLOSED]服务器收到了对 FIN 的 ACK,彻底关闭连接。
2.2 客户端状态转化
相对应地,客户端的状态转化路径如下:
- [CLOSED -> SYN_SENT]客户端调用 connect,发送同步报文段;
- [SYN_SENT -> ESTABLISHED]connect 调用成功,则进入 ESTABLISHED 状态,开始读写数据;
- [ESTABLISHED -> FIN_WAIT_1]客户端主动调用 close 时,向服务器发送结束报文段,同时进入 FIN_WAIT_1;
- [FIN_WAIT_1 -> FIN_WAIT_2]客户端收到服务器对结束报文段的确认,则进入 FIN_WAIT_2,开始等待服务器的结束报文段;
- [FIN_WAIT_2 -> TIME_WAIT]客户端收到服务器发来的结束报文段,进入 TIME_WAIT,并发出 LAST_ACK;
- [TIME_WAIT -> CLOSED]客户端要等待一个 2MSL(Max Segment Life, 报文最大生存时间)的时间,才会进入 CLOSED 状态。
以上是一张汇总图。
- 较粗的虚线表示服务端的状态变化情况;
- 较粗的实线表示客户端的状态变化情况;
- CLOSED是一个假想的起始点,不是真实状态;