一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情。
往期文章
- 网络是怎样连接的(一)—— 浏览器访问 Web 服务器过程概览
- 网络是怎样连接的(二)—— 浏览器生成 HTTP 消息
- 网络是怎样连接的(三)—— 通过 DNS 服务器查询 IP 地址
- 网络是怎样连接的(四)—— DNS 服务器工作介绍
- 网络是怎样连接的(五)—— 委托操作系统进行收发消息过程概览
- 网络是怎样连接的(六)—— 协议栈内部探索步骤
- 网络是怎样连接的(七)—— 协议栈的内部结构
- 网络是怎样连接的(八)—— 探索套接字
- 网络是怎样连接的(九)—— 连接连的到底是啥
- 网络是怎样连接的(十)—— 连接操作的实际过程
- 网络是怎样连接的(十一)—— 协议栈发送数据特性
- 网络是怎样连接的(十二)—— 数据收发操作中重要标志位 ACK
前言
在前面的文章中,我们学习了 套接字的创建、连接的建立、数据的收发,今天我们来看下关于数据收发的最后一个操作,连接断开。
下面,让我们开始我们今天的学习之旅!
数据发送完毕后断开连接
毫无疑问,收发数据结束的时间点应该是应用程序判断所有数据都已经发送完毕的时候。这时,数据发送完毕的一方会发起断开过程。
但不同的应用程序会选择不同的断开时机。 以 Web 为例,在 HTTP1.0 中,浏览器向 Web 服务器发送请求消息,Web 服务器再返回响应消息,这时收发数据的过程就全部结束了,服务器一方会发起断开过程。而在 HTTP1.1 中,服务器返回响应消息之后,客户端还可以继续发送下一个请求消息,如果接下来没有请求要发送了,客户端一方会发起断开过程。
协议栈在设计上允许任何一方先发起断开过程。
无论哪种情况,完成数据发送的一方会发起断开过程,这里我们以服务器一方发起断开过程为例来进行讲解。首先,服务器一方的应用程序会调用 Socket 库的 close 程序。然后,服务器的协议栈会生成包含断开信息的 TCP 头部,具体来说就是将控制位中的 FIN 标志位设置为 1。接下来,协议栈会委托 IP 模块向客户端发送数据,同时,服务器的套接字也会记录下断开操作的相关信息。
接下来轮到客户端了。当收到服务器发来的 FIN 为 1 的 TCP 头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态。然后,为了告知服务器已收到 FIN 为 1 的包,客户端会向服务器返回一个 ACK 号,如下图所示。这些操作完成后,协议栈就可以等待应用程序来取数据了(取在接收缓冲区中的数据)。
过了一会儿,应用程序就会调用 read 来读取数据(应用程序有可能在收到 FIN 为 1 的包之后就来读取数据,这时读取数据的操作会被挂起,等待 FIN 包到达在继续执行。原因是:服务端发送 FIN 包时也有可能会携带数据)。这时,协议栈不会向应用程序传递数据(如果接收缓冲区中还有剩余的已接收数据,则这些数据会被传递给应用程序),而是会告知应用程序(浏览器)来自服务器的数据已经全部收到了。根据规则,服务器返回请求之后,Web 通信操作就全部结束了,因此只要收到服务器返回的所有数据,客户端的操作也就随之结束了。
接着客户端在处理完全部数据之后,客户端应用程序会调用 close 来结束数据收发操作,这是客户端的协议栈也会和服务器一样,生成一个 FIN 为 1 的 TCP 包,然后委托 IP 模块发送给服务器。一段时间之后,服务器就会返回 ACK 号。到这里,客户端和服务器的通信就全部结束了。

删除套接字
和服务器的通信结束之后,用来通信的套接字也就不会再使用了,这时我们就可以删除这个套接字了。不过,套接字并不会立即被删除,而是会等待一段时间再被删除,如上图中的等待 2MSL。
等待这段时间是为了防止误操作,引发误操作的原因有很多,我们来看一个例子:
客户端断开连接,断开的操作顺序如下:
- 客户端发送
FIN - 服务器返回
ACK号 - 服务器发送
FIN - 客户端返回
ACK号
如果最后客户端返回的 ACK 号丢失了,结果会如何呢?这时,服务器没有接收到 ACK 号,可能会重发一次 FIN。如果这时客户端的套接字已经删除了,会发生什么呢?
套接字被删除,那么套接字中保存的控制信息也就跟着消失了,套接字对应的端口号就会被释放出来。这时,如果别的应用程序要创建套接字,新套接字碰巧又被分配了同一个端口号,而服务器重发的 FIN 正好到达,会怎么样呢?本来这个 FIN是要发给刚刚删除的那个套接字的,但新套接字具有同样的端口号,于是这个 FIN 就会错误地跑到新套接字里面,新套接字就开始执行断开操作了。之所以不马上删除套接字,就是为了防止这样的误操作。
至于具体等待多长时间,这和包重传的操作方式有关。网络包丢失之后会进行重传,这个操作通常要持续几分钟。如果重传了几分钟之后依然无效,则停止重传。
在这段时间内,网络中可能存在重传的包,也就有可能发生前面讲到的这种误操作,因此需要等待到重传完全结束。协议栈对于这个等待时间没有明确的规定,一般来说会等待几分钟之后再删除套接字。
总结一下套接字被延迟删除的原因就是:
- 保证网络包能够被正确接收到;
- 防止新的套接字被失效的连接请求;
数据收发操作小结
到这里,用 TCP 协议收发应用程序数据的操作就全部结束了。因为数据收发操作涉及流程较长,我们再来完整的回顾下这部分流程。
- 创建套接字:数据收发操作的第一步是创建套接字。一般来说,服务器一方的应用程序在启动时就会创建好套接字并进入等待连接的状态。客户端则一般是在用户触发特定动作,需要访问服务器的时候创建套接字。在这个阶段,还没有开始传输网络包。
- 客户端发起连接操作:创建完套接字之后,客户端会向服务器发起连接操作。首先,客户端会生成一个
SYN为1的 TCP 包并发送给服务器。这个 TCP 包的头部还包含了客户端向服务端发送数据时使用的 初始序号,以及服务器向客户端发送数据时需要用到的 窗口大小。 - 服务端返回确认包:当这个包到达服务器之后,服务器会返回一个
SYN为1的 TCP 包。这个包的头部中也包含了 序号和窗口大小,此外还包含表示确认已收到包的ACK号。 - 客户端返回确认包(三次握手结束) :当这个包到达客户端时,客户端会向服务器返回一个包含表示确认的
ACK号的 TCP 包。到这里,连接操作就完成了,双方进入数据收发阶段。 - 数据收发:数据收发阶段的操作根据应用程序的不同而有一些差异。以 Web 为例,首先客户端会向服务器发送请求消息,TCP 会将请求消息切分成一定大小的块,并在每一块前面加上 TCP 头部,然后发送给服务器。TCP 头部中包含 序号,它表示当前发送的是第几个字节的数据。当服务器收到数据时,会向客户端返回
ACK号。在最初的阶段,服务器只是不断接收数据,随着数据收发的进行,数据不断传递给应用程序,接收缓冲区就会被逐步释放。这是,服务器需要将新的窗口大小告知客户端。当服务器收到客户端的请求消息后,会向客户端返回响应消息。这个过程和刚才的过程正好相反。 - 连接断开:服务器的响应消息发送完毕之后,数据收发操作就结束了,这是就会开始执行断开操作。以 Web 为例,服务器会先发起断开流程。在这个过程中,服务器先发送一个
FIN为1的 TCP 包,然后客户端返回一个表示确认收到的ACK号。接下来,双方还会交换一组方向相反的FIN为1的 TCP 包和包含ACK号的 TCP 包。最后,在等待一段时间后,套接字会被删除。
总结
-
协议栈在设计上允许任何一方先发起断开过程。
-
连接断开的过程是双方交换一组方向相反的
FIN为1的 TCP 包和包含ACK号的 TCP 包的过程; -
套接字并不会立即被删除,而是会等待一段时间再被删除,原因是:
- 保证网络包能够被正确接收到;
- 防止新的套接字被失效的连接请求;