找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

6093

积分

0

好友

779

主题
发表于 前天 21:06 | 查看: 0| 回复: 0

本文基准: 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 实现了 ConnPacketConn 接口,并提供:

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 网络编程或系统设计感兴趣,欢迎来云栈社区交流,那里有更多开发者在讨论这些话题。




上一篇:深入理解TCP协议:从可靠字节流到Go网络编程实战
下一篇:Socket 是什么?Go 语言 net.Dial 如何连接 TCP/IP 网络
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-13 18:08 , Processed in 1.691486 second(s), 46 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表