计算机网络之六 - 可靠的 TCP 与高效的 UDP
TCP 与 UDP 是网络传输层的两大核心协议,它们以截然不同的方式定义了数据在应用程序间的传输。TCP 如同一次通话,追求可靠与完整;UDP 则像一张明信片,主张高效与迅捷。正是这两种设计哲学的差异,决定了它们在网页浏览、文件传输、视频会议、在线游戏等不同场景下的应用。本文将深入解析二者的核心机制与关键差异。
本系列其余几篇的目录:
- 计算机网络之一 - IP 与端口
- 计算机网络之二 - URL 与 DNS
- 计算机网络之三 - DHCP 与内网穿透
- 计算机网络之四 - 虚拟网卡与 WireGuard
- 计算机网络之五 - HTTP 与 HTTPS
TCP:可靠的“电话通话”
如果说 UDP 像一张随手寄出的明信片,那么 TCP (Transmission Control Protocol, 传输控制协议) 就是一通严谨的国际长途电话。在通话开始前,你必须先拨号、等待对方接听、双方确认身份并都说“喂,听得到吗?”之后,才会开始真正的交谈。通话结束后,还要礼貌地道别,确保双方都知晓通话结束。
这个过程虽然繁琐,但它确保了整个对话的完整性和有序性,这正是 TCP 的核心设计哲学。
核心特性:面向连接与可靠
- 面向连接 (Connection-Oriented):在发送任何应用数据之前,通信双方(客户端和服务器)必须先通过一个标准化的过程建立一个虚拟的连接。所有后续的数据交换都在这个已建立的连接上进行。
- 可靠传输 (Reliable):TCP 提供了一系列复杂的机制来保证数据能够准确、有序地从发送方传输到接收方。它承诺“不丢包、不失序、无差错、无重复”。
这一切的可靠性,都始于那个著名的“三次握手”过程。但在深入流程之前,我们必须先了解构成这次“握手”的几个关键“零件”。
TCP 报文:一次通话的“信封”
TCP 通信的数据单元被称为报文段 (Segment)。你可以把它想象成一个高度结构化的信封,其“信封皮”,也就是 TCP 头部 (Header),包含了所有用于控制通信的元信息。下表是其主要字段的概览:
字段 | 英文名称 | 简称 | 大小 | 描述 |
---|---|---|---|---|
源端口 | Source Port | sport |
16 bits | 标识发送方应用程序的端口号 |
目标端口 | Destination Port | dport |
16 bits | 标识接收方应用程序的端口号 |
序列号 | Sequence Number | seq |
32 bits | 标记本报文段数据第一个字节在数据流中的位置 |
确认号 | Acknowledgment Number | ack |
32 bits | 期望收到的对方下一个报文段的序列号 |
数据偏移 | Data Offset | - | 4 bits | TCP 头部自身的长度,单位为4字节(32位) |
保留 | Reserved | - | 6 bits | 未使用的保留位,必须为 0 |
标志位 | Flags | - | 6 bits | 用于控制连接状态,如 SYN , ACK , FIN |
窗口大小 | Window Size | win |
16 bits | 用于流量控制,表示接收方还能接收多少数据 |
校验和 | Checksum | csum |
16 bits | 用于检查头部和数据的传输错误 |
紧急指针 | Urgent Pointer | - | 16 bits | 当URG 标志位为1时有效 |
选项 | Options | - | 可变 | 用于携带额外的控制信息 |
在理解后续的握手流程时,我们无需关注所有细节,只需将注意力集中在最重要的四个“角色”上:SYN
和 ACK
这两个标志位,以及 seq
和 ack
这两个核心编号。
SYN
和 ACK
是 TCP 报文头中的两个非常重要的标志位 (Flags),它们就像是通信双方用来表达意图的“信号旗”。
-
SYN (Synchronize Sequence Numbers - 同步序列号)
- 含义:这个标志位用于发起和建立连接。当一方想要与另一方建立连接时,它会发送一个
SYN
标志位置为 1 的报文。这可以理解为在说:“你好,我想和你建立通信,我们来同步一下初始的序列号吧!”
- 含义:这个标志位用于发起和建立连接。当一方想要与另一方建立连接时,它会发送一个
-
ACK (Acknowledgment - 确认)
- 含义:这个标志位用于确认收到数据。当
ACK
标志位置为 1 时,意味着报文中的“确认号”字段有效。它告诉对方:“你之前发送的数据我已经收到了。” 在连接建立之后,几乎所有的 TCP 报文都会将ACK
位置为 1。
- 含义:这个标志位用于确认收到数据。当
而 seq
和 ack
是 TCP 实现可靠传输的基石,它们共同解决了一个核心问题:在不可靠的网络上,如何保证数据不丢、不乱、不重。
-
seq (Sequence Number - 序列号)
- 作用:它的核心作用是给数据包进行编号。TCP 把要传输的数据看作一个连续的字节流,
seq
就是这个流中每一个数据包里第一个字节的编号。 - 设计原因:
- 保证顺序:网络传输中,数据包可能会因为路由不同而失序到达。接收方可以根据
seq
号对数据包进行重新排序,从而恢复出原始的、有序的数据。 - 丢包检测:接收方如果发现收到的
seq
号不连续(比如收到了 100 和 300,但没收到 200),就知道中间有数据包丢失了,可以请求发送方重传。
- 保证顺序:网络传输中,数据包可能会因为路由不同而失序到达。接收方可以根据
- 作用:它的核心作用是给数据包进行编号。TCP 把要传输的数据看作一个连续的字节流,
-
ack (Acknowledgment Number - 确认号)
- 作用:它的作用是告诉发送方我期望接收的下一个字节的序列号是多少。这个设计非常巧妙,因为它隐含地确认了在这个编号之前的所有数据都已成功收到。
- 设计原因:
- 高效确认:如果发送方发送了 100、200、300 三个包,接收方只需回复一个
ack=400
,就代表“100、200、300 我都收到了,请从 400 开始发”。这比为每个包都单独回复一次确认要高效得多。 - 建立可靠连接:它是发送方判断对方是否成功收到数据的唯一依据。
- 高效确认:如果发送方发送了 100、200、300 三个包,接收方只需回复一个
三次握手:同步序列号与交换能力
理解了上述几个核心“词汇”后,我们再来审视三次握手的过程,它的每一步都变得有据可循。其本质,是通过三次通信,完成两个核心任务:
- 交换并确认双方的初始序列号 (ISN),为后续数据的有序传输打下基础。
- 确认双方都具备可靠的发送和接收能力。
-
第一次握手 (Client -> Server):
- 内容: 客户端发送一个 TCP 报文,其中
SYN
标志位置为 1,并选择一个随机的初始序列号seq=x
。 - 目的: 客户端向服务器表明“我想要建立连接”,并告知自己的起始序列号。
- 状态: 客户端进入
SYN_SENT
状态。
- 内容: 客户端发送一个 TCP 报文,其中
-
第二次握手 (Server -> Client):
- 内容: 服务器收到客户端的
SYN
包后,回复一个报文。该报文中SYN
和ACK
标志位都置为 1。服务器也选择一个自己的随机初始序列号seq=y
,同时将确认号ack
设置为x+1
。 - 目的: 服务器通过
ACK=1
和ack=x+1
告诉客户端:“你的请求我收到了”。通过SYN=1
和seq=y
表明:“我也同意建立连接,这是我的起始序列号”。 - 状态: 服务器进入
SYN_RCVD
状态。此时,服务器已确认客户端的发送能力正常。
- 内容: 服务器收到客户端的
-
第三次握手 (Client -> Server):
- 内容: 客户端收到服务器的
SYN-ACK
包后,发送最后一个确认报文。该报文ACK
标志位置为 1,seq
设置为x+1
,并将确认号ack
设置为y+1
。 - 目的: 客户端通过
ACK=1
和ack=y+1
告诉服务器:“你的回应我已收到,现在我们可以开始通信了”。 - 状态: 此报文发送后,客户端进入
ESTABLISHED
状态。服务器收到后,也进入ESTABLISHED
状态。连接正式建立。此时,双方都确认了对方的收发能力正常。
- 内容: 客户端收到服务器的
说到这里,我有一个问题:“为什么要交换 seq
和 ack
呢?”
本质是双向确认:TCP 是一个全双工的协议,意味着通信双方都可以同时发送和接收数据。因此,每一方都必须有自己的
seq
号来标记自己发送的数据,也必须有自己的ack
号来确认收到的对方数据。
三次握手就是交换和确认彼此的初始序列号(ISN)的过程:
- 第一次握手:客户端发送
SYN
和自己的seq=x
。它在说:“我的初始序列号是 x,你收到了吗?”- 第二次握手:服务器回复
SYN
、自己的seq=y
和ack=x+1
。它在说:“我收到了你的 x,所以我确认你的下一个应该是 x+1。同时,我的初始序列号是 y,你收到了吗?”- 第三次握手:客户端回复
ACK
和ack=y+1
。它在说:“我收到了你的 y,所以我确认你的下一个应该是 y+1。”
为什么必须是三次握手,而不是两次?
最核心的原因,是为了防止早已失效的、旧的连接请求突然又送达服务器,从而引发错误。
想象一个网络有些延迟的场景:
- 客户端发送了第一个连接请求
SYN
(我们称之为请求A
),但它在网络中被卡住了,迟迟没有到达服务器。 - 客户端等了一会儿没收到回应,以为丢包了,于是又发送了一个新的连接请求
SYN
(请求B
)。 请求B
顺利到达,服务器正常回应,双方通过三次握手建立了连接,传输数据,然后正常关闭了连接。- 就在这时,那个被卡了很久的
请求A
终于抵达了服务器。
如果只有两次握手,服务器收到 请求A
后,会误以为是客户端又发起了一个新的连接请求。它会立即分配资源,建立连接,然后傻傻地等待客户端发来数据。但此时的客户端对此一无所知,它根本不会理会服务器的确认,更不会发送任何数据。
结果就是,服务器单方面开启了一个“空连接”,白白浪费了系统资源,直到超时后才关闭。而三次握手,通过增加第三次客户端的最终确认,完美地解决了这个问题。服务器只有在收到客户端对自己的 SYN
的最终 ACK
之后,才会确信这是一个有效的、全新的连接请求。
这个过程可以用下面的时序图来表示:
TCP 可靠性的基石
TCP 的可靠性并非单一功能,而是一个由多种机制协同工作的复杂系统。这些机制相互配合,共同确保了数据传输的完整性、有序性、无差错和高效性。
-
序列号 (Sequence Numbers) 与确认应答 (Acknowledgements, ACK):TCP 将发送的数据分割成一个个小的数据段(Segment),并为每个字节都分配一个唯一的序列号。接收方收到数据后,会发送一个
ACK
报文作为回应,其中包含一个确认号,告诉发送方“我已经收到了你到哪个序列号为止的所有数据,请从下一个序列号开始发”。这种“有问有答”的机制是保证数据不丢失的基础。 -
超时重传 (Timeout Retransmission):如果在发送数据后的一段时间内(这个时间是动态计算的)没有收到对方的
ACK
,发送方就会认为数据包可能在路上丢失了,于是会重新发送这个数据包。 -
流量控制 (Flow Control):接收方会通过 TCP 头部中的“窗口大小 (Window Size)”字段,告诉发送方自己当前还能接收多少数据。发送方则根据这个窗口大小来调整自己的发送速率,确保不会因为发送过快而导致接收方处理不过来,造成数据溢出。
-
拥塞控制 (Congestion Control):流量控制关心的是“点对点”的速率匹配,而拥塞控制则着眼于整个网络的健康状况。TCP 通过一系列算法(如慢启动、拥塞避免等)来探测网络的拥堵程度,并主动调整发送速率,避免因自身流量过大而加剧网络拥堵,最终导致大规模丢包。
四次挥手:礼貌地“挂断电话”
与建立连接同样重要的是,如何安全、完整地断开连接。这个过程被称为“四次挥手”,因为它需要四次信息交换来确保双方的数据都已传输完毕。
-
第一次挥手 (FIN):客户端决定关闭连接,向服务器发送一个
FIN
报文,表示“我的数据已经全部发送完毕了”。此时客户端进入FIN_WAIT_1
状态。 -
第二次挥手 (ACK):服务器收到
FIN
报文后,回复一个ACK
报文,表示“收到了你的关闭请求”。但此时服务器可能还有未发送完的数据,所以它还不能立即关闭连接。此时,服务器进入CLOSE_WAIT
状态,客户端收到ACK
后进入FIN_WAIT_2
状态。 -
第三次挥手 (FIN):服务器将所有剩余数据发送完毕后,会向客户端发送一个
FIN
报文,表示“我这边的数据也发完了,可以关闭了”。服务器随之进入LAST_ACK
状态。 -
第四次挥手 (ACK):客户端收到服务器的
FIN
报文后,回复最后一个ACK
报文进行确认。发送完毕后,客户端会进入TIME_WAIT
状态,等待一段时间(通常是 2MSL,两倍的最大报文段生存时间)以确保服务器收到了这个ACK
,防止网络中可能存在的延迟报文造成问题。服务器收到ACK
后则直接进入CLOSED
状态。至此,连接被完全断开。
一定是挥四次手吗 👋?
我们可以分别从逻辑上以及行为上来看待“四次挥手”这件事:
- 从逻辑上看:TCP 协议一定会按照逻辑进行完整的四个过程,所以从这个角度上来看,一定是“四次挥手”。四个过程分别是:
- 客户端关闭发送通道。
- 服务器确认客户端关闭发送通道。
- 服务器关闭发送通道。
- 客户端确认服务器关闭发送通道。
- 从行为上看:但是如果抓包的话,你可能会发现只有三个报文段的情况,并且这种情况还不少见。这是因为当第二次挥手时,如果服务器没有剩余要发送给客户端的数据,那么 TCP 就会将第二、三次挥手进行合并,所以最终只有三个报文段。相关逻辑如下图所示:
TCP 状态机:连接的生命周期
三次握手和四次挥手描述了 TCP 连接建立和断开的关键时刻。但一个完整的 TCP 连接生命周期,远不止这几个瞬间。它由一系列精确定义的状态组成,这些状态之间的转换共同构成了一个“状态机 (State Machine)”。这个模型清晰地展示了从连接的萌芽到最终消亡的全过程。
下面的流程图描绘了 TCP 中所有状态以及它们之间可能的转换:
这张图看起来复杂,但它其实是将我们之前讨论的握手和挥手过程,以及一些中间状态,串联成了一幅完整的地图。我们可以将这些状态归为几类来理解:
-
连接建立:
LISTEN
: 仅存在于服务端。当服务器应用程序调用listen()
函数后,进入此状态,表示已准备好接收来自客户端的连接请求。一旦收到客户端的SYN
报文,将发送SYN+ACK
并进入SYN_RCVD
状态。SYN_SENT
: 客户端在调用connect()
函数后,发送SYN
报文请求建立连接,随即进入此状态。在此状态下,客户端等待接收服务器的SYN+ACK
报文。如果收到,则发送ACK
并进入ESTABLISHED
状态;如果超时未收到,则会重传SYN
报文。SYN_RCVD
: 服务端在LISTEN
状态下收到客户端的SYN
报文后,会发送SYN+ACK
报文并进入此状态。在此状态下,服务端等待接收客户端的最终ACK
报文。一旦收到ACK
,连接即建立,进入ESTABLISHED
状态。
-
数据传输:
ESTABLISHED
: 连接已成功建立,双方可以自由地进行双向数据传输。这是 TCP 连接最主要、最活跃的状态,在三次握手完成后进入此状态。
-
连接断开:
FIN_WAIT_1
: 主动关闭方(即发起关闭连接的一方,可能是客户端也可能是服务端)发送FIN
报文后进入此状态。在此状态下,主动关闭方等待接收对方对FIN
报文的ACK
。一旦收到ACK
,则进入FIN_WAIT_2
状态。CLOSE_WAIT
: 被动关闭方(即收到对方FIN
报文的一方)在收到FIN
报文后进入此状态。此时,TCP 层已经接收到对方关闭发送通道的请求,并向应用层报告连接已中断。在此状态下,被动关闭方会等待本地应用层处理完所有剩余数据并调用close()
函数,然后发送自己的FIN
报文,进入LAST_ACK
状态。FIN_WAIT_2
: 主动关闭方在收到对方对其FIN
报文的ACK
后进入此状态。此时,主动关闭方已经完成了数据发送,并且也收到了对方对其关闭请求的确认。在此状态下,它将等待接收被动关闭方发送的FIN
报文。一旦收到对方的FIN
报文,主动关闭方将发送最终的ACK
并进入TIME_WAIT
状态。LAST_ACK
: 被动关闭方在发送完所有剩余数据并发送自己的FIN
报文后进入此状态。在此状态下,被动关闭方等待接收主动关闭方对其FIN
报文的最终ACK
。一旦收到此ACK
,连接即完全关闭,进入CLOSED
状态。TIME_WAIT
: 主动关闭方在收到对方的FIN
并发送了最后一个ACK
后进入此状态。这是状态机中一个至关重要的状态。
TIME_WAIT
状态的深意
TIME_WAIT
状态,也常被称为 2MSL
等待状态,是 TCP 可靠性的最后一道屏障。主动关闭连接的一方,在发送最后一个 ACK
后,必须在这个状态停留两倍的 MSL (Maximum Segment Lifetime, 最大报文段生存时间)。MSL 是网络中任何 IP 数据包能够存活的最长时间。
这个等待机制有两个核心目的:
- 确保最后一个
ACK
报文能够到达对方:如果这个ACK
在网络中丢失了,对方(处于LAST_ACK
状态)会因为收不到确认而超时重传FIN
报文。如果主动关闭方此时已经彻底关闭(进入CLOSED
),它将无法响应这个重传的FIN
,导致对方无法正常关闭。TIME_WAIT
状态的存在,确保了它有足够的时间来处理这种情况,重新发送ACK
,帮助对方顺利关闭。 - 防止“旧连接”的延迟报文干扰新连接:假设没有
TIME_WAIT
,一个连接(例如,源端口 10000 -> 目标端口 80)刚关闭,马上又用完全相同的四元组(源IP、源端口、目标IP、目标端口)建立了一个新连接。此时,如果前一个连接中迷路的、延迟的数据包突然到达,它可能会被新连接错误地接收,造成数据混乱。等待2MSL
的时间,足以让本次连接中所有在网络中“游荡”的报文段都自行消亡,从而保证新连接的环境是“干净”的。
UDP:轻快的“明信片”
与 TCP 严谨的通话模式截然相反,UDP (User Datagram Protocol, 用户数据报协议) 奉行的是极简主义。你可以把它想象成一张明信片:写好地址、贴上邮票,然后直接投进邮筒。你不会先打电话确认收件人是否在家,也不会收到对方的回信确认。
这种“发完即走”的模式,正是 UDP 的核心。
核心特性:无连接与尽力而为
- 无连接 (Connectionless):UDP 在发送数据之前,不需要进行三次握手来建立连接。它直接将数据打包成“数据报 (Datagram)”就发送出去。
- 尽力而为 (Best-Effort):UDP 不提供任何可靠性保证。它不保证数据包一定能到达目的地,不保证数据包的顺序,也不会进行流量控制或拥塞控制。如果网络拥堵导致丢包,UDP 不会进行重传。这种看似“不负责任”的特性,正是其速度和效率的来源。
UDP 报文结构
UDP 的极简主义也体现在其报文头部上。它的头部固定只有 8 个字节,开销极小,所有字段一目了然:
字段 | 英文名称 | 简称 | 大小 | 描述 |
---|---|---|---|---|
源端口 | Source Port | sport |
16 bits | 标识发送方应用程序的端口号(此字段可选) |
目标端口 | Destination Port | dport |
16 bits | 标识接收方应用程序的端口号 |
长度 | Length | len |
16 bits | UDP 头部和数据的总长度(以字节为单位) |
校验和 | Checksum | csum |
16 bits | 用于简单的错误检测(此字段可选) |
适用场景
UDP 的高效和低延迟特性,使其在以下场景中备受青睐:
- 实时通信:在线游戏、视频会议、语音通话(VoIP)、直播等。在这些应用中,最新的数据远比旧数据重要。我们更能容忍画面偶尔的花屏(丢包),也无法接受为了等一个丢失的数据包而导致整个画面卡住。
- 查询类协议:如 DNS(域名系统)查询。客户端向服务器发送一个简短的查询请求,服务器返回一个简短的响应。这种“一问一答”的模式使用 UDP 效率极高。
- 广播与多播:当需要向网络中的多个节点发送相同的信息时,UDP 的无连接特性使其非常适合用于广播和多播。
对比与选择:TCP vs. UDP
现在,我们可以通过一个清晰的表格来总结 TCP 和 UDP 的核心差异,这将帮助我们理解在不同场景下该如何做出选择。
特性 | TCP (传输控制协议) | UDP (用户数据报协议) |
---|---|---|
连接性 | 面向连接 | 无连接 |
可靠性 | 可靠 | 不可靠(尽力而为) |
传输效率 | 慢,开销大 | 快,开销小 |
头部大小 | 至少 20 字节 | 固定 8 字节 |
控制机制 | 流量控制、拥塞控制 | 无 |
应用场景 | 网页(HTTP/S)、文件传输(FTP)、邮件(SMTP) | 视频会议、在线游戏、DNS、直播 |
选择的艺术:从上表可以看出,TCP 和 UDP 之间没有绝对的优劣之分。它们是为解决不同问题而设计的两种工具。选择哪种协议,完全取决于应用场景对可靠性和实时性的权衡。如果你的应用(如银行转账)绝不能容忍任何数据差错,那么 TCP 是不二之选;如果你的应用(如在线游戏)更看重实时反馈,可以容忍偶尔的数据丢失,那么 UDP 将是更明智的选择。
新的挑战与未来:QUIC
既然 TCP 如此可靠,为什么像 HTTP/3 这样的现代协议反而开始转向基于 UDP 构建?这引出了 TCP 一个长期存在的痛点。
在系列第五篇《计算机网络之五 - HTTP 与 HTTPS》中我们提到,HTTP/2 虽然通过多路复用技术,解决了应用层的队头阻塞,但它无法解决其底层 TCP 协议自身的队头阻塞 (Head-of-Line Blocking) 问题。在一条 TCP 连接中,如果一个数据包丢失了,那么后续所有的数据包(即使已经到达)都必须排队等待,直到那个丢失的包被成功重传。对于高并发的现代 Web 应用来说,这是一个巨大的性能瓶颈。
为了从根本上解决这个问题,QUIC (Quick UDP Internet Connections) 协议应运而生。它是一个构建在 UDP 之上的、全新的传输层协议。
QUIC 的设计非常巧妙,它相当于在 UDP 的“快车道”上,重新实现了一套现代化的可靠传输机制:
- 内置多路复用:QUIC 的流是独立的,一个流的丢包完全不会影响其他流的传输,从根本上解决了队头阻塞。
- 更快的连接建立:它将 TCP 的三次握手和 TLS 的加密握手过程合并,大大减少了建立安全连接所需的往返时间。
- 更好的拥塞控制:拥有比传统 TCP 更先进的拥塞控制算法。
QUIC 代表了传输层协议的未来演进方向,它试图将 TCP 的可靠性与 UDP 的低延迟优势集于一身,为下一代互联网应用提供更坚实的基础。
总结
TCP 和 UDP 是互联网传输层最核心的两个协议,它们各自代表了一种截然不同的设计哲学。
- TCP 如同一位严谨的工程师,通过三次握手、序列号、确认应答、超时重传等一系列复杂机制,构建了一个几乎万无一失的可靠数据通道。它的座右铭是:“宁可慢,不出错”。
- UDP 则像一位追求极致速度的信使,它卸下了所有保证可靠性的“包袱”,以最轻量、最直接的方式投递数据。它的信条是:“天下武功,唯快不破”。
正是这两种协议的差异化设计与共存,才共同支撑起了我们今天这个既需要高度可靠(如在线交易)又需要极致实时(如视频直播)的、丰富多彩的互联网世界。