TCP 与 UDP 对开发人员而言 — 协议何时真正重要
停止将TCP和UDP视为可以互换的复选框。以下是可靠性保证实际上带来的成本,DNS为何使用UDP,以及你的游戏为何会卡顿的原因。
你正在开发一款多人游戏,玩家抱怨存在延迟问题。你已经优化了服务器代码,减少了数据库查询,p99延迟也表现良好。但问题其实很简单:你使用的是TCP协议。问题就在这里——这就是延迟的根源。
TCP和UDP不仅仅是协议列表上的两个选项,它们代表了对网络行为截然不同的假设。错误的选择不仅会降低性能,还会改变系统出错时的表现方式。
TCP所做出的承诺
TCP向你保证一个可靠且有序的字节流。如果一个数据包丢失,TCP会重新发送它;如果数据包到达顺序错误,TCP会重新排序它们。你的应用程序始终接收到干净、连续的数据。
这种保证带来了三大代价:
- 握手延迟 — TCP在任何应用数据传输之前都需要进行三次握手。在一个50毫秒往返时间的网络中,你将在第一个请求开始前消耗掉150毫秒的延迟。
- 首包阻塞 — 这是导致游戏卡顿的关键因素。如果数据包#5丢失,TCP会将后续的数据包#6、#7和#8暂存缓冲区,等待#5被重传并到达。整个数据流被阻塞。玩家100毫秒前的位置更新仍停留在缓冲区中,而网络正在自行修复。
- 拥塞控制开销 — TCP的拥塞窗口算法(如CUBIC、BBR等)在检测到丢包时会降低发送速率。在网络丢包的情况下,这会导致TCP在用户最感到不适的时刻降低吞吐量。
UDP实际上能为你提供的能力
UDP发送一个数据报后不再回头。没有握手,没有确认,没有重传。如果一个数据包丢失,它就消失了。接收方收到的数据是按实际到达顺序接收的。
这不是一个局限,而是核心所在。当你更看重低延迟而非数据交付保障时,UDP让你明确地做出这种权衡。可靠性逻辑被移到了应用层,你可以更智能地决定哪些数据需要重传。
在游戏场景中,50毫秒前的玩家位置更新毫无价值——你想要的是当前状态。使用TCP时,你需要缓冲并等待;使用UDP时,你直接发送最新状态,跳过所有过时数据。即使在网络丢包更多的情况下,体验也更流畅。
TCP与UDP:真正重要的对比
| 财产 | TCP | UDP |
|---|---|---|
| 连接建立 | 三次握手(在第一个字节发送前增加1.5倍RTT) | 无——立即发送 |
| 交付保障 | 是——丢包时重传 | 否——发送后即忘 |
| 数据包排序 | 由协议栈强制执行 | 你的问题 |
| 首包阻塞 | 是——一个丢失的数据包会阻塞整个流 | 否——每个数据报是独立的 |
| 拥塞控制 | 内置(CUBIC、BBR等) | 无——自行实现或完全跳过 |
| 典型延迟开销 | 冷连接下为150–300毫秒 | 亚毫秒级 |
| 用例 | HTTP/1.1、HTTP/2、数据库、文件传输、电子邮件 | DNS、游戏、实时视频、HTTP/3(QUIC) |
每个协议实际的应用场景
DNS通过UDP运行——这其中蕴含一个教训
你应用程序中每一个DNS查询默认都通过UDP进行。请求能放进一个数据包,响应也能放进一个数据包,你只需一个往返即可获得答案。没有握手开销,服务器无需维护状态。
如果响应过大(如DNSSEC记录、多个A记录),DNS会退化到TCP。但常见情况——主机名查询——是纯粹的UDP,因为权衡是显而易见的:三次握手的时间会超过实际查询时间。
你可以通过 IO Tools DNS查询工具 — 输入一个域名,观察不同记录类型解析的速度。这种速度正是UDP消除了完整握手开销的结果。
游戏:UDP是唯一真正可行的选择
所有主流游戏网络库——Valve的GameNetworkingSockets、Epic的EOS传输、Unity的UTP——都是基于UDP构建的。原因正是首包阻塞问题。
在竞争性第一人称射击游戏中,你每15毫秒发送一次位置更新(64个tick)。如果一个数据包丢失,TCP会阻塞接下来的五个数据包,直到重传到达,这会在最糟糕的时刻引入75毫秒的卡顿。而使用UDP时,你立即发送下一个更新,客户端通过插值填补间隙。体验是平滑的。
大多数基于UDP的游戏网络代码会实现自己的选择性可靠性机制——序列号、优先级队列、选择性确认——但仅针对真正需要的数据:聊天消息、拾取物品、匹配状态。位置数据在设计上是不可靠的。一个过时的读数比没有读数更糟糕。
视频流媒体:取决于具体场景
实时直播(如Twitch、体育赛事)使用基于UDP的协议——RTP、WebRTC、SRT——因为少量丢帧可以接受,但延迟不可容忍。你无法缓冲30秒的直播内容以保证流畅播放。
点播内容(如Netflix、YouTube)实际上使用TCP,因为缓冲隐藏了成本。几秒钟的预缓冲意味着TCP的重传开销变得不可见——你只是看到平滑播放。当你观看的是昨天发生的内容时,延迟惩罚并不重要。
HTTP/3基于UDP运行——并改变了网页性能
HTTP/3基于QUIC运行,而QUIC运行在UDP之上。谷歌专门设计QUIC来解决TCP在网页流量中的首包阻塞问题。在HTTP/2 over TCP中,一个丢失的数据包会阻塞所有共享该连接的多路复用流。QUIC在传输层实现了流多路复用,并通过独立确认——一个丢失的数据包只影响一个流,而不是所有流。
QUIC还将TLS集成到握手过程中,将冷连接建立时间减少到单个往返(会话恢复时为0-RTT)。在丢包较多的网络——如移动网络、拥堵的Wi-Fi——这一改进是显著的。截至2024年,大约 30%的网站支持HTTP/3,所有主流浏览器默认启用它。如果你部署在Cloudflare或现代CDN之后,你很可能已经无需配置就正在提供HTTP/3服务。
实际的决策流程
当你为新协议或服务选择传输方式时,问题不是“TCP还是UDP?”而是“我能够容忍哪些失败?”
- 每个字节都必须按顺序到达,否则操作将失败 → TCP。文件上传、数据库连接、API调用、电子邮件。数据缺失意味着数据损坏或解析错误。
- 延迟比保证交付更重要 → UDP。游戏、实时视频、语音通话、传感器遥测。一个过时的读数比没有读数更糟糕。
- 你需要每条消息都具备两者 → 基于UDP构建选择性可靠性。QUIC做到了这一点。WebRTC的SCTP数据通道做到了这一点。ENet和GameNetworkingSockets等库也做到了这一点——尽管自行实现这种功能工作量巨大,且容易出现微妙错误。
一个值得指出的错误:认为“内部流量”意味着可以随意使用UDP。数据中心内的丢包虽然罕见,但并非为零——硬件故障、高峰负载下的网络拥塞、配置错误的交换机。如果你的应用在丢包时静默地损坏数据,内部网络将无法保护你。
总结
TCP是大多数应用代码的合理默认选择。如果你在进行API调用、与数据库通信或文件传输,TCP的保障正是你需要的,其开销在人类时间尺度上是不可见的。
UDP是当延迟是硬性约束且你的应用能掌控其可靠性时的正确选择。这是一类特定问题——实时游戏、实时媒体、自定义协议——而不是一种在TCP表现缓慢时就应随意采用的通用性能优化。
真正的错误不是在UDP更快的情况下使用TCP。而是不知道自己选择了哪一个,以及为何选择,当故障模式出现时感到惊讶。
