同样跑在一条网络上,两个应用的表现可能完全不同。
下载文件时,少一个字节,压缩包就可能打不开;视频通话时,丢几帧画面,通常只是短暂花屏,通话还能继续。前者通常依赖 TCP,后者通常选择 UDP。
它们都在传输层,都建立在 IP 之上。区别不在于“谁更高级”,而在于它们对可靠性、延迟和顺序做了不同的取舍。
IP 只负责尽力而为
要理解 TCP 和 UDP,先看它们下面的 IP 协议。
IP 负责把数据包从一台机器送到另一台机器。它只承诺“尽力而为”:不保证送达,不保证顺序,也不保证不重复。网络拥塞、路由变化、链路故障,都可能让数据包丢失或乱序。
TCP 和 UDP 都复用 IP 的寻址能力,但在此之上做了不同的事:
- TCP 补上可靠性、顺序和连接管理。
- UDP 几乎不补,只增加端口、长度和校验和。
一个像挂号信,一个像明信片。
TCP:可靠的字节流
TCP 的全称是 Transmission Control Protocol,传输控制协议。它最核心的承诺是:把一段字节流可靠、有序地送到对端。
面向连接
TCP 通信前要先建立连接,也就是常说的三次握手:
客户端 服务端
| -------- SYN ----------> |
| <----- SYN + ACK ------- |
| -------- ACK ----------> |
| 连接建立 |
握手的目的不只是“打个招呼”,而是确认双方都能收、都能发,并同步初始序列号。
通信结束后还要四次挥手,确保双方都把剩余数据发送完,再释放连接。
序列号、确认和重传
TCP 给每个字节编号。接收方收到数据后返回 ACK,告诉发送方“我已经收到了哪些字节”。如果发送方在一定时间内没收到确认,就会重传。
这带来几个结果:
- 丢包会重传,所以数据最终能到达。
- 接收方可以按序列号重新排序,所以应用读到的是有序字节流。
- 重复包会被识别并丢弃,避免应用读到重复数据。
流量控制和拥塞控制
TCP 不只会重传,还会控制发送速度。
流量控制关注接收方:接收方缓冲区快满了,就通过窗口字段告诉发送方慢一点,别把数据压垮。
拥塞控制关注网络:发送方根据丢包和往返时延估计网络状况,动态调整发送速率。慢启动、拥塞避免、快速重传等机制,都是为了避免把网络打爆。
字节流,不是消息流
这是 TCP 最容易被误解的地方。
TCP 不保留应用层的消息边界。你调用两次 send 写入两条消息,接收方可能一次读到两条,也可能分三次读到一条。应用层必须自己定义边界,例如:
- 固定长度
- 长度前缀
- 换行符或特殊分隔符
- 更上层的协议格式,如 HTTP 头部的
Content-Length
所以 TCP 提供的是可靠有序的字节流,而不是“可靠的消息队列”。
代价:延迟和队头阻塞
可靠性不是免费的。
建立连接需要往返,重传需要等待,拥塞控制会限制发送速率。更麻烦的是队头阻塞:如果前面某个 TCP 段丢了,后面的数据即使已经到达,也不能立刻交给应用,必须等缺失部分重传并补齐。
在网页场景里,这会影响多个请求复用同一条连接时的表现。HTTP/2 的多路复用仍然跑在 TCP 上,所以底层丢包依然可能拖住所有流。
UDP:轻量的数据报
UDP 的全称是 User Datagram Protocol,用户数据报协议。它只做很少的事:
- 复用端口,把数据交给正确的应用。
- 保留消息边界,一个 UDP 数据报对应一次收发。
- 提供校验和,尽量发现数据损坏。
- 不建立连接,不确认,不重传,不排序,不控制拥塞。
UDP 头部只有 8 字节,而 TCP 头部最少 20 字节。这个差距在高频小包场景里很显眼。
无连接,发完就走
UDP 发送前不需要握手。应用把数据交给 UDP,UDP 加上端口和长度,交给 IP 发出去。
它不关心对方是否在线,也不关心数据有没有到达。丢包、乱序、重复,UDP 都不处理,交给应用自己决定。
这听起来很不可靠,但“不可靠”不等于“不能用”。很多实时场景宁愿丢一帧,也不愿意等重传。
支持广播和多播
UDP 支持一对多通信:
- 广播:发给同一网络内的所有主机。
- 多播:发给加入了特定组的主机。
DHCP、ARP 之外的很多发现协议、局域网服务发现、直播分发,都会利用这些能力。TCP 是点对点的,做不了这些事。
适合实时和低延迟场景
视频会议、在线游戏、语音通话、实时音视频,通常更在意“现在到”,而不是“一定到”。
一帧画面晚到 500 毫秒,基本就失去意义了。重传旧数据不如直接丢掉,继续处理下一帧。UDP 的低开销和无队头阻塞,正好适合这种场景。
DNS 也是典型例子。一次查询通常只有一问一答,建立 TCP 连接的成本可能比查询本身还高,所以 DNS 主要使用 UDP。响应太大或需要区域传送时,才会改用 TCP。
一张表看清差异
| 维度 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接,需要握手 | 无连接,直接发送 |
| 可靠性 | 确认、重传,保证送达 | 不保证送达 |
| 顺序 | 保证有序 | 不保证顺序 |
| 消息边界 | 不保留,是字节流 | 保留数据报边界 |
| 头部大小 | 最少 20 字节 | 8 字节 |
| 流量控制 | 有 | 无 |
| 拥塞控制 | 有 | 无,应用自行处理 |
| 传输速度 | 开销较大,通常更慢 | 开销小,延迟低 |
| 广播/多播 | 不支持 | 支持 |
| 典型场景 | HTTP、文件传输、邮件、SSH、数据库 | DNS、DHCP、音视频、游戏、QUIC |
怎么选:先问三个问题
实际开发中,可以用三个问题快速判断。
1. 丢一个包可以接受吗?
如果每一个字节都必须到达,例如转账记录、文件内容、配置下发,优先 TCP。
如果少量丢失可以接受,例如视频帧、游戏位置更新、实时语音,UDP 更合适。
2. 延迟和可靠性,哪个更重要?
需要严格可靠时,TCP 的重传和拥塞控制省去了大量应用层工作。
需要低延迟时,UDP 不会被重传和队头阻塞拖住。代价是应用要自己处理丢包、乱序和重复。
3. 是否需要广播、多播或极致轻量?
需要一对多、局域网发现、实时媒体分发时,UDP 往往是唯一选择。
如果只是普通请求响应,TCP 的可靠性更省心。HTTP/1.1 和 HTTP/2 都跑在 TCP 上,就是这个原因。
几个常见误解
误解一:UDP 一定比 TCP 快
UDP 头部更小、没有握手和重传,单看协议开销确实更轻。但“快”取决于场景。
如果网络丢包严重,应用在 UDP 之上自己实现可靠传输,最终可能比 TCP 更慢、更复杂。很多基于 UDP 的可靠协议,实际上是在用户态重新造了一套 TCP 式的机制。
误解二:TCP 保证消息完整
TCP 保证字节流可靠有序,但不保证应用层消息完整。一次 read 可能只读到半条消息,也可能读到一条半。应用必须自己处理粘包和拆包。
误解三:用了 UDP 就不能可靠
UDP 本身不提供可靠性,但应用可以在它之上实现确认、重传、排序和拥塞控制。QUIC 就是这么做的。
QUIC:把可靠性搬到 UDP 之上
HTTP/3 使用 QUIC,而 QUIC 基于 UDP。
它保留 UDP 的低开销和灵活调度,同时在用户态实现可靠传输、加密、多路复用和拥塞控制。因为每个流可以独立处理丢包,一个流受阻不会像 TCP 那样拖住所有流。
这也说明 TCP 和 UDP 不是简单的“可靠”与“不可靠”二选一。UDP 更像一块空白画布,协议设计者可以在上面画出自己需要的可靠性模型。
用 Node.js 感受一下
TCP 服务端:
const net = require('node:net');
const server = net.createServer((socket) => {
socket.on('data', (data) => {
socket.write(`echo: ${data}`);
});
});
server.listen(3000);
UDP 服务端:
const dgram = require('node:dgram');
const server = dgram.createSocket('udp4');
server.on('message', (msg, rinfo) => {
console.log(`from ${rinfo.address}:${rinfo.port}: ${msg}`);
});
server.bind(41234);
TCP 的 socket 是有状态的连接,可以持续读写;UDP 的 message 是一次独立数据报,发送方地址从 rinfo 里取。两种编程模型,对应两种协议假设。
最后
TCP 用握手、确认、重传、排序、流量控制和拥塞控制,换来可靠有序的字节流。它适合“一个字节都不能少”的场景,但也要接受延迟、开销和队头阻塞。
UDP 只提供最小的传输能力,把可靠性、顺序和拥塞控制交给应用。它适合实时、低延迟、一对多和自定义协议的场景,但应用必须自己兜底。
选协议时,不要问“哪个更先进”,先问:这个业务丢一个包会怎样?能等多久?是否需要多播?答案通常就在这三个问题里。