即便是对网络协议了解不多的朋友,第一反应可能也是肯定的。这背后的原因,大家或许也能说个大概。

但这就引出了更深层的问题:使用UDP就一定比TCP快吗?是否存在UDP反而比TCP慢的场景? 我们今天就来深入探讨一下。
使用socket进行数据传输
当开发者需要在A电脑的进程发送数据到B电脑的进程时,通常会使用socket编程。Socket可以看作是一个电话或者邮箱。当你发送消息时,就像拨打电话或将信件投入邮箱,操作系统内核会通过Socket自动完成数据的传输过程。
通过Socket,我们可以选择基于TCP或UDP协议进行通信。
TCP作为一种可靠性协议,每次发送消息后都能得到明确的确认,类似于打电话时的“喂喂”,能立刻知道对方是否在线接听。
而UDP则像是给邮政信箱寄信,信件寄出后,你无法确认对方是否成功收到,丢失的可能性是存在的。
回到技术层面,创建Socket的典型方式如下:
fd = socket(AF_INET, 具体协议, 0);
注意这里的“具体协议”。如果传入 SOCK_STREAM,表示使用字节流传输数据,即 TCP协议。

如果传入 SOCK_DGRAM,则表示使用数据报传输,也就是 UDP协议。

函数返回的 fd 是Socket的文件描述符,可以理解为Socket的唯一标识。通过这个 fd,内核可以找到对应的Socket数据结构。
发送消息时,只需操作这个 fd,例如执行 send(fd, msg, ...)。接收方则执行 recv(fd, msg, ...) 来获取数据。在网络/系统编程中,理解Socket和文件描述符是基础。

对于异常情况的处理
但如果传输过程不顺利呢? 比如,数据包丢失了怎么办?
UDP和TCP对此的态度截然不同。
UDP的态度基本是:“哦,丢了?那不关我的事。”
TCP则截然相反:“这不行!是不是我发太快了?还是网络拥堵?放心,我一定给你重发。”
TCP为了实现这种可靠性承诺,在背后默默做了大量工作。
重传机制
TCP为每个发出的数据包分配一个序列号。接收方收到后,会回复一个确认号。发送方通过确认号就能知道哪些数据包已被成功接收。
如果长时间未收到某个数据包的确认,TCP会认为该包已丢失,并重新发送,这就是重传机制。

流量控制机制
然而,重传对性能影响较大,属于不得已而为之的下策。因此,TCP需要尽力避免重传的发生。
考虑到发送方和接收方的数据处理能力可能不同,TCP引入了发送窗口和接收窗口。接收窗口大小表示接收方当前还能处理多少数据,发送窗口大小则限制了发送方当前能发送的数据量。TCP通过动态调整窗口大小来控制发送速率,从而有效降低丢包概率。

滑动窗口机制
接收方的处理能力是动态变化的,有时快有时慢。处理快时希望接收更多数据,处理慢时则希望对方发送慢一些。发送过多数据可能导致处理不过来而丢包,进而触发重传。因此,接收窗口的大小需要能够动态调整,这就是滑动窗口机制。
简单来说,TCP通过滑动窗口机制来实现流量控制。

拥塞控制机制
但问题不仅限于通信双方。网络环境的拥堵也可能导致丢包。可以将网络想象成一条公路,当路上挤满了其他车辆时,即使你家有5辆车且目的地有5个车位,也无法一次性全部上路。
TCP希望能感知外部网络状况,并据此调整发包速率。其策略是“慢启动,拥塞避免”:先发送少量数据试探,然后逐渐增加发送量,直到出现丢包。此时,TCP就大致知道了当前网络的承载能力,并以此调整后续的发送节奏,这就是拥塞控制机制。
简单区分:流量控制针对的是单个连接两端的数据处理能力;拥塞控制针对的是整个网络路径的传输能力。

分段机制
尽管上述机制旨在降低丢包率,但重传仍无法完全避免。那么,当重传发生时,如何减少其影响呢?
答案是分段。如果需要发送一个超大数据包,一旦丢失,就需要重传整个大包,代价高昂。如果将其分割成多个小段,那么丢失时只需重传丢失的那一小段即可,压力骤减。这就是TCP的分段机制。
这一小段的长度,在传输层称为 MSS。当应用层下发的数据长度大于MSS时,TCP层会将其分成多个小于等于MSS的TCP段。

数据包到达网络层(IP层)后,如果其长度仍大于 MTU,IP层还会继续分片。

通常情况下,MSS = MTU - 40字节(20字节IP头+20字节TCP头)。因此,经过TCP分段的数据,到了IP层一般就不会再被分片了。

乱序重排机制
数据包既会被分段,网络路径又复杂多变,乱序也就成了常见现象。例如,发送顺序为1,2,3的数据包,可能因为路径不同,以2,3,1的顺序到达。
TCP利用数据包的序列号来识别其原始顺序。对于先到达的后发数据包,TCP会将其暂存到乱序队列中。待前面的数据包都到达后,再按照正确顺序重新组装并交付给应用层,这就是乱序重排机制。

连接机制
我们常说UDP无连接,TCP面向连接。这里的“连接”究竟是什么?
TCP通过上述种种机制实现了可靠传输,这些机制的背后,是操作系统内核在两端维护的一套复杂状态机(用于处理三次握手、四次挥手、各种异常等)。这套状态机就是所谓的“连接”。而UDP无需维护这套状态机,因此它是“无连接”的。
网络链路漫长而复杂,数据包丢失是常态。我们日常使用TCP传输数据时对此毫无感知,正是因为TCP默默承担了所有复杂性。
这就是TCP三大特性——“面向连接、可靠、基于字节流”中“可靠”一词的含义。如果换成UDP,丢包就是真丢了。
用UDP就一定比用TCP快吗?
此时UDP可能会反驳:“正因为我没有这些复杂的可靠性机制,所以我天生就快啊!”
在绝大多数情况下,这确实是事实。但问题也随之而来:
有没有使用了UDP,性能却反而不如TCP的场景呢?
答案是:有。在探讨这个场景前,我们需要先了解UDP的常见用途。
实际上,很少有项目会直接在生产环境中使用“裸UDP”。UDP的核心价值,在于它为内核提供了一个最基础、最轻量的网络传输原语。
正因为“忌惮”UDP的丢包问题,许多基于UDP的协议都会在应用层实现各自的可靠性保证机制。例如,游戏常用的KCP协议,以及新兴的QUIC协议,都在UDP之上实现了重传等逻辑,构建了一套类似TCP的可靠性体系。
教科书常说UDP适合音视频传输,因为这类场景可以容忍部分丢包。但并非所有包都能丢,比如关键帧丢失会导致画面模糊或卡顿,此时仍需应用层重传。此外,应用层通常也会实现乱序处理。例如,在网络通话中,你可能会听到断断续续的语句,但绝不会听到词语顺序完全错乱的话。
所以,虽然选择了UDP,但应用层往往会自行实现一部分可靠性机制。
关键问题来了:当需要传输一个超大数据包时,会发生什么?
对于TCP,其内部会根据MSS自动分段。分段后的每个数据包大小不超过MTU,因此IP层通常不会再分片。此时若发生丢包,只需重传丢失的那个MSS分段即可。

对于UDP,其本身不具备分段功能。如果应用层下发的数据包过大,到达IP层时就会被分片。此时一旦某个IP分片丢失,就需要重传整个原始的大数据包(因为UDP和IP层都没有记录分片与原始数据包的完整映射关系,无法只重传丢失的片段)。

在这种情况下,使用UDP反而会比TCP慢。
当然,解决方案也很直接:在应用层实现类似的分段机制。只要UDP之上的应用协议能够将大数据分割成适合网络传输的块,并管理它们的重传,就能避免上述性能问题。
总结
- TCP为了实现可靠性,引入了重传、流量控制、滑动窗口、拥塞控制、分段及乱序重排等一系列复杂机制,这通常使其在单纯的数据传输速度上慢于“轻装上阵”的UDP。
- TCP是面向连接的协议,UDP是无连接的。这里的“连接”本质上是操作系统内核维护的一套复杂状态机。
- 大部分实际项目会在UDP基础上,模仿TCP实现不同程度的可靠性机制,例如KCP协议就在应用层实现了重传。
- 在传输超大数据包且未实现应用层分段的情况下,UDP数据包会在IP层被分片。此时丢包会导致整个大数据包重传,性能可能反而不如具备自动分段能力的TCP。
深入了解这些底层机制,有助于我们在不同场景下做出更合理的协议选择。如果你想进一步探讨网络协议或其他技术话题,欢迎在云栈社区交流分享。
