本文基准: Go 1.26.5 本文所有 Go 示例均以 Go 1.26.5 为基准。Go 1.26.5 于 2026 年 7 月 7 日发布,包含 net 包及其他组件的安全修复与缺陷修复。需要特别说明的是:本文讨论的是 Go 1.26.5,而不是后来发布的 Go 1.27。
很多程序员第一次接触 UDP 时,都会形成一个非常自然的印象:
TCP 很可靠,但比较重。 UDP 不可靠,但比较快。
这句话不能说完全错。
但如果我们把它当成结论,而不是起点,就会很快走进一个误区:
UDP 不是“更快的 TCP”。
它从一开始就没有打算成为 TCP 的替代品。
UDP 真正的价值,是把传输层中大量“必须由所有应用承担的能力”拿掉,让应用自己决定:
到底需要可靠性到什么程度?
需要严格有序吗?
需要重传吗?
需要拥塞控制吗?
需要去重吗?
还是说:
丢掉一个包,比等待它重新发送更划算?
这才是 UDP 真正有意思的地方。
1980 年发布的 RFC 768 对 UDP 的定位就非常直接:它提供一种最小机制的数据报通信方式,不保证投递,也不保证重复保护;如果应用需要有序、可靠的数据流,应当使用 TCP。
所以今天我们真正应该研究的问题,不是:
“UDP 为什么比 TCP 快?”
而是:
为什么现代网络系统有时候宁愿接受丢包,也不愿意接受等待?
一、先从一个真实系统的问题开始
想象这样一个服务器。
它负责实时传输游戏状态。
客户端每 20ms 向服务器发送一次自己的位置:
Player X = 102
Player Y = 51
Rotation = 37°
Timestamp = 18:32:41.120
下一次:
Player X = 104
Player Y = 52
Rotation = 39°
Timestamp = 18:32:41.140
再下一次:
Player X = 107
Player Y = 54
Rotation = 42°
Timestamp = 18:32:41.160
现在网络发生了一点问题。
第二个包丢了。
如果使用 TCP:
Packet 1 ✓
Packet 2 ✗
Packet 3 →
Packet 4 →
TCP 会维护可靠、有序的字节流语义。
应用看到的并不是:
1
3
4
而是:
1
等待 2
等待 2
等待 2
2
3
4
对于文件传输来说,这非常合理。
因为文件:
A B C D E F
丢掉 C 以后:
A B D E F
通常是没有意义的。
但对于实时位置:
位置 100
位置 101
位置 102
位置 103
如果:
位置 101
丢掉了,而:
位置 102
已经到了,那么应用很可能根本不想要 101。
因为:
102 已经比 101 更新。
此时真正的问题就出现了。
TCP 解决的问题是:
“我怎样保证这些数据最终正确、完整、有序地到达?”
而实时系统经常需要解决的是:
“我怎样尽可能快地拿到当前有价值的数据?”
这两个问题根本不是同一个问题。
二、网络通信真正困难的地方:可靠性是有成本的
所谓“可靠”,不是一个免费属性。
你要求:
不丢包
↓
有序
↓
重传
↓
确认
↓
流量控制
↓
拥塞控制
↓
连接状态
每增加一个能力,就意味着协议需要维护更多状态。
于是我们可以看到一个非常重要的工程规律:
可靠性 ↑
协议状态 ↑
复杂度 ↑
控制成本 ↑
反过来:
协议机制 ↓
应用自由度 ↑
状态管理 ↓
UDP 的核心思想恰恰是后者。
它只提供最基本的传输能力:
应用数据
↓
UDP
↓
IP
↓
网络
UDP 本身不替你建立 TCP 那样的可靠字节流。
RFC 768 甚至直接把它描述为一种“minimum protocol mechanism”的数据报服务。它的目标从设计之初就是减少协议机制,而不是增加协议能力。
这也是理解 UDP 的第一把钥匙:
UDP 不是能力不足,而是主动少做事情。
三、UDP 到底是什么
UDP 全称:
User Datagram Protocol
用户数据报协议。
它工作在:
Application
↓
UDP
↓
IP
↓
Link
一份 UDP 报文的核心头部非常简单:
+-----------------------+
| Source Port |
+-----------------------+
| Destination Port |
+-----------------------+
| Length |
+-----------------------+
| Checksum |
+-----------------------+
| Data |
+-----------------------+
RFC 768 定义的基本 UDP 头部就是:
Source Port
Destination Port
Length
Checksum
之后才是用户数据。
这和 TCP 的设计哲学形成了非常鲜明的对比。
TCP 需要围绕:
序列号
确认
窗口
状态机
重传
拥塞控制
构建一个可靠字节流。
UDP 则更接近:
我要发一个 Datagram。
给你。
剩下的事情,
你自己决定。
四、为什么 UDP 叫“数据报”而不是“字节流”
这是 UDP 最重要的概念之一。
TCP 给应用的是:
Byte Stream
UDP 给应用的是:
Datagram
比如发送:
HELLO
再发送:
WORLD
TCP 的接收端最终可能看到:
HELLOWORLD
也可能:
HEL
LOWORLD
也可能:
HELLOW
ORLD
TCP 关心的是:
一条连续的字节流。
UDP 则保留消息边界:
Datagram 1 = HELLO
Datagram 2 = WORLD
接收端读取的是:
HELLO
然后:
WORLD
这是非常重要的区别。
因为在很多实时协议中:
消息本身就是一个完整的状态单元。
例如:
PlayerState
AudioFrame
DNS Query
Telemetry
Heartbeat
这些数据天然适合 Datagram。
五、UDP 真正“快”在哪里
我们现在可以重新讨论那个经典问题:
UDP 为什么经常被认为更快?
不是因为:
UDP = 魔法
而是因为它少做了很多事情。
比如 TCP 为可靠传输维护:
Sequence Number
ACK
Retransmission
Flow Control
Congestion Control
Connection State
UDP 基本协议机制则非常有限。
所以在一个非常粗略的抽象模型中:
TCP
Application
↓
TCP State
↓
Sequence
↓
ACK
↓
Retransmission
↓
Congestion Control
↓
Socket
↓
IP
UDP
Application
↓
Datagram
↓
Socket
↓
IP
UDP 的路径更简单。
但这里必须纠正一个非常常见的错误:
UDP 更简单,不意味着 UDP 在所有场景都比 TCP 更快。
真实性能取决于:
协议
+
实现
+
网络状况
+
应用算法
+
数据大小
+
丢包率
+
拥塞
+
CPU
+
内存
+
系统调用
甚至一个设计糟糕的 UDP 协议,完全可能比 TCP 更慢。
因为:
可靠性如果被应用层自己重新实现,UDP 只是把工作搬到了应用层。
六、UDP 最大的代价:可靠性消失以后,问题并没有消失
UDP 最大的误解就是:
“UDP 不可靠,所以我们不用管。”
恰恰相反。
UDP 不负责:
保证到达
保证顺序
保证不重复
RFC 768 对这一点写得非常明确:UDP 不提供 delivery guarantee,也不提供 duplicate protection。
因此应用可能遇到:
Packet 1
Packet 2
Packet 4
Packet 3
Packet 4
也可能:
Packet 1
Packet 3
Packet 4
甚至:
Packet 1
Packet 1
Packet 2
Packet 5
所以应用层必须决定:
我要不要排序?
我要不要重传?
我要不要 ACK?
我要不要去重?
我要不要超时?
我要不要丢弃旧消息?
这就是 UDP 最有价值,也最危险的地方。
七、于是产生了一种现代网络工程思想
很多人认为:
UDP
↓
不可靠
于是:
TCP
↓
可靠
好像只有两个选择。
实际上现代网络工程不是这么玩的。
真正的设计空间是:
完全不可靠
↓
部分可靠
↓
应用级可靠
↓
可靠传输
例如一个实时游戏状态协议:
Sequence
Timestamp
Position
Rotation
Velocity
服务器可以设计:
if sequence <= lastSequence {
discard
}
那么旧数据自动丢弃。
这比 TCP 等待旧包更加符合实时系统的目标。
八、最简单的 UDP 程序
到了 Day 63,我们终于可以直接进入 Go。
Go 标准库中的 net 包提供了完整的 UDP 支持。
UDPConn 实现了 Conn 和 PacketConn 接口,并提供:
Read
ReadFrom
ReadFromUDP
ReadFromUDPAddrPort
Write
WriteTo
WriteToUDP
WriteToUDPAddrPort
等方法。
我们先写一个最简单的 UDP Server。
Server
package main
import (
"fmt"
"log"
"net"
)
func main() {
addr := &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 9000,
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
fmt.Println("UDP server listening on", addr)
buf := make([]byte, 1500)
for {
n, clientAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Println("read error:", err)
continue
}
fmt.Printf(
"from %s: %s\n",
clientAddr,
string(buf[:n]),
)
_, err = conn.WriteToUDP(buf[:n], clientAddr)
if err != nil {
log.Println("write error:", err)
}
}
}
客户端:
package main
import (
"fmt"
"log"
"net"
)
func main() {
serverAddr, err := net.ResolveUDPAddr(
"udp",
"127.0.0.1:9000",
)
if err != nil {
log.Fatal(err)
}
conn, err := net.DialUDP(
"udp",
nil,
serverAddr,
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
message := []byte("hello udp")
_, err = conn.Write(message)
if err != nil {
log.Fatal(err)
}
buf := make([]byte, 1500)
n, err := conn.Read(buf)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(buf[:n]))
}
运行:
go version
应该使用:
go1.26.5
然后:
go run server.go
另一个终端:
go run client.go
你会看到:
hello udp
这段程序已经完成了一次完整的 UDP:
Client
|
| UDP Datagram
v
Server
|
| UDP Datagram
v
Client
但它还有一个非常重要的问题。
它什么可靠性都没有。
九、Go 的 UDP API 背后到底发生了什么
这一点非常值得理解。
当我们调用:
conn.ReadFromUDP(buf)
我们以为发生的是:
Go函数
↓
读取数据
实际上远比这复杂。
抽象出来:
Go Application
|
v
net
|
v
internal/poll
|
v
Go Runtime netpoll
|
v
OS Socket
|
v
Kernel Network Stack
|
v
NIC
Go 源码中的 UDPConn.ReadFromUDP 并没有自己实现一个完整的网络栈,它最终会继续进入底层 FD 的读取逻辑;当前 Go 源码里可以看到 IPv4 / IPv6 分别通过底层 readFromInet4 / readFromInet6 完成接收。
而 Go 的 internal/poll 层负责把 FD 纳入 runtime 的网络轮询机制。其 FD 提供的 ReadFrom 最终对应底层的 recvfrom 网络操作。
再往下一层,就是 Go Runtime 的 netpoll。
Runtime 会等待文件描述符进入可读或可写状态;源码中的 runtime_pollWait 明确承担了等待 FD 就绪的职责,并通过网络轮询机制阻塞和唤醒等待中的 goroutine。
所以:
n, addr, err := conn.ReadFromUDP(buf)
绝不是:
函数直接读取网卡
而是:
你的 Goroutine
↓
net
↓
internal/poll
↓
runtime netpoll
↓
OS kernel
↓
socket
↓
network
这就是我们前面学习网络编程时非常重要的一条线:
Go 并没有消灭操作系统,它把操作系统能力包装成了更适合 Go 程序员使用的抽象。
十、这也是为什么 Go 很适合做网络服务器
假设服务器有:
10,000 个 UDP 客户端
你可能会直觉地认为:
10,000 clients
=
10,000 OS threads
并不是。
Go 的网络 IO 和 Runtime netpoll 协作,使一个阻塞式 API 的编程模型可以和事件驱动底层结合。
在应用层,你可以继续写:
for {
n, addr, err := conn.ReadFromUDP(buf)
...
}
而不是自己直接处理:
epoll
kqueue
select
poll
Linux 自己提供的 epoll 本质上就是一种 I/O 事件通知机制,用于监听多个文件描述符是否已经可以进行 IO;对于数据报类型的文件描述符,在边沿触发等场景下需要持续读取直到 EAGAIN。
Go 将这种 OS 层机制进一步封装进 Runtime 网络轮询器中。
于是:
业务代码
可以保持:
ReadFromUDP()
而:
Runtime
负责:
什么时候真正等待
什么时候唤醒
哪个 goroutine 可以继续执行
这就是 Go 网络编程非常重要的工程价值。
十一、UDP 的“连接”到底是什么
很多初学者看到:
net.DialUDP(...)
会问:
UDP 不是无连接吗?
这里容易误解。
UDP 协议本身没有 TCP 那种连接建立过程。
也就是说:
UDP
不会经历:
SYN
SYN-ACK
ACK
但是操作系统的 UDP socket 可以记录一个默认的远端地址。
于是:
conn, err := net.DialUDP(...)
之后就可以直接:
conn.Write(...)
conn.Read(...)
从应用编程角度看更加方便。
但这不意味着 UDP 获得了 TCP 的可靠连接语义。
这是:
Socket API 的状态
而不是:
UDP 协议建立了可靠连接
十二、UDP 最大的工程问题:MTU
现在进入一个很多入门文章不会认真讲的问题。
你有一个 UDP:
payload = 100 KB
然后:
conn.WriteToUDP(bigPacket, addr)
你不能因为代码写成功了,就认为网络也会完整传输一个 100 KB 的“逻辑消息”。
网络路径中存在 MTU。
例如某段链路可能只能有效处理更小的 IP 数据包。
一旦报文过大,就涉及:
fragmentation
这会带来:
更多丢包风险
更复杂的重组
更高资源消耗
Linux UDP 文档也明确指出,UDP 是无连接、不可靠的数据报服务,数据包可能乱序或重复,并讨论了超过接口 MTU 时的分片行为。
所以工程上非常重要的一条原则是:
UDP 不应该被理解成“随便扔一个巨大数据包”。
优秀的 UDP 协议设计往往更加关注:
Packet Size
MTU
Fragmentation
Reassembly
Loss
Retry
十三、自己给 UDP 加上可靠性
现在我们开始真正进入工程设计。
假设我们需要:
可靠 UDP
最简单的做法是增加:
Sequence Number
ACK
Timeout
Retransmission
例如定义一个自己的消息格式:
+------------+
| Version |
+------------+
| Type |
+------------+
| Sequence |
+------------+
| Timestamp |
+------------+
| Length |
+------------+
| Payload |
+------------+
客户端发送:
SEQ = 100
服务器返回:
ACK = 100
如果 200ms 内没有 ACK:
retry
于是:
UDP
+
Sequence
+
ACK
+
Timeout
+
Retransmission
我们就开始拥有一个非常基础的可靠协议。
十四、但是问题来了
既然可以这样做:
那为什么不直接使用 TCP?
这才是 UDP 真正有价值的地方。
因为:
我们不一定需要 TCP 的全部可靠性语义。
例如:
实时位置
可能只需要:
最新状态
而不需要:
所有历史状态
那么协议可以设计:
SEQ 100
SEQ 101
SEQ 102
SEQ 103
服务器突然收到:
102
103
直接接受。
甚至:
101
后来才到:
discard
因为:
101 < 103
它已经没有价值。
这就是实时系统和可靠字节流之间的本质区别。
十五、可靠性不是“开关”,而是设计空间
一个成熟的协议不会简单地问:
UDP 可靠吗?
而会问:
这个业务需要什么可靠性?
比如:
| 场景 |
是否需要可靠 |
是否需要有序 |
| 文件传输 |
高 |
高 |
| 数据库复制 |
高 |
高 |
| 登录请求 |
高 |
高 |
| 游戏位置 |
低~中 |
通常不需要严格历史顺序 |
| 实时音频 |
低 |
通常更关心连续性 |
| 视频实时帧 |
低~中 |
允许丢旧数据 |
| 心跳 |
低 |
通常最新即可 |
| DNS 查询 |
根据协议 |
请求/响应模型 |
| 遥测 |
视场景 |
通常允许少量丢失 |
所以不存在:
UDP = 不可靠 = 不适合生产
这种结论。
现实恰恰相反。
UDP 长期被用于 DNS、实时媒体等数据报场景,而且现代传输协议甚至可以直接建立在 UDP 之上。RFC 768 最初就列举了 Name Server 与 TFTP 等应用。
十六、一个更有意思的例子:实时音视频
假设:
Video Frame 100
传输失败。
服务器需要重新发送。
但是重新发送花费:
50ms
这时:
Frame 101
Frame 102
Frame 103
已经出现了。
那么:
Frame 100
还值得重新传吗?
很多实时系统的答案是:
不值得。
因为实时系统更关心:
Now
而不是:
History
于是协议可能选择:
丢掉 100
继续传 101
继续传 102
继续传 103
这正是 UDP 的设计空间。
十七、这也解释了 QUIC 为什么建立在 UDP 之上
如果 UDP 只是“又快又不可靠”,那么现代协议为什么还会在 UDP 之上构建如此复杂的传输协议?
答案非常简单:
UDP 提供了一个更灵活的基础。
QUIC 就是一个非常典型的例子。
RFC 9000 定义的 QUIC 是一种基于 UDP 的多路复用、安全传输协议,它提供流控、低延迟连接建立、路径迁移以及安全能力;丢包检测和拥塞控制等机制由 QUIC 体系负责,而不是依赖 UDP 本身。
于是结构变成:
Application
↓
QUIC
↓
UDP
↓
IP
也就是说:
UDP 并不试图把所有问题解决掉。
它提供一个相对薄的基础。
上层协议再根据需求决定:
我要不要可靠?
我要不要多路复用?
我要不要 TLS?
我要不要拥塞控制?
我要不要连接迁移?
这个思想非常重要。
十八、甚至 UDP 本身也在继续演进
一个很有意思的事实是:
UDP 并不是几十年前定义完成以后就彻底结束了。
2025 年发布的 RFC 9868 对 RFC 768 进行了更新,为 UDP 定义了传输层 Options 的扩展空间,用于支持诸如分片、最大数据报大小、附加校验等能力,同时仍然保持 UDP 的基本状态模型。
这件事本身非常值得思考。
因为它再次说明:
简单并不等于停止演进。
UDP 的核心设计仍然保持:
少状态
低机制
数据报
然后在必要时增加可扩展能力。
十九、UDP、TCP、QUIC 到底该怎么选
可以建立这样一个判断框架:
需要可靠、有序字节流?
|
+---------+---------+
| |
YES NO
| |
TCP 是否需要现代传输能力?
|
+----------+----------+
| |
YES NO
| |
QUIC UDP
当然,真正的生产架构远比这张图复杂。
但这个思维方式很重要:
不是:
TCP vs UDP
而是:
我要什么传输语义?
二十、C / C++ / Java / Rust / Go 怎么看 UDP
UDP 是一个非常适合观察不同语言工程哲学的案例。
C
优点:
最直接
控制力强
系统调用边界清晰
你可以直接面对:
socket()
bind()
sendto()
recvfrom()
但代价也明显:
内存管理
生命周期
错误处理
并发
线程安全
全部需要工程师自己负责。
二十一、C++
C++ 可以进一步构建:
Socket Object
Buffer
RAII
Protocol Class
Concurrency
但工程规模越来越大以后,抽象本身也会变得复杂。
二十二、Java
Java 的优势来自:
成熟 Runtime
GC
丰富网络 API
成熟线程与并发模型
但是高性能网络服务越来越依赖:
异步 IO
Netty
事件循环
Buffer 管理
应用层工程会明显变复杂。
二十三、Rust
Rust 最大的优势之一,是能够把:
内存安全
生命周期
并发安全
低层控制
放进编译器检查体系。
但代价仍然存在:
学习成本
类型复杂度
生命周期复杂度
工程设计成本
对于极度重视底层控制的网络系统,这是非常有价值的。
二十四、Go
Go 的思路又不同。
你写:
n, addr, err := conn.ReadFromUDP(buf)
并不需要自己直接处理:
epoll_wait
recvfrom
event loop
thread scheduling
因为:
net
↓
internal/poll
↓
runtime netpoll
已经替你完成了大量系统层协调工作。Go 的网络 FD 可以被标记为由 runtime netpoll 管理,而 internal/poll 再负责等待网络 FD 就绪。
这就是 Go 很典型的工程哲学:
让程序员直接表达网络问题,而不是让程序员先解决操作系统 IO 问题。
二十五、性能分析:UDP 真正应该看什么
不能只看:
发送多少包
更应该看:
Latency
Packet Loss
Jitter
Throughput
CPU
Memory
Syscall
Queue
Buffer
Contention
尤其是实时系统:
Average Latency
甚至还不够。
你还需要看:
P50
P95
P99
P99.9
因为平均值可能很好:
平均 10ms
但实际可能是:
大多数 1~5ms
偶尔:
500ms
对于实时系统来说,这依然可能是灾难。
二十六、UDP 的 CPU 成本从哪里来
UDP 看起来简单,但高 PPS(Packets Per Second)场景下,CPU 压力可能迅速增加。
因为:
Packet
↓
NIC
↓
Kernel
↓
Socket
↓
Runtime
↓
Application
每个包都会产生一定处理成本。
假设:
100 bytes × 1,000,000 packets/s
业务数据量并不巨大。
但是:
1,000,000 packet/s
意味着系统需要处理大量 packet。
所以在高性能 UDP 服务中:
Packet Rate
有时比:
Bytes/s
更值得关注。
二十七、Cache 与内存布局
当你处理:
百万 UDP Packet
问题就不再只是网络。
还会进入 CPU:
L1 Cache
L2 Cache
L3 Cache
Memory
例如我们定义:
type Packet struct {
Sequence uint64
Time int64
Type uint16
Length uint16
Payload []byte
}
如果程序每接收一个包都:
new
allocate
copy
append
GC
那么即使 UDP 本身非常轻量:
Application Layer
也可能把性能拖垮。
所以真正的高性能网络设计经常会进一步考虑:
Buffer Reuse
Object Reuse
Allocation
Escape Analysis
GC
Cache Locality
Batching
到了后面的 Go 性能专题,这些才会真正变成重点。
二十八、Lock Contention
假设一个 UDP Server 有:
100 个 Goroutine
全部往:
map[ClientID]State
里面写。
然后你:
mu.Lock()
...
mu.Unlock()
锁竞争可能成为瓶颈。
于是 UDP:
网络很快
但应用:
锁很慢
最终系统仍然很慢。
所以不要把:
UDP
和:
高性能
画上等号。
真正的系统性能是:
Protocol
×
Kernel
×
Runtime
×
Application
×
CPU
×
Memory
×
Architecture
任何一个环节都可能成为瓶颈。
二十九、一个真正实用的 Go UDP 协议
我们可以尝试写一个简单的可靠层。
例如:
package protocol
import "encoding/binary"
const HeaderSize = 16
type Header struct {
Version uint8
Type uint8
Flags uint16
Sequence uint64
Timestamp uint32
}
编码:
func EncodeHeader(h Header, buf []byte) {
buf[0] = h.Version
buf[1] = h.Type
binary.BigEndian.PutUint16(buf[2:4], h.Flags)
binary.BigEndian.PutUint64(buf[4:12], h.Sequence)
binary.BigEndian.PutUint32(buf[12:16], h.Timestamp)
}
解析:
func DecodeHeader(buf []byte) Header {
return Header{
Version: buf[0],
Type: buf[1],
Flags: binary.BigEndian.Uint16(buf[2:4]),
Sequence: binary.BigEndian.Uint64(buf[4:12]),
Timestamp: binary.BigEndian.Uint32(buf[12:16]),
}
}
然后:
UDP Packet
│
├── Header
│ ├── Version
│ ├── Type
│ ├── Flags
│ ├── Sequence
│ └── Timestamp
│
└── Payload
此时我们已经开始从:
“使用 UDP”
进入:
“设计一个基于 UDP 的协议”
这就是网络工程真正的分水岭。
三十、不要轻易自己造 TCP
这里必须给一个非常重要的工程建议。
看到 UDP 可以自己实现:
ACK
Sequence
Retry
很多人马上就会产生一个危险想法:
“那我自己实现一个 TCP 不就行了?”
理论上可以。
工程上通常不应该这么做。
因为真正的可靠传输远远不只有:
ACK
还涉及:
Retransmission
RTT
RTO
Sliding Window
Flow Control
Congestion Control
Fast Retransmit
Loss Detection
Fairness
Reordering
Buffer Management
Path MTU
尤其是:
拥塞控制。
你不能只考虑:
我的数据怎样更快发出去?
还必须考虑:
整个网络发生拥塞时,我应该怎样退让?
否则:
发送端不断加速
最后得到的不是:
更快
而是:
拥塞
丢包
重传
延迟
雪崩
三十一、UDP 最大的优点其实是“选择权”
现在回头看标题:
UDP协议:速度与可靠性的选择
我们会发现:
“速度”和“可靠性”其实并不是简单的二选一。
真正的工程问题是:
哪些可靠性我需要?
哪些可靠性我不需要?
哪些数据必须重传?
哪些数据可以丢?
哪些数据必须有序?
哪些数据到晚了就没有价值?
这才是 UDP 带来的核心价值。
它把决定权更多地交给了应用。
三十二、UDP 的工程边界在哪里
UDP 非常适合:
实时通信
游戏
音视频
DNS
遥测
广播/组播场景
自定义传输协议
高频状态同步
但并不意味着所有东西都应该使用 UDP。
例如:
数据库事务
文件传输
订单系统
支付
配置同步
可靠数据复制
这些场景通常更需要:
可靠
有序
完整
而不是:
尽可能及时
所以判断 UDP 是否合适时,最应该问的不是:
UDP 快不快?
而应该问:
这个业务允许丢什么?
这可能是网络工程里最重要的一个问题。
三十三、Go 1.26.5 下的一个重要观察
Go 1.26 的 net 包本身没有因为版本更新而改变 UDP 的基本编程模型。
你仍然可以使用:
net.ListenUDP
net.DialUDP
UDPConn.ReadFromUDP
UDPConn.WriteToUDP
以及现代 netip 地址接口:
ReadFromUDPAddrPort
WriteToUDPAddrPort
官方 Go 1.26.5 的 net 包文档明确保留这些 UDP API。
这其实说明了 Go 标准库一个非常重要的特点:
API 足够稳定,但底层实现可以持续演进。
今天你写的是:
conn.ReadFromUDP(...)
底层却可能经过:
net
↓
internal/poll
↓
runtime netpoll
↓
OS
不断优化。
这就是稳定接口与复杂实现之间的价值。
三十四、最终重新理解 TCP 与 UDP
我们可以用一句话概括。
TCP:
我替你承担可靠传输的复杂性。
UDP:
我给你最小的传输基础,你决定需要什么。
因此:
TCP
=
成熟的可靠传输抽象
而:
UDP
=
可自由构建的传输基础
这也是为什么 UDP 从 1980 年一直活到了今天。
它并没有因为 TCP 出现而消失。
反而在:
实时通信
游戏
音视频
QUIC
现代互联网传输
中继续成为重要基础。
三十五、真正应该记住的不是“UDP更快”
如果 Day 62 学 TCP,我们会看到:
可靠性
是如何通过大量协议机制建立起来的。
到了 Day 63,UDP 应该让我们看到另一面:
如果把这些机制拿掉,
系统会发生什么?
答案不是:
网络突然变快。
而是:
自由度提高了。
你可以:
接受丢包
可以:
选择性重传
可以:
只接受最新状态
可以:
允许乱序
可以:
自己设计拥塞控制
甚至可以在 UDP 之上构建:
QUIC
或者自己的专用传输协议。
现代网络工程真正有价值的能力,并不是知道:
TCP = 可靠
UDP = 不可靠
而是能够面对一个具体业务,判断:
我要什么语义?
我要承担什么成本?
我要把复杂度放在哪里?
三十六、一个更大的问题
到了这里,我们其实已经发现了一个非常重要的系统设计规律:
软件工程中的“简单”,往往不是把问题消灭,而是把决定权交给更合适的一层。
TCP 选择:
传输层负责更多。
UDP 选择:
传输层负责更少。
QUIC 则选择:
在 UDP 之上重新构建一套现代传输能力。
这三种设计并没有谁天然更先进。
它们只是:
复杂度放置的位置不同。
而这恰恰是架构设计中最容易被忽视的一件事情。
当系统从一台机器扩展到一万台服务器,
当网络从局域网进入公网,
当客户端从几个增长到几百万,
真正困难的问题就不再是:
“怎样把数据发出去?”
而变成:
“哪些事情应该由操作系统负责,哪些事情应该由 Runtime 负责,哪些事情应该由传输协议负责,哪些事情最终必须由业务自己决定?”
UDP 留给我们的真正答案,也许不是“快”。
而是:
不要替所有应用做同一个决定。
当一份数据晚到已经没有意义时,可靠性可能是一种负担。
当一份数据绝对不能丢时,低机制反而会成为代价。
所以网络协议真正要解决的,从来不是:
快 vs 慢
也不是:
可靠 vs 不可靠
而是:
什么样的系统,
应该承担什么样的成本。
而 UDP 最有价值的地方,就是把这个问题重新交回给了工程师。
如果你对 UDP、Go 网络编程或系统设计感兴趣,欢迎来云栈社区交流,那里有更多开发者在讨论这些话题。