3 W 字图文 | 网络协议的幕后故事:从开发者视角自顶向下彻底理解 TCP

B站影视 日本电影 2025-09-08 18:00 1

摘要:大部分人在听到“网络”、“连接“、“协议“、“数据传输“等概念时,脑海中往往会浮现出大学课堂上那些令人头疼的讲解和枯燥的幻灯片。但作为软件工程师,我们不能因其复杂就对网络技术敬而远之。尽管我们以后不一定要成为网络专家,但掌握一定的网络知识还是很有必要的,它能帮

大部分人在听到“网络”、“连接“、“协议“、“数据传输“等概念时,脑海中往往会浮现出大学课堂上那些令人头疼的讲解和枯燥的幻灯片。但作为软件工程师,我们不能因其复杂就对网络技术敬而远之。尽管我们以后不一定要成为网络专家,但掌握一定的网络知识还是很有必要的,它能帮助我们在进行系统架构设计时,做出更加合理的决策。

然而大多数关于网络的书籍或文章其内容繁杂、风格严肃,充满了各种细节和术语,读起来令人望而却步。

而本系列文章的出发点则完全不同:我们将站在软件工程师的角度,以自顶向下的方式,用通俗易懂的语言解释网络基础概念和 TCP 协议,同时保留足够的技术细节。阅读完后,大家将能够清晰地认识到 TCP 是如何工作的、它为什么如此设计等这些关键问题。

这篇文章是整个系列的起点,目的是为后面的内容奠定基础。我们会先从整体视角入手,理解网络中的数据流模型,以及数据在协议栈中的流动方式。接着,会逐步深入到 TCP 的细节,比如连接的生命周期、报文头的结构以及各个字段的含义。有了这些扎实的基础知识,那么在阅读后续更高级的内容时会更加轻松。

在深入探讨 TCP 的细节之前,我们先从整体上把握网络中“数据是如何流动的”。

还记得 OSI(开放系统互联)模型和 TCP/IP 参考模型吗?在正式展开讨论之前,我们需要简要回顾一下这些必要的概念。因为理解数据如何在不同层之间流转,是深入掌握网络协议(尤其是 TCP)运作机制的前提。

OSI 模型是一个概念性的网络通信模型,它与具体协议无关,重点在于划分每一层的职责。它有助于我们理解数据在网络中是如何流动的,并明确网络应用与设备之间是如何协同通信的。

OSI 模型共分为 7 层,从应用程序一直到底层的物理传输,每一层都承担特定的功能。彼此配合,完成一次完整的数据通信过程。接下来,我们从最靠近用户的应用层开始,自上而下简要介绍每一层的作用。

这一层是应用程序(如 Web 浏览器和电子邮件客户端)实际运行的地方,也是唯一直接接触用户数据的层。当我们发布一条微博或者完成一次移动支付时,表面上只是点击了几下屏幕或输入了一些信息,背后却是应用层在发挥作用。它负责将用户的操作转化为网络能够理解的请求,并交给下层进行传输,而用户不需要关心这些信息是如何被发送和处理的,这就是应用层的作用。

不过,应用层生成的数据还不能直接在网络中传输。无论用户上传的是文字、视频还是图片,都需要有一层将这些数据转换为网络和操作系统能够理解的格式,这就是表示层的任务。

表示层的主要职责是 数据格式转换。它会把各种数据(字符串、图片、符号等)编码成比特流,并在需要时进行压缩,以便更高效地传输。同时,为了保障数据安全,表示层还会负责加密处理。可以说,所有与数据编码、格式转换、压缩和加密相关的工作,都是在这一层完成的。

经过表示层理后,数据已经可以被网络理解和传输。但仅仅有正确的数据格式还不够,要让数据在不同设备之间可靠地交换,还需要一个机制来维持通信关系。

这时,就轮到会话层登场了。

想象一下,如果你已经登录微博,却突然被强制退出,或者应用与服务器的连接时不时中断,这显然会带来糟糕的体验。

在如今大量应用需要持续通信的背景下,客户端与服务器之间必须维持一个稳定的会话状态。

会话层的作用是在设备之间建立、管理和终止“会话”关系,

它会在一定时间范围内将客户端与特定服务器“绑定”,从而确保通信持续、顺畅。此外,会话层还负责对话控制、令牌管理以及发送方与接收方之间的通信同步。

建立会话只是开始,真正的数据传输才是重点。

当会话层确保客户端和服务器之间的“对话关系”建立好之后,接下来就需要有人来负责具体的数据传输了。

想象一下,当我们上传一段视频时,视频文件并不会“嗖”地一声瞬间传到服务器那端。整个传输过程会受到网络带宽、时延、丢包、抖动等因素的影响(这些问题我们稍后会详细探讨)。

因此,传输层的主要作用,就是在应用与网络之间建立一条可靠的通道。它决定我们的设备在特定时刻可以发送多少数据、又能接收多少数据;必要时,还会调整发送速率,以保证通信效率;同时,它还负责确保数据在传输过程中不丢失、不乱序,即便中途发生丢包,也能通过重传和重组机制,恢复出完整的数据。

互联网并不是一个统一的巨大网络,而是由许多较小的网络(即子网)互相连接而成的,当数据从一台网络设备发出时,它并不是直接飞向目标服务器,而是要经过一个又一个子网和路由器的转发,最终才能到达目标。

拿家庭网络举个例子:你家中所有设备都连在同一个 Wi-Fi 路由器上,这实际上就是一个局域网。

这些设备通过一个统一的公网 IP 地址与互联网通信。当外部世界想向你家某台设备发送数据时,数据首先抵达这个公网 IP,然后由家庭网络将其转发到正确的内部设备。这个过程依赖于一个叫做 网络地址转换(NAT) 的机制。

网络层不仅负责 NAT,还负责路由选择、地址解析等关键任务,决定了数据该如何走、走哪条路最优、如何找到目标地址,让信息从世界的一端正确无误地送达另一端。

网络层为数据选好了路径,决定它该走哪条路才能到达目标设备。但在实际传输中,数据并不是一下子就“跑完全程”的,而是要在一跳一跳的节点之间逐步传递。那么,在这些具体的跳点之间,数据是如何被传送的呢?

这就需要数据链路层来发挥作用了。

在一个局部网络内部,设备之间要实现通信,必须通过某种物理媒介连接起来。这些设备通常连接到一个叫做“交换机(Switch)”的硬件上,交换机会识别并记录所有连接设备的 MAC 地址(即物理地址),并据此进行数据的转发。交换机本身又通常连接到路由器,从而实现整个网络的上下游数据流动。

因此,一段数据从设备传输到交换机,再到路由器、调制解调器,最终进入 ISP 的网络。这种一跳一跳的数据传输过程就是由数据链路层负责的。

具体说来,数据链路层会将网络层传下来的数据封装成一种称为“帧(Frame)”的格式,并负责将这些帧可靠地从一个节点传输到下一个节点。同时,它还负责控制帧的发送速率、冲突处理、介质访问控制,确保每一跳之间的数据能够稳定、准确地传递。

数据链路层确保了每一跳之间的传输可靠。但别忘了,这一切仍然需要有真正的物理载体来承载数据的传输。

这就是物理层的职责所在。

网络中的计算机需要以某种形式实际连接在一起,比如通过标准铜缆、同轴电缆、光纤,甚至是卫星链路等方式,而这些物理连接方式所处的层级就是物理层。它是网络通信中数据真正传输发生的地方,负责将比特流以电信号、光信号等形式在网络中传递。

物理层处理的是设备与传输介质之间接口的各种特性描述,包括比特的表示与同步、数据传输速率、物理拓扑结构、线路配置以及传输模式等内容。

为了更直观地理解这些抽象概念,下面这张图展示了 OSI 各层的主要功能和它们所对应的典型设备、具体协议:

接下来是一张非常简化的示意图,用于帮助理解设备、交换机、路由器和调制解调器之间的通信与网络连接方式,以及它们在网络中分别扮演的角色。

这里举个例子,假设在一个办公室里,有两个网络:左侧是供员工使用的普通办公网络,右侧是专用于服务器集群的后台网络。所有的个人设备通过一个交换机连接在一起,服务器则接入另一个交换机。为了实现网络隔离,这两个交换机分别连接到各自独立的路由器。

再往外,这两个路由器又连接到不同的调制解调器(Modem),这是因为这两个网络可能采用了不同类型的物理连接方式。例如,员工网络可能通过标准铜缆接入 ISP,而服务器网络则通过高速光纤线路连接到 ISP。由于物理传输介质的不同,通常需要不同类型的调制解调器来进行适配和优化,因此每种物理连接方式往往对应一个专用的 Modem。

通过上面的例子,我们可以更清晰地理解 OSI 模型各层在现实网络中是如何协同工作的。不过,OSI 模型只是一个理论模型 ,在实际的网络协议栈实现中,更常采用的是下面要介绍的 TCP/IP 模型。

TCP/IP 网络模型将 OSI 模型中的多个层进行了合并。虽然这两个模型在概念上有一定的相似性,但也存在一些关键的差异。

首先,OSI 模型是一个与具体协议无关的理论模型,主要用于帮助理解网络通信的分层结构;而 TCP/IP 模型本身就是一组可以实际运行的协议集合,每一层都有明确的协议支撑其功能。其次,OSI 模型中的传输层是面向连接的(在开始传输数据之前需要先建立连接),并且保证数据传输的可靠性;而在 TCP/IP 模型中,传输层则不仅支持面向连接的方式(如 TCP),也支持无连接的通信方式(如 UDP),可以根据应用场景灵活选择。

在本系列文章中,我们将重点关注 TCP 的内部工作机制。那么,TCP 在前面提到的网络模型中属于哪一层呢?

TCP 是传输层中的一种协议,当然它并不是这一层中唯一的协议,传输层还包括像 UDP 这样的其他协议。

互联网上对 TCP 的正式定义很多,大多数都将其描述为一种面向连接、高度可靠的通信协议,用于在发送方与接收方之间维护一个有序、持续的数据传输流。为了实现这种可靠性,TCP 背后其实有很多复杂的机制,我们将在接下来的内容中一一展开讲解。

当我们发起一个请求时,比如打开一个网页或访问某个 API,数据的传输过程都会经过多个层级。它首先从应用程序进入发送方的网络协议栈,然后穿越由多个中间设备组成的网络路径,最终通过接收方的网络协议栈抵达目标设备。

下面这张图展示了 TCP/IP 协议栈中数据流动的简化示意图。

在这个模型中,每一层都有明确的职责:它既可能接收来自上层的数据并进行封装,也可能把数据交还给上一层进行解析,取决于系统当前是作为发送方还是接收方。

在数据传输过程中,每一层对其处理的数据单元都有自己的专属术语:

在传输层,TCP 协议将数据单元称为段(segment),而 UDP 协议称之为 数据报(datagram);在网络层,数据单元通常也被称为 数据报(datagram),有时也称为包(packet);在数据链路层和物理层(或称为网络接口层),则称为帧(frame)。

关于这些术语的使用,实际上存在不少混淆。许多资料在提到 TCP 段、IP 数据报或应用层数据时,常常随意地统称为“包(packet)”,甚至在某些 TCP 的 RFC 文档中也未严格区分这些术语。

但归根结底,无论你称它为包、帧还是段,它们本质上都是应用层生成的数据,经过每一层的处理后被逐层封装,每一层都附加了自己的控制信息和元数据,从而形成特定格式的数据单元。

下面这张示意图展示了以 TCP/IP 模型 为参考,从应用层到底层网络接口层,数据如何一层层被封装并向下传递的过程。

数据从应用层开始流动(对应 OSI 模型的第 5、6、7 层)。在这一层中,会进行各种数据转换操作,比如:

字符编码的转换(如从 ASCII 转换为 EBCDIC)数据加密或压缩与域名系统(DNS)通信,获取目标主机的 IP 地址。

然后,应用层将解析出的 IP 地址与目标端口号组合,形成一个套接字地址(socket address),并将其写入报文头中。随后,应用层会在报文头之后附加应用数据的字节,构成一个完整的“应用层数据单元”。

需要注意的是,应用层并不会一次性把所有数据都交给传输层,因为那样效率不高。相反,它通常以 字节流(stream of bytes) 的形式,分批次、逐步把数据推送到传输层,由传输层负责进一步的切分与处理。

从应用层传来的字节流,会交由传输层协议,在 TCP/IP 协议栈中,这一层常见的协议有 TCP 和 UDP。在这里,我们重点关注 TCP。

TCP 的第一项工作,就是把连续的字节流切分成更小的单元,这个过程称为 分段(Segmentation)。每个 TCP 段(segment) 都由两部分构成:

TCP 头部(header):包含源端口、目标端口、序列号、确认号等控制信息;应用层数据:也就是实际要传输的内容。

此外,在数据真正开始传输前,TCP 还需要和接收方先“握个手”。也就是说,发送端与接收端会建立一个逻辑上的连接,用来保证双方能可靠通信。这一过程就是我们熟知的三次握手。

这个连接的建立、维护和断开过程,以及段头中的各类字段,本文后续将会详细讲解。

传输层生成的 TCP 段会被交给网络层处理,在 TCP/IP 协议栈中,这一层的核心协议就是 IP 协议。

IP 协议的主要任务,是把来自传输层的数据封装成 IP 数据报(IP datagram)。具体来说,它会在 TCP 段前面加上一个 IP 头部,其中包含源 IP 地址、目标 IP 地址、数据报长度等信息。这样,数据就具备了在网络中“寻找路径”的能力。

在实际传输中,如果一个 TCP 段过大,超过了底层网络能够承载的最大传输单元(mtu),IP 协议还会对其进行 分片(fragmentation)。每个片段都会带有相同的标识符,以便接收方在收到所有片段后,能够重新组装成完整的数据。

需要特别说明的是,IP 协议本身并不提供可靠性保证:它不能确保数据一定会送达,也不保证顺序。。但它的价值在于屏蔽了底层网络的差异性,让上层协议能够在各种物理网络(如以太网、Wi-Fi、光纤链路等)之上统一运行,而不必关心具体的硬件实现细节。

关于 IP 协议的深入技术讨论并不在本文的讨论范围之内。

当 IP 数据报准备好后,它会被交给数据链路层,在这一层,最核心的任务是让数据能够在 同一局域网 内的设备之间顺利传输。

为此,数据链路层需要知道目标设备的 MAC 地址(物理地址)。如果本地还不知道这个地址,就会借助 ARP 协议 来完成解析:

发送方会先广播一个 ARP 请求;目标设备收到后,再返回包含自己 MAC 地址的 ARP 响应。

获得目标 MAC 地址后,数据链路层便会为 IP 数据报添加一个帧头部,构成完整的以太网帧。这个帧头中除了源 MAC 和目标 MAC 地址外,还包含一个用于差错检测的字段 —— CRC(循环冗余校验),它可以检测数据在传输过程中是否发生了错误。

完成帧的封装后,数据链路层将其交给物理层进行最终的传输。

物理层的工作,就是把这些二进制比特真正“变成信号”,并通过物理介质发出去。这包括:

把比特流编码成特定格式(如曼彻斯特编码);将编码转换为物理信号(电信号、光脉冲、无线电波等);最终通过具体的介质(铜缆、光纤、Wi-Fi 空气波段等)发送到目标设备。

至此,一个应用层的数据已经完成了从上到下的逐层封装,并走向传输介质:

应用层 → 生成原始数据;传输层(如 TCP)→ 划分并封装为段;网络层(IP)→ 添加 IP 头,生成数据报;数据链路层(以太网)→ 封装为帧;物理层 → 将帧转换为物理信号,真正发送出去。

这是一场 逐层配合的“接力赛”:每一层都只关注自己的任务,把数据加工好后交给下一层。

需要注意的是,在这个过程中,每一层对数据单元大小都有自己的限制。

MTU 指的是数据链路层一次能够承载的最大 IP 数据报大小。

如果一个 IP 数据报超过了 MTU,就必须进行 IP 分片(Fragmentation),而分片操作本身存在额外的开销,因此,在实际应用中,通常会尽量控制数据大小,使其不超过 MTU。

需要注意的是,MTU 是链路层对 整个 IP 数据报(IP 头 + TCP 头 + TCP 负载)的限制,并不包括链路层自身的帧头和帧尾(如以太网帧的 14 字节头和 CRC 校验字段)。

MTU = IP 头部(最小为 20 字节) + TCP 头部(最小为 20 字节) + TCP 负载(即 MSS)

MTU 的大小取决于底层链路层的类型,不同的链路支持的最大大小不同。以下是一些常见的例子:

相较于 MTU,MSS(Maximum Segment Size) 是一个更高层的概念,指的是 TCP 段中“纯数据部分”的最大大小,不包括 TCP 头部本身。

MSS 的计算通常是基于 MTU 推导出来的:

// 在没有使用 TCP/IP 选项的情况下)MSS = MTU - IP头部长度 - TCP头部长度= MTU - 20 - 20 = MTU - 40

因此,当 MTU 为 1500 字节(以太网常见默认值)时,MSS 就是 1460 字节。

需要注意的是,,MSS 并不是固定不变的,而是在 TCP 连接建立的“三次握手”阶段,由双方协商确定:双方在发送 SYN 报文时,会声明自己所期望的 MSS 值,最终采用较小者作为连接中的实际 MSS。

通过这种方式,双方可以根据各自底层链路的 MTU 限制,决定每次 TCP 报文段传输的数据最大值,从而尽量避免 IP 分片。

你可能还会在其它文档中会看到 “IP MTU” 这个术语,它实质上就是指针对 IP 层数据报的最大长度限制,通常与底层链路的 MTU 保持一致。

IP MTU = IP 头部 + TCP 头部 + TCP 负载(即 MSS)

如果我们通过命令手动修改了接口的 MTU(例如 mtu 命令),那么 IP MTU 也会自动同步更新;但是,反过来并不成立:修改 IP MTU 值并不会影响通过 mtu 命令配置的 MTU 值。

下图展示了 MTU、IP MTU 和 MSS 三者之间的关系。在这个示例中,MSS 设置为 1460 字节,仅供参考。你可以从图中直观看到,各层头部在计算中是如何被包含或排除的。

而 TCP 则是在 IP 之上的传输层协议,它通过一系列机制为数据通信提供可靠性保障:包括数据重传、顺序保证、流量控制、拥塞控制,以及完整性校验等。因此,我们常说 TCP 是面向连接的可靠传输协议,相比之下,它更关注数据传输的“准确性”而非“实时性”。

虽然 HTTP 协议的规范中并未强制要求必须使用 TCP 作为传输协议,理论上也可以使用 UDP 或其他传输协议,但由于 TCP 的可靠性和强大的特性,实际中几乎所有 HTTP 流量都是通过 TCP 传输的。

TCP 是一种面向连接的协议,也就是说,在进行数据传输之前,通信双方必须先建立一条连接,明确各自的起始状态,并协商通信参数。这种连接的建立,为后续的可靠传输打下基础。

在开始数据传输之前,TCP 需要通过“三次握手”来建立一条可靠的连接。

所谓“三次握手”,是指客户端和服务器之间需要往返交换 三次报文,其目的是确认彼此的通信能力、初始序列号,以及缓冲区大小等参数。

具体而言,三次握手过程如下:

第一次握手:客户端请求建立连接,向服务端发送一个同步报文(SYN=1),同时选择一个随机数 seq = x 作为初始序列号第二次握手:服务端收到连接请求报文后,如果同意建立连接,则向客户端发送同步确认报文(SYN=1,ACK=1),确认号为 ack = x + 1,同时选择一个随机数 seq = y 作为自己的初始序列号第三次握手:客户端收到服务端的确认后,向服务端发送一个确认报文(ACK=1),确认号为 ack = y + 1,序列号为 seq = x + 1,表示自己已经准备好通信。

客户端在发送完第三个 ACK 报文后可以立即开始发送数据包,而服务器则必须接收到第三次握手的 ACK 报文后(客户端发给它的 ACK 报文)后才能进行数据传输。

可能有人会问,TCP 建立连接为什么一定要三次握手?能不能只用两次?或者做得更保险些,用四次?

先看看为什么不能是两次?

问题在于,如果客户端由于网络原因没有收到第二个报文(SYN-ACK),那么它认为连接尚未建立,但服务器却已经认为连接建立了。这会出现“半开连接(Half-Open Connection)”,从而造成服务器资源浪费甚至被恶意利用。

那为什么不需要四次?其实没有必要。三次握手已经让双方确认了发送和接收能力,多一次并不会增加额外价值,只会平白增加延迟:

第一次握手:客户端确认“我能发”,服务器能收;第二次握手:服务器确认“我能收,我也能发”,客户端能收;第三次握手:客户端确认“我能收你的回应”,服务器也能收我的确认。

TCP 三次握手会引入延时,在现代网页中,一个页面可能包含上百个资源请求(如图片、CSS、JS 等),这意味着可能要建立大量 TCP 连接,三次握手的累积延迟将严重影响加载速度。

为此,TCP 引入了一项优化机制:TCP Fast Open(TFO)。

TFO 是一种减少建立 TCP 连接延迟的方法。

在传统三次握手中,客户端必须等到连接完全建立后才能发送数据。

而 TFO 允许客户端在第一次握手的 SYN 报文中就附带部分请求数据。服务器在验证通过后直接处理请求,而无需等待三次握手完成。

首次连接请求:客户端发送 SYN 报文,报文中包含 Fast Open 选项,且该选项的 Cookie 为空,表示客户端请求 Fast Open Cookie。服务器如果支持 Fast Open 功能,会生成一个 Cookie,并将 Cookie 放置在 SYN+ACK 报文中的 Fast Open 选项中返回给客户端。客户端收到 SYN+ACK 报文以后,本地缓存 Fast Open 选项中的 Cookie。后续连接建立:客户端再次向服务器建立连接时,发送的 SYN 报文会包含数据以及本地缓存的 Fast Open Cookie。服务器在收到 Cookie 后会对其进行校验,如果 Cookie 有效,服务器将在 SYN+ACK 报文中对 SYN 和数据进行确认,并且随后会将数据返回给客户端。如果 Cookie 无效,服务器会丢弃 SYN 报文中的数据,随后的确认报文只会确认 SYN 对应的序列号。客户端会发送 ACK 确认服务器发回的 SYN 以及数据,如果第一次握手时数据没有被确认,客户端会重新发送数据。

研究表明,引入TFO 后:

可将 HTTP 请求的延迟降低约 15%;整体网页加载时间平均缩短 10%,在高延迟网络中甚至提升 40%;

虽然 TFO 可以减少三次握手带来的往返延迟,但它的适用场景也有限制:

SYN 报文中可携带的数据量有大小限制;仅支持特定类型的 HTTP 请求;由于需要使用加密 Cookie 机制,它只适用于重复连接(即客户端曾与服务器建立过连接)。

要全面理解 TCP 的行为,仅了解建立连接的过程是不够的。一个 TCP 连接从发起到断开,中间会经历多个不同的状态,每一个状态都对应着特定的控制逻辑和数据流动方式。下面我们就来看看TCP 状态转换。

CLOSED(关闭):默认状态。在连接尚未建立之前,TCP 处于 CLOSED 状态;连接终止后,也会回到这一状态。LISTEN(监听):服务端启动监听端口后,会进入 LISTEN 状态,等待客户端的连接请求。SYN_SENT(同步已发送):当客户端主动发起连接时,会向服务器发送一个 SYN 报文,并进入 SYN_SENT 状态。SYN_RECD(同步已接收):服务器收到客户端的 SYN 报文后,返回一个包含 SYN 和 ACK 标志位的报文(即 SYN+ACK),并进入 SYN_RECV 状态。客户端收到该 SYN+ACK 报文后,再发送一个 ACK 报文作为确认。此时,客户端和服务器分别进入 ESTABLISHED(已建立) 状态,连接正式建立,双方可以开始进行数据传输。

换句话说,TCP 状态的变化就是“三次握手”的具象化过程:

至此,连接正式建立,双方已准备好开始数据传输。

当数据传输完成后,任意一方都可以主动发起关闭连接的请求。CP 使用 四次挥手 的方式来终止连接。假设由接收方主动发起关闭,我们称其为 发起方(initiator),而另一方称为 响应方(responder)。

四次挥手的过程如下:

FIN_WAIT_1 & CLOSE_WAIT发起方会向响应方发送一个 FIN 报文,表示“我没有数据要发了,但仍能接收数据”。此时发起方的状态从 ESTABLISHED 进入 FIN_WAIT_1。响应方收到后,会立刻回复一个 ACK 报文,并进入 CLOSE_WAIT 状态。这意味着“我知道你要关闭了,但我可能还有数据没发完”。FIN_WAIT_2:发起方收到 ACK 确认后,进入 FIN_WAIT_2 状态,安静等待对方的数据传输完成,不再进行任何操作。LAST_ACK:当响应方的数据都发送完毕,它会再发一个 FIN 报文,表示“我也没有数据要发了”。此时响应方进入 LAST_ACK 状态,等待最终确认。TIME_WAIT & CLOSED:当发起方收到来自响应方的 FIN 报文后,会回复一个 ACK 报文,并进入 TIME_WAIT 状态。这个状态会持续两个最大报文段生命周期(2MSL,通常约两分钟),然后进入 CLOSED 状态。响应方收到这个 ACK 报文后,也会进入到 CLOSED 状态。

在前面我们提到的“四次挥手”场景中,通常是 一方主动关闭,另一方被动响应。但是TCP 也允许双方“同时发起”连接终止过程。下图展示了这一过程中各个状态的转换:

如图所示,这种情况下的关闭过程是完全对称的:

需要注意的是在同时关闭的情况下,设备在处于 FIN_WAIT_1 状态时就接收到了对方发送的 FIN 报文,从而导致后续进入 CLOSING 状态,而不是先进入 FIN_WAIT_2。

下图更完整的展示了一个 TCP 生命过程中各个状态的转换过程以及对应的操作,图中几乎所有内容在前文中都已详细介绍过了:

到目前为止,我们已经了解了数据在网络中如何通过 TCP 协议进行传输,TCP 连接是如何建立的,以及操作系统如何管理连接状态。

但我们还没有看到在网络中实际传输的内容究竟是什么。既然本篇文章讲的是 TCP,那我们接下来就来看一下 一个 TCP 段(segment)到底长什么样。

每个 TCP 段由 首部(Header) 和 数据部分(Payload) 组成。其中,首部携带了控制信息,是保证 TCP 能实现可靠传输的关键部分。

下图展示了 TCP 首部的结构。

表示当前发送的数据在整个字节流中的位置。每次发送数据,序列号会累加已发送数据的字节数。需要注意的是在建立连接和断开连接时发送的SYN包和FIN包虽然并不携带数据,但也会占用一个序列号。此外,序列号的初始值并不是固定的 0 或 1,而是在连接建立时由系统随机生成。

确认应答号,是指下一次应该收到的数据的序列号。换句话说,它确认了之前所有字节都已正确接收。发送端一旦收到这个确认号,就可以认为在该序号之前的数据都已经成功送达。

该字段用于指明 TCP 首部的长度,也就是数据部分从哪里开始。该字段占 4 位,单位为4字节(即32位)。在没有选项字段时,TCP 首部长度固定为 20 字节,此时该字段的值为 5。如果该值大于 5,则说明首部中包含了额外的可选字段。

标志位,总共占用 8 bit,从左至右分别为CWR、ECE、URG、ACK、PSH、RST、SYN、FIN。这些标志位也叫做控制位,分别表示以下含义:

CWR & ECE:与显式拥塞通知(ECN)机制相关,用于拥塞控制。URG:紧急标志,该位为1时,用于通知接收方包中有需要紧急处理的数据,需要在处理其他普通数据段之前先处理这些数据。ACK:“确认”标志,用于确认成功接收到一个数据段。TCP规定除了最初建立连接时的SYN包之外该位必须设置为1。PSH:“推送(Push)”标志,该位为1时,表示需要将收到的数据立刻传给上层应用协议。PSH 为0时,则不需要立即传而是先进行缓存。RST:“复位(Reset)”标志,该位为1时表示TCP连接中出现异常必须强制断开连接。例如SYN:用于建立连接。SYN为1表示希望建立连接。FIN:该位为1时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN位置为1的TCP段。每个主机又对对方的FIN包进行确认应答以后就可以断开连接。不过,主机收到FIN设置为1的TCP段以后不必马上回复一个FIN包,而是可以等到缓冲区中的所有数据都因已成功发送而被自动删除之后再发。

窗口大小字段就是用于 流量控制 的,它表示接收方还能接收的最大数据量(以字节为单位)。发送方根据这个数值调整发送速率,从而避免网络拥塞或接收端缓存溢出。

Checksum

这是一个 16 位的字段,用于对 TCP 头部、负载数据以及伪首部进行差错检测。所谓伪首部,包括源 IP 地址、目的 IP 地址、协议号(指定为 TCP 协议)以及 TCP 头部和负载数据的总长度(以字节为单位)。通过校验和机制,接收端可以判断报文在传输过程中是否出现了差错。

除了保证数据正确性,TCP 还提供了一些机制来支持特殊场景的通信需求。紧急指针(Urgent Pointer) 就是其中之一。它同样占用 16 位。当 URG 标志位被置 1 时,该字段的值表示相对于当前序号的偏移量,用来指示最后一个紧急数据字节的位置,从而让接收方能够优先处理这部分数据。

Options

TCP 报文头部的固定字段占用 20 字节。在它之后是可选字段。每个可选字段由“类型、长度和值”三部分组成。TCP 报文头的总长度必须是 4 的倍数,如果不足,就会用 NOP(No Operation,无操作)选项来填充。

由于 TCP 头部长度字段只有 4 位,因此它所能表示的最大值是二进制 1111,也就是十进制的 15。而该长度的单位是 4 字节,所以 TCP 头部的最大长度为 15 × 4 = 60 字节。其中固定字段已经占了 20 字节,因此最多还剩下 40 字节可以用于可选字段。

下图展示了部分 TCP 头部选项及其结构。许多选项只会出现在三次握手的初始阶段(SYN 和 SYN/ACK 报文中),而其他一些选项则可以在整个 TCP 会话过程中随时使用。其中比较常见的有:最大报文段长度(MSS)、窗口缩放以及选择性确认(SACK)等。关于 MSS 我们已经介绍过,其他选项会在本系列的下一篇文章中再进行介绍。

下面有一个需要重点理解的问题是:接收方是如何对收到的 TCP 报文段进行确认的?

我们知道,TCP 头部中包含一个 32 位的确认号字段,以及一个 1 位的 ACK 标志位。

假设发送方发送了一个序号为 1000 的报文段,接收方成功接收后,就需要对这个报文段进行确认。一般来说,确认的方式主要有两种。

第一种方式是:接收方根据自己已经收到的数据字节数来计算确认号。比如说,收到了 200 字节的数据,么它实际上已经处理到序号 1000 + 200 = 1200 的位置。在确认报文中,接收方会发送 1201 作为确认号(即最后一个字节序号加 1),并将 ACK 标志位置 1。这样,发送方在下一次传输时,就会从序号 1201 开始发送数据。

第二种方式是:接收方不关心具体收到了多少字节,而是直接在发送方的序号上加 1。以上例来说,就是把 1000 + 1 = 1001 作为确认号,并同样将 ACK 标志位置 1。当发送方收到这个确认报文后,就知道接收方已经完整收到了之前的报文段,可以从指定的序号开始继续发送后续数据。

需要注意的是,ACK 报文段并不一定要携带数据。如果其中完全没有数据,就称为“纯确认报文”(pure acknowledgement)。

来源:心平氣和

相关推荐