UDP 与可靠传输
用生活化的比喻,让你理解 UDP 的特点、QUIC 协议、以及为什么游戏用 UDP 而不是 TCP
前置知识:第06章 TCP/IP 协议栈(理解 TCP 的拥塞控制和队头阻塞问题)
阅读指南(初学者必看)
为什么你需要学习 UDP 与可靠传输?
上一章你学了 TCP 的致命问题——队头阻塞和拥塞控制对游戏不利。那游戏用什么?UDP!但 UDP 不可靠,怎么保证消息送达?
- 为什么游戏用 UDP?UDP 和 TCP 的核心区别是什么?
- QUIC 是什么?为什么 HTTP/3 基于 QUIC 而不是 TCP?
- 怎么在 UDP 上实现可靠传输?
学完本章,你能回答:
- UDP 和 TCP 的核心区别是什么?为什么游戏用 UDP?
- QUIC 协议解决了 TCP 的哪些问题?
- 0-RTT 连接建立是怎么做到的?
- 怎么在 UDP 上实现自定义可靠传输?
本文结构
第一部分:UDP 的特点(和 TCP 的对比)
第二部分:UDP 编程
第三部分:QUIC 协议(UDP 上的可靠传输 + 多路复用 + 加密)
第四部分:自定义可靠 UDP
7.1 UDP 的特点
生活比喻:UDP 就像寄明信片。
TCP:挂号信
- 需要登记(建立连接)
- 确认收到(确认应答)
- 丢失可查(可靠传输)
- 速度较慢
UDP:明信片
- 直接投递(无连接)
- 不确认收到(不可靠)
- 可能丢失(无保证)
- 速度快
UDP vs TCP:
| | UDP | TCP |
|---|---|---|
| 连接 | 无连接 | 面向连接 |
| 可靠性 | 不保证送达 | 保证送达 |
| 有序性 | 不保证有序 | 保证有序 |
| 流量控制 | 无 | 有(滑动窗口)|
| 拥塞控制 | 无 | 有 |
| 头部大小 | 8 字节 | 20~60 字节 |
| 传输效率 | 高 | 低 |
| 适用场景 | 实时游戏、视频通话 | 文件传输、网页浏览 |
为什么游戏用 UDP?
1. 不需要等重传 → 低延迟
2. 丢了一个位置更新无所谓 → 下一个更新很快就来
3. 没有拥塞控制 → 不会因为丢包而大幅降速
4. 没有队头阻塞 → 一个包丢了不影响其他包
UDP 头部结构
0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| Source | Destination |
| Port | Port |
+--------+--------+--------+--------+
| Length | Checksum |
+--------+--------+--------+--------+
| Data... |
+-----------------------------------+
头部只有8字节,非常精简!
7.2 QUIC 协议
生活类比:QUIC 就像"快递升级版"。TCP 是普通快递(必须按顺序签收,一个丢了后面的都等着),QUIC 是智能快递(每个包裹独立签收,互不影响)。
QUIC = UDP 上的可靠传输 + 多路复用 + 加密
核心特性:
1. 基于 UDP → 不需要内核修改,用户态实现
2. 0-RTT 连接建立 → 首次 1-RTT,重连 0-RTT
3. 连接迁移 → IP 变化不断连(WiFi → 4G 无缝切换)
4. 多路复用无队头阻塞 → 每个流独立
5. 内置 TLS 1.3 → 所有数据加密
TCP 队头阻塞 vs QUIC:
TCP: 流A丢包 → 流A/B/C 全部等待
QUIC: 流A丢包 → 只有流A等待,流B/C 继续
0-RTT 连接建立
TCP + TLS 1.3:
客户端 → SYN → 服务器 (1 RTT)
客户端 ← SYN-ACK ← 服务器 (1 RTT)
客户端 → ClientHello → 服务器 (1 RTT)
客户端 ← ServerHello ← 服务器 (1 RTT)
客户端 → 请求 → 服务器
总计:3 RTT 后才能发数据
QUIC:
首次连接:
客户端 → CHLO → 服务器 (1 RTT)
客户端 ← SHLO ← 服务器 (1 RTT)
客户端 → 请求 → 服务器
总计:1 RTT 后就能发数据
重连(0-RTT):
客户端 → CHLO + 请求 → 服务器 (0 RTT!)
服务器直接处理请求
总计:0 RTT 后就能发数据
游戏场景:
- 首次进入游戏:1 RTT 建立 QUIC 连接
- 断线重连:0 RTT 恢复连接
- 网络切换(WiFi→4G):连接不断!
7.3 自定义可靠 UDP
为什么需要可靠 UDP
游戏场景需要:
- 低延迟(UDP优势)
- 可靠性(某些数据不能丢,如技能释放)
- 有序性(状态更新顺序重要)
解决方案:在UDP之上实现可靠传输机制
核心机制:
1. ACK 确认——接收方收到包后发送确认
2. 序号——给每个包编号,检测丢包和重排序
3. 重传——超时未确认的包重发(选择性重传比全部重传高效)
4. 流量控制——类似 TCP 的滑动窗口
序列号与确认
class ReliablePacket {
constructor(seq, data, flags = {}) {
this.seq = seq;
this.data = data;
this.ack = flags.ack || 0;
this.ackBits = flags.ackBits || 0;
this.timestamp = Date.now();
}
serialize() {
let buffer = new ArrayBuffer(12 + this.data.length);
let view = new DataView(buffer);
view.setUint32(0, this.seq);
view.setUint32(4, this.ack);
view.setUint32(8, this.ackBits);
let dataView = new Uint8Array(buffer, 12);
for (let i = 0; i < this.data.length; i++) {
dataView[i] = this.data[i];
}
return new Uint8Array(buffer);
}
}
class ReliableTransport {
constructor() {
this.localSeq = 0;
this.remoteSeq = 0;
this.sentPackets = new Map();
this.receivedPackets = new Map();
this.resendTimeout = 100;
this.maxResends = 5;
}
send(data) {
let packet = new ReliablePacket(this.localSeq, data, {
ack: this.remoteSeq,
ackBits: this.calculateAckBits()
});
this.sentPackets.set(this.localSeq, {
packet, sends: 1, lastSend: Date.now()
});
this.localSeq++;
return packet.serialize();
}
calculateAckBits() {
let bits = 0;
for (let i = 0; i < 32; i++) {
let seq = this.remoteSeq - i - 1;
if (this.receivedPackets.has(seq)) {
bits |= (1 << i);
}
}
return bits;
}
checkResends() {
let now = Date.now();
let toResend = [];
for (let [seq, info] of this.sentPackets) {
if (now - info.lastSend > this.resendTimeout) {
if (info.sends >= this.maxResends) {
this.sentPackets.delete(seq);
continue;
}
toResend.push({ seq, packet: info.packet });
info.sends++;
info.lastSend = now;
}
}
return toResend;
}
}
滑动窗口实现
class SlidingWindowProtocol {
constructor(windowSize = 32) {
this.windowSize = windowSize;
this.sendWindow = new Map();
this.recvWindow = new Map();
this.sendBase = 0;
this.recvBase = 0;
this.nextSeq = 0;
}
canSend() {
return this.nextSeq < this.sendBase + this.windowSize;
}
send(data) {
if (!this.canSend()) return null;
let seq = this.nextSeq++;
this.sendWindow.set(seq, { data, timestamp: Date.now(), acked: false });
return { seq, data };
}
receive(seq, data) {
if (seq >= this.recvBase && seq < this.recvBase + this.windowSize) {
this.recvWindow.set(seq, data);
this.slideRecvWindow();
return true;
}
return false;
}
slideRecvWindow() {
let deliverable = [];
while (this.recvWindow.has(this.recvBase)) {
deliverable.push({ seq: this.recvBase, data: this.recvWindow.get(this.recvBase) });
this.recvWindow.delete(this.recvBase);
this.recvBase++;
}
return deliverable;
}
}
7.4 游戏状态同步
class GameStateSync {
constructor() {
this.transport = new ReliableTransport();
this.localState = {};
this.remoteState = {};
this.stateSeq = 0;
}
updateState(newState) {
this.localState = { ...this.localState, ...newState };
this.stateSeq++;
let delta = this.calculateDelta(this.localState, this.lastSentState || {});
if (Object.keys(delta).length > 0) {
let data = JSON.stringify({ seq: this.stateSeq, state: delta });
this.transport.send(data);
this.lastSentState = { ...this.localState };
}
}
calculateDelta(current, previous) {
let delta = {};
for (let key in current) {
if (current[key] !== previous[key]) {
delta[key] = current[key];
}
}
return delta;
}
receiveState(buffer) {
let packet = this.transport.receive(buffer);
if (packet.seq > (this.lastReceivedSeq || 0)) {
let state = JSON.parse(packet.data);
this.remoteState = { ...this.remoteState, ...state.state };
this.lastReceivedSeq = packet.seq;
}
}
}
自问自答
Q:UDP 不可靠,为什么游戏还用 UDP? A:因为游戏对"延迟"的要求远高于"可靠性"。一个位置更新丢了,不需要重传——下一个更新 50ms 后就来了。但 TCP 丢包后会:1)等待重传(增加延迟);2)后续包被队头阻塞;3)拥塞控制降速。这些对实时游戏是致命的。关键数据(如技能释放)可以在应用层实现可靠传输。
Q:QUIC 为什么基于 UDP 而不是直接修改 TCP? A:1)TCP 在操作系统内核中实现,修改需要更新所有操作系统,推广周期可能数十年;2)中间网络设备(NAT、防火墙)认识 TCP 但不认识新协议,会被丢弃;3)UDP 是"白名单"——只要应用层实现了就能用,不需要操作系统支持。所以 QUIC 选择在用户态基于 UDP 实现。
Q:0-RTT 连接安全吗? A:0-RTT 有"重放攻击"的风险——攻击者可以重放之前捕获的 0-RTT 请求。QUIC 通过以下方式缓解:1)服务器为每次连接生成新的随机数;2)0-RTT 数据不能包含非幂等操作;3)客户端和服务器共同维护重放窗口。对于游戏来说,0-RTT 通常只用于恢复连接上下文,真正的游戏操作在 1-RTT 之后。
Q:连接迁移是怎么实现的? A:TCP 用"四元组"(源IP、源端口、目标IP、目标端口)标识连接,IP 变化 = 新连接。QUIC 用"Connection ID"标识连接,Connection ID 不随 IP 变化。WiFi→4G 切换时,IP 变了但 Connection ID 没变,服务器识别出这是同一个连接,继续通信。这对移动游戏特别重要!
Q:怎么在 UDP 上实现可靠传输? A:核心机制:1)ACK 确认——接收方收到包后发送确认;2)序号——给每个包编号,检测丢包和重排序;3)重传——超时未确认的包重发(选择性重传比全部重传高效);4)流量控制——类似 TCP 的滑动窗口。QUIC 就是这样做的,但比 TCP 更灵活——可以按流独立控制。
实践任务
- 任务1:用 Wireshark 抓包,对比 TCP 和 UDP 的头部结构,理解 UDP 为什么更轻量
- 任务2:用
curl --http3测试一个支持 HTTP/3 的网站,观察 QUIC 的连接建立过程 - 任务3:实现一个简易可靠 UDP——给 UDP 包加序号,实现 ACK + 选择性重传
- 任务4:测试 WiFi→4G 切换时 TCP 和 QUIC 的表现差异(如果有条件)
- 任务5:对比 TCP 和 UDP 在丢包环境下的延迟差异(用
tc netem模拟丢包)
与其他章节的关联
| 本章内容 | 关联章节 | 关联点 |
|---|---|---|
| UDP vs TCP | 第06章 TCP/IP | UDP 解决了 TCP 的队头阻塞和拥塞控制问题 |
| QUIC | 第08章 HTTP/2与HTTP/3 | HTTP/3 基于 QUIC,解决了 TCP 层的问题 |
| 连接迁移 | 第09章 网络安全 | QUIC 内置 TLS 1.3,连接迁移不影响加密 |
| 可靠传输 | 第10章 实战篇 | 游戏网络同步需要自定义可靠传输 |
上一章:06-TCPIP协议栈深度 | 下一章:08-HTTP2与HTTP3