TCP 与 UDP 是网络传输层的两大核心协议,它们以截然不同的方式定义了数据在应用程序间的传输。TCP 如同一次通话,追求可靠与完整;UDP 则像一张明信片,主张高效与迅捷。正是这两种设计哲学的差异,决定了它们在网页浏览、文件传输、视频会议、在线游戏等不同场景下的应用。本文将深入解析二者的核心机制与关键差异。

本系列其余几篇的目录:


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 - 可变 用于携带额外的控制信息

在理解后续的握手流程时,我们无需关注所有细节,只需将注意力集中在最重要的四个“角色”上:SYNACK 这两个标志位,以及 seqack 这两个核心编号

SYNACK 是 TCP 报文头中的两个非常重要的标志位 (Flags),它们就像是通信双方用来表达意图的“信号旗”。

  • SYN (Synchronize Sequence Numbers - 同步序列号)

    • 含义:这个标志位用于发起和建立连接。当一方想要与另一方建立连接时,它会发送一个 SYN 标志位置为 1 的报文。这可以理解为在说:“你好,我想和你建立通信,我们来同步一下初始的序列号吧!”
  • ACK (Acknowledgment - 确认)

    • 含义:这个标志位用于确认收到数据。当 ACK 标志位置为 1 时,意味着报文中的“确认号”字段有效。它告诉对方:“你之前发送的数据我已经收到了。” 在连接建立之后,几乎所有的 TCP 报文都会将 ACK 位置为 1。

seqack 是 TCP 实现可靠传输的基石,它们共同解决了一个核心问题:在不可靠的网络上,如何保证数据不丢、不乱、不重

  • seq (Sequence Number - 序列号)

    • 作用:它的核心作用是给数据包进行编号。TCP 把要传输的数据看作一个连续的字节流,seq 就是这个流中每一个数据包里第一个字节的编号
    • 设计原因
      1. 保证顺序:网络传输中,数据包可能会因为路由不同而失序到达。接收方可以根据 seq 号对数据包进行重新排序,从而恢复出原始的、有序的数据。
      2. 丢包检测:接收方如果发现收到的 seq 号不连续(比如收到了 100 和 300,但没收到 200),就知道中间有数据包丢失了,可以请求发送方重传。
  • ack (Acknowledgment Number - 确认号)

    • 作用:它的作用是告诉发送方我期望接收的下一个字节的序列号是多少。这个设计非常巧妙,因为它隐含地确认了在这个编号之前的所有数据都已成功收到
    • 设计原因
      1. 高效确认:如果发送方发送了 100、200、300 三个包,接收方只需回复一个 ack=400,就代表“100、200、300 我都收到了,请从 400 开始发”。这比为每个包都单独回复一次确认要高效得多。
      2. 建立可靠连接:它是发送方判断对方是否成功收到数据的唯一依据。

三次握手:同步序列号与交换能力

理解了上述几个核心“词汇”后,我们再来审视三次握手的过程,它的每一步都变得有据可循。其本质,是通过三次通信,完成两个核心任务:

  1. 交换并确认双方的初始序列号 (ISN),为后续数据的有序传输打下基础。
  2. 确认双方都具备可靠的发送和接收能力
  1. 第一次握手 (Client -> Server):

    • 内容: 客户端发送一个 TCP 报文,其中 SYN 标志位置为 1,并选择一个随机的初始序列号 seq=x
    • 目的: 客户端向服务器表明“我想要建立连接”,并告知自己的起始序列号。
    • 状态: 客户端进入 SYN_SENT 状态。
  2. 第二次握手 (Server -> Client):

    • 内容: 服务器收到客户端的 SYN 包后,回复一个报文。该报文中 SYNACK 标志位都置为 1。服务器也选择一个自己的随机初始序列号 seq=y,同时将确认号 ack 设置为 x+1
    • 目的: 服务器通过 ACK=1ack=x+1 告诉客户端:“你的请求我收到了”。通过 SYN=1seq=y 表明:“我也同意建立连接,这是我的起始序列号”。
    • 状态: 服务器进入 SYN_RCVD 状态。此时,服务器已确认客户端的发送能力正常。
  3. 第三次握手 (Client -> Server):

    • 内容: 客户端收到服务器的 SYN-ACK 包后,发送最后一个确认报文。该报文 ACK 标志位置为 1,seq 设置为 x+1,并将确认号 ack 设置为 y+1
    • 目的: 客户端通过 ACK=1ack=y+1 告诉服务器:“你的回应我已收到,现在我们可以开始通信了”。
    • 状态: 此报文发送后,客户端进入 ESTABLISHED 状态。服务器收到后,也进入 ESTABLISHED 状态。连接正式建立。此时,双方都确认了对方的收发能力正常。

说到这里,我有一个问题:“为什么要交换 seqack 呢?”

本质是双向确认:TCP 是一个全双工的协议,意味着通信双方都可以同时发送和接收数据。因此,每一方都必须有自己的 seq 号来标记自己发送的数据,也必须有自己的 ack 号来确认收到的对方数据
三次握手就是交换和确认彼此的初始序列号(ISN)的过程

  1. 第一次握手:客户端发送 SYN 和自己的 seq=x。它在说:“我的初始序列号是 x,你收到了吗?”
  2. 第二次握手:服务器回复 SYN、自己的 seq=yack=x+1。它在说:“我收到了你的 x,所以我确认你的下一个应该是 x+1。同时,我的初始序列号是 y,你收到了吗?”
  3. 第三次握手:客户端回复 ACKack=y+1。它在说:“我收到了你的 y,所以我确认你的下一个应该是 y+1。”

为什么必须是三次握手,而不是两次?

最核心的原因,是为了防止早已失效的、旧的连接请求突然又送达服务器,从而引发错误

想象一个网络有些延迟的场景:

  1. 客户端发送了第一个连接请求 SYN(我们称之为 请求A),但它在网络中被卡住了,迟迟没有到达服务器。
  2. 客户端等了一会儿没收到回应,以为丢包了,于是又发送了一个新的连接请求 SYN请求B)。
  3. 请求B 顺利到达,服务器正常回应,双方通过三次握手建立了连接,传输数据,然后正常关闭了连接。
  4. 就在这时,那个被卡了很久的 请求A 终于抵达了服务器。

如果只有两次握手,服务器收到 请求A 后,会误以为是客户端又发起了一个新的连接请求。它会立即分配资源,建立连接,然后傻傻地等待客户端发来数据。但此时的客户端对此一无所知,它根本不会理会服务器的确认,更不会发送任何数据。

结果就是,服务器单方面开启了一个“空连接”,白白浪费了系统资源,直到超时后才关闭。而三次握手,通过增加第三次客户端的最终确认,完美地解决了这个问题。服务器只有在收到客户端对自己的 SYN 的最终 ACK 之后,才会确信这是一个有效的、全新的连接请求。

这个过程可以用下面的时序图来表示:

TCP 可靠性的基石

TCP 的可靠性并非单一功能,而是一个由多种机制协同工作的复杂系统。这些机制相互配合,共同确保了数据传输的完整性、有序性、无差错和高效性。

  • 序列号 (Sequence Numbers) 与确认应答 (Acknowledgements, ACK):TCP 将发送的数据分割成一个个小的数据段(Segment),并为每个字节都分配一个唯一的序列号。接收方收到数据后,会发送一个 ACK 报文作为回应,其中包含一个确认号,告诉发送方“我已经收到了你到哪个序列号为止的所有数据,请从下一个序列号开始发”。这种“有问有答”的机制是保证数据不丢失的基础。

  • 超时重传 (Timeout Retransmission):如果在发送数据后的一段时间内(这个时间是动态计算的)没有收到对方的 ACK,发送方就会认为数据包可能在路上丢失了,于是会重新发送这个数据包。

  • 流量控制 (Flow Control):接收方会通过 TCP 头部中的“窗口大小 (Window Size)”字段,告诉发送方自己当前还能接收多少数据。发送方则根据这个窗口大小来调整自己的发送速率,确保不会因为发送过快而导致接收方处理不过来,造成数据溢出。

  • 拥塞控制 (Congestion Control):流量控制关心的是“点对点”的速率匹配,而拥塞控制则着眼于整个网络的健康状况。TCP 通过一系列算法(如慢启动、拥塞避免等)来探测网络的拥堵程度,并主动调整发送速率,避免因自身流量过大而加剧网络拥堵,最终导致大规模丢包。

四次挥手:礼貌地“挂断电话”

与建立连接同样重要的是,如何安全、完整地断开连接。这个过程被称为“四次挥手”,因为它需要四次信息交换来确保双方的数据都已传输完毕。

  1. 第一次挥手 (FIN):客户端决定关闭连接,向服务器发送一个 FIN 报文,表示“我的数据已经全部发送完毕了”。此时客户端进入 FIN_WAIT_1 状态。

  2. 第二次挥手 (ACK):服务器收到 FIN 报文后,回复一个 ACK 报文,表示“收到了你的关闭请求”。但此时服务器可能还有未发送完的数据,所以它还不能立即关闭连接。此时,服务器进入 CLOSE_WAIT 状态,客户端收到 ACK 后进入 FIN_WAIT_2 状态。

  3. 第三次挥手 (FIN):服务器将所有剩余数据发送完毕后,会向客户端发送一个 FIN 报文,表示“我这边的数据也发完了,可以关闭了”。服务器随之进入 LAST_ACK 状态。

  4. 第四次挥手 (ACK):客户端收到服务器的 FIN 报文后,回复最后一个 ACK 报文进行确认。发送完毕后,客户端会进入 TIME_WAIT 状态,等待一段时间(通常是 2MSL,两倍的最大报文段生存时间)以确保服务器收到了这个 ACK,防止网络中可能存在的延迟报文造成问题。服务器收到 ACK 后则直接进入 CLOSED 状态。至此,连接被完全断开。

一定是挥四次手吗 👋?

我们可以分别从逻辑上以及行为上来看待“四次挥手”这件事:

  • 从逻辑上看:TCP 协议一定会按照逻辑进行完整的四个过程,所以从这个角度上来看,一定是“四次挥手”。四个过程分别是:
    1. 客户端关闭发送通道。
    2. 服务器确认客户端关闭发送通道。
    3. 服务器关闭发送通道。
    4. 客户端确认服务器关闭发送通道。
  • 从行为上看:但是如果抓包的话,你可能会发现只有三个报文段的情况,并且这种情况还不少见。这是因为当第二次挥手时,如果服务器没有剩余要发送给客户端的数据,那么 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 数据包能够存活的最长时间。

这个等待机制有两个核心目的:

  1. 确保最后一个 ACK 报文能够到达对方:如果这个 ACK 在网络中丢失了,对方(处于 LAST_ACK 状态)会因为收不到确认而超时重传 FIN 报文。如果主动关闭方此时已经彻底关闭(进入 CLOSED),它将无法响应这个重传的 FIN,导致对方无法正常关闭。TIME_WAIT 状态的存在,确保了它有足够的时间来处理这种情况,重新发送 ACK,帮助对方顺利关闭。
  2. 防止“旧连接”的延迟报文干扰新连接:假设没有 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 则像一位追求极致速度的信使,它卸下了所有保证可靠性的“包袱”,以最轻量、最直接的方式投递数据。它的信条是:“天下武功,唯快不破”。

正是这两种协议的差异化设计与共存,才共同支撑起了我们今天这个既需要高度可靠(如在线交易)又需要极致实时(如视频直播)的、丰富多彩的互联网世界。