bc's club

This is Bc's club

计算机网络:自顶向下方法 - Day 7 | UDP 协议详解与可靠数据传输原理

Day 7 — UDP 协议详解与可靠数据传输原理#

前情回顾:Day 6 我们聊了可靠数据传输的”甲方”——网络层只提供”尽力而为”的服务,丢包、乱序、比特出错全不管。那传输层怎么办?今天我们就来认识两位选手:一位是”摆烂大师” UDP,另一位是”完美主义者”——可靠数据传输协议(rdt 系列模型)。


一、UDP:我就主打一个”信不信由你”#

1.1 UDP 是什么?#

UDP(User Datagram Protocol,用户数据报协议),是传输层最朴素的协议。它做的事儿少到令人发指:

  • 多路复用/多路分解:把数据送到正确的应用进程(通过端口号)
  • 差错检测:可选的校验和(checksum)
  • 没了

就这两件事。不保证交付,不保证顺序,不保证不出错。你发出去的数据包,它生不生死不死,UDP 拒不负责。

🎯 生活类比:UDP 就像街头发的传单。发的人(发送方)往街上一撒就走了,你收到没收到?传单被风吹走了?发的人根本不在乎。而 TCP 像是挂号信,必须签收、必须确认。

1.2 UDP 数据报首部格式#

UDP 首部只有 8 个字节,小得可怜:

1
2
3
4
5
6
7
8
 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
| 源端口 | 目的端口 |
+--------+--------+--------+--------+
| 长度 | 校验和 |
+--------+--------+--------+--------+
| 数据(可选) |
+----------------------------------+
字段 大小 说明
源端口 2 字节 发送方端口号(可选,全 0 表示不需要回复)
目的端口 2 字节 接收方端口号
长度 2 字节 整个 UDP 数据报(首部+数据)的字节数
校验和 2 字节 差错检测(IPv4 中可选,IPv6 中必须)

就这么简单。首部开销 8 字节,相比之下 TCP 首部 20 字节起步,UDP 省下来的带宽在高速场景下相当可观。

1.3 UDP 校验和#

虽然 UDP “摆烂”,但它还是提供了一个校验和机制——这就像摆烂的人出门前还是看了一眼天气。

原理:发送方将数据按 16 位为一个单位,做反码求和(one’s complement sum),结果取反得到校验和。接收方收到后,把所有 16 位数据和校验和一起做反码求和,如果全为 1,说明没出错。

注意:校验和只能检测错误,不能纠正错误。发现错误后怎么办?直接丢掉。没错,UDP 的纠错策略就是——

🎯 生活类比:校验和就像快递箱上的”易碎品”贴纸。快递员(网络)看到这个标签,如果发现箱子破了,他不会帮你修补,而是直接扔掉。至于你知不知道快递丢了……那不是快递员的事。

1.4 为什么应用层要”选择”UDP?#

既然 UDP 这么不靠谱,为什么还有应用要用它?因为”不靠谱”换来了”高效率”:

优势 说明
无连接 不需要握手,想发就发
低开销 8 字节首部,省带宽
无拥塞控制 发送速率不受网络拥塞影响
实时性好 延迟低,适合实时应用

常见使用 UDP 的应用:

  • DNS:查询就一个请求一个响应,握手纯属浪费时间
  • SNMP:网络管理,偶尔丢个数据包无所谓
  • 流媒体 / 视频会议:实时性 > 可靠性,丢几帧画面可以接受
  • 游戏:你不想因为一个丢包就让整个游戏卡住等重传吧?
  • QUIC:HTTP/3 的底层协议,在 UDP 上构建了自己的可靠性(取 UDP 之长,补 UDP 之短)

1.5 实战:DNS 查询中的 UDP#

在你的实际系统中,每次你打开一个网页、解析一个域名,背后都是 UDP 在工作。来看一个真实的 DNS 查询:

1
2
3
4
5
你的电脑 → DNS服务器:UDP报文(目的端口 53)
"哥们,bilibili.com 的 IP 是多少?"

DNS服务器 → 你的电脑:UDP报文(源端口 53)
"61.135.169.125,拿去"

整个过程没有握手,没有建立连接,一发一收,干净利落。如果丢了?DNS 客户端会超时重试(这是应用层的事,UDP 不管)。

你可以用 tcpdump 或 Wireshark 实际抓包看看:

1
sudo tcpdump -i any port 53 -n

你会看到所有 DNS 查询都是 UDP,快速且高效。

💡 思考题:为什么 DNS 不用 TCP?答案:DNS 查询通常很小(一个请求一个响应),建立 TCP 连接的三次握手开销太大。UDP 直接发出去,效率高得多。不过当 DNS 响应数据超过 512 字节时,会切换到 TCP(通过 EDNS0 或区域传输)。

1.6 UDP 的可靠性补救方案#

UDP 本身不可靠,但很多应用在 UDP 上构建了自己的可靠性机制:

应用 可靠性策略
QUIC 在 UDP 上实现了类似 TCP 的可靠性 + 加密
TFTP 停等协议,每个数据包都要 ACK
你的 frp UDP 心跳 + 超时重连机制

这就是 UDP 的精髓——它给你最大的自由度,可靠性你自己加


二、可靠数据传输原理:从零造一个可靠协议#

现在进入今天的重头戏。网络层(IP 层)是不可靠的:数据包可能丢失、损坏、乱序。但传输层需要给应用层提供可靠的数据传输。

问题来了:如何在不可靠的信道上实现可靠的数据传输?

这就是 rdt(reliable data transfer)协议系列要解决的问题。教材用了渐进式的方法,一步步从最简单的版本演进到接近 TCP 的模型。

🎯 生活类比:你在网上下单买了个东西,快递可能丢、可能损坏、可能寄错地址。你(发送方)和卖家(接收方)需要一套规则,确保你最终拿到正确的东西——这就需要确认、重传、序号等机制。

2.1 rdt 1.0:完美世界(理想信道)#

假设:底层信道完全可靠——不丢包、不出错、不乱序。

这种情况下,可靠传输简单到离谱:

  • 发送方:rdt_send(data) → 把数据发出去(udt_send(packet)
  • 接收方:rdt_rcv(packet) → 把数据取出来,交给上层

不需要确认,不需要重传,不需要序号。完美!

当然,现实中的网络从来不完美,所以这只是一个起点。

2.2 rdt 2.0:引入 ARQ(有比特错误的信道)#

假设:底层信道可能发生比特错误(0 变成 1),但不会丢包。

这是个更现实的场景。如果数据在传输中出了错,接收方需要能检测到错误(通过校验和),然后告诉发送方重传。这就是 ARQ(Automatic Repeat reQuest,自动重传请求) 协议。

rdt 2.0 引入了三个机制:

  1. 差错检测:校验和
  2. 接收方反馈:ACK(确认正确)或 NAK(确认有错)
  3. 重传:收到 NAK 就重传

发送方状态机

1
2
3
4
5
6
7
8
9
[等待上层数据]
|
| rdt_send(data)
v
[等待ACK/NAK] --收到NAK--> 重传 --> [等待ACK/NAK]
|
| 收到ACK
v
[等待上层数据]

接收方状态机

1
2
3
4
5
6
7
8
9
[等待下方数据]
|
| rdt_rcv(packet) 且没出错
| → 发送ACK,提取数据交给上层
|
| rdt_rcv(packet) 且出错
| → 发送NAK
v
[等待下方数据](继续等下一个)

🎯 生活类比:你网购收到快递,当面拆开检查。东西没问题就说”OK”(ACK);发现坏了就说”退换”(NAK)。卖家收到 NAK 就重新发一个。

rdt 2.0 的致命缺陷#

如果 ACK 或 NAK 本身在传输中出了错怎么办?发送方收到的反馈不可靠!

比如发送方收到一个损坏的 ACK,它不知道这是 ACK 还是 NAK,也不知道接收方到底有没有正确收到数据。

rdt 2.1:加序号#

解决方案:给数据包加序号(0 或 1,交替使用)。这样即使 ACK/NAK 出错,发送方也能通过序号判断:

  • 如果收到对”当前序号”的 ACK → 数据被正确接收,发下一个
  • 如果收到对”上一个序号”的 ACK 或收到 NAK → 重传当前包

发送方状态:

  • 等待上层调用 0(准备发序号 0 的数据)
  • 等待 ACK 0(等序号 0 的确认)
  • 等待上层调用 1
  • 等待 ACK 1

接收方也能通过序号判断:这是新数据(序号变化)还是重传(序号和上次一样)?

🎯 生活类比:卖家发货时在快递上标注”第 1 单”、”第 2 单”。你收到后回复”第 1 单 OK”。即使你的回复在路上被弄模糊了,卖家看到”第 1 单”的字样就知道第一单没问题了。

rdt 2.2:去掉 NAK#

rdt 2.2 是 2.1 的优化版——用 ACK 携带序号代替 NAK

如果接收方发现数据出错,不发 NAK,而是发送上一个正确接收包的 ACK(即”重复 ACK”)。发送方收到重复 ACK 就知道需要重传。

这样做的好处:反馈消息只有一种类型(ACK),简化了协议。

🎯 生活类比:你不再说”这个坏了”(NAK),而是重复说”上一个好的”(重复 ACK)。卖家发现你说了两次”第 1 单 OK”,就知道第 2 单出了问题。

2.3 rdt 3.0:处理丢包(现实世界)#

假设:底层信道不仅会出错,还会丢包

这是最接近真实互联网的情况。如果数据包或 ACK 丢了,发送方会一直等下去,永远收不到回复——这就是死锁

解决方案超时重传

发送方在发送数据后启动一个定时器。如果在超时前没收到 ACK,就认为数据丢了,重传。

rdt 3.0 的发送方状态机:

1
2
3
4
5
6
7
8
9
10
11
[等待上层调用 0]
|
| rdt_send(data),发送序号0的包,启动定时器
v
[等待ACK 0]
| | |
| | +--收到ACK 0--> 停止定时器 --> [等待上层调用 1]
| |
| +--收到ACK 1(或损坏)--> 忽略,继续等
|
+--超时--> 重传序号0的包,重启定时器

这个协议被称为停等协议(Stop-and-Wait)——发送方发一个包,停下来等 ACK,收到确认后再发下一个。

🎯 生活类比:你给朋友发微信,发完之后看表。如果 5 分钟没回,你就重发一遍。虽然原始,但能保证可靠性。

停等协议的性能问题#

停等协议虽然可靠,但效率极低。考虑以下场景:

  • 数据包大小:1 KB
  • 链路带宽:1 Gbps
  • 往返时延(RTT):100 ms

发送一个包的实际传输时间:1KB / 1Gbps ≈ 8 μs
但发送方要等 RTT(100 ms)才能发下一个包。

信道利用率 = 8μs / (8μs + 100ms) ≈ 0.008%

这个效率简直惨不忍睹!就好比你有一辆法拉利(高带宽链路),但每开一米就要停下来等红灯(等 ACK),大部分时间都在等。

这就引出了我们今天最后一个重要话题——流水线协议


三、流水线协议:从”一个一个来”到”批量发送”#

3.1 核心思想#

停等协议的问题在于:发送方只允许”在路上”存在一个未确认的数据包。如果我们允许同时有多个未确认的数据包在路上,就能大幅提高信道利用率。

这就是流水线(pipelining)技术。

流水线需要两个新机制:

  1. 序号范围扩大:不再是 0/1 交替,而是每个包有唯一的序号(比如 0, 1, 2, …, N)
  2. 缓冲:发送方需要缓存已发送但未确认的包(以备重传),接收方可能需要缓存乱序到达的包

🎯 生活类比:你不再是一个一个发快递、收到确认再发下一个。而是一次发出去 10 个快递(流水线),然后等确认。效率直接拉满。

流水线协议有两种经典实现:回退 N 步(GBN)选择重传(SR)

3.2 回退 N 步(Go-Back-N, GBN)#

核心规则#

GBN 允许发送方在不等待确认的情况下连续发送最多 N 个数据包(N 称为”窗口大小”)。但它的容错策略比较粗暴——只要有一个包丢了,从那个包开始后面全都要重传

关键参数

  • 窗口大小 N:最多允许 N 个未确认的包在途
  • 基号 base:最早未确认的包的序号
  • 下一个序号 nextseqnum:下一个要发的包的序号

窗口范围:[base, base + N - 1]

1
2
已确认        窗口内(已发送未确认)    不能发
[0...base-1] [base ... base+N-1] [base+N ...]

发送方行为#

  1. 收到上层新数据

    • 如果 nextseqnum < base + N → 在窗口内,发送数据,nextseqnum++
    • 如果 nextseqnum >= base + N → 窗口满了,拒绝或缓存
  2. 收到 ACK n:GBN 使用累积确认——ACK n 表示”序号 n 及之前的所有包都正确收到了”。收到 ACK n 后,base = n + 1(窗口滑动)

  3. 超时:只对最早的未确认包(base)启动一个定时器。超时后,重传从 base 开始的所有未确认包

接收方行为#

GBN 接收方也很简单:只按序接收

  • 期望序号 expected_seqnum
  • 收到正确序号的包 → 提取数据,发 ACK,expected_seqnum++
  • 收到乱序包 → 直接丢弃,只重发最后一个正确包的 ACK

🎯 生活类比:你在食堂排队打饭。你期望第 1 号、第 2 号、第 3 号菜。如果第 2 号没来直接跳到第 3 号,你拒绝接收第 3 号,让厨房从第 2 号重新做。

GBN 的优缺点#

优点:简单。接收方不需要缓存乱序包。

缺点:浪费严重。如果窗口大小是 100,第 2 个包丢了,后面 98 个包虽然可能已经到达,但全部要重传。在丢包率高的网络中性能急剧下降。

3.3 选择重传(Selective Repeat, SR)#

核心规则#

SR 是 GBN 的优化版——只重传出错的包,不再”株连九族”。

关键设计

  • 发送方和接收方各自维护一个窗口
  • 窗口内的每个包都有独立的定时器
  • 接收方可以缓存乱序到达的包

发送方行为#

  1. 收到上层新数据

    • 窗口未满 → 发送数据,启动该包的定时器
  2. 收到 ACK n

    • 标记包 n 为已确认
    • 如果 n == base → 窗口向前滑动到第一个未确认的包
    • 停止包 n 的定时器
  3. 某个包超时:只重传那一个包,重启该包定时器

接收方行为#

  • 收到期望范围内的包(无论是否按序)→ 缓存,发 ACK
  • 收到窗口外的包 → 发 ACK(确认之前收到过)
  • 当一组连续的包都收到后,一起交给上层,窗口滑动

🎯 生活类比:你网购了 10 件商品。第 2 件丢了,但第 3~10 件正常收到,你先放着。你只让卖家补发第 2 件,收到后你按顺序把所有东西整理好。

SR 的窗口大小约束#

SR 有一个重要的窗口大小约束:

窗口大小 ≤ 序号空间大小 / 2

例如如果序号用 4 位(0~15),窗口大小最大为 8。否则会出现序号歧义——接收方无法区分一个包是新的还是重传的。

3.4 GBN vs SR 对比#

特性 回退 N 步(GBN) 选择重传(SR)
累积确认 ❌(独立确认)
接收方缓存
定时器 1 个 N 个(每包一个)
丢包后重传 base 之后全部 只重传丢失的
实现复杂度 简单 较复杂
高丢包率性能

💡 TCP 的做法:TCP 的可靠传输机制融合了 GBN 和 SR 的思想——使用累积确认(像 GBN),但也支持选择性确认 SACK(像 SR)。这是个”取其精华”的混合方案,Day 8 会详细讲。


四、实战思考:frp UDP 心跳与可靠传输#

你在实际项目中用 frp 做内网穿透,frp 的心跳机制就是一个简单的”应用层可靠传输”实现:

1
2
3
4
5
6
7
8
frpc(客户端)  →  frps(服务端)
| |
| -- UDP 心跳包 --> |
| <-- 心跳回复 --- |
| |
| 超过N次没收到回复 |
| → 认为连接断开 |
| → 触发重连 |

这其实就是 rdt 3.0 的超时重传思想的应用:

  • 定期发送心跳包(数据包)
  • 等待回复(ACK)
  • 连续超时(超时重传阈值)→ 判定连接失效 → 重连

虽然简单,但原理和今天学的可靠数据传输是相通的。


五、关键知识点回顾#

UDP#

知识点 要点
UDP 首部 8 字节:源端口 + 目的端口 + 长度 + 校验和
校验和 反码求和,只能检错不能纠错
无连接 不需要握手,低延迟
适用场景 DNS、流媒体、游戏、QUIC
可靠性补救 应用层自己实现(如 QUIC)

可靠数据传输#

模型 信道假设 关键机制
rdt 1.0 完全可靠 无需额外机制
rdt 2.0 有比特错误 ACK/NAK + 重传
rdt 2.1 有比特错误(含ACK出错) 增加序号(0/1)
rdt 2.2 有比特错误 用重复 ACK 代替 NAK
rdt 3.0 丢包 + 比特错误 超时重传(停等协议)

流水线协议#

协议 策略 确认方式 接收方缓存
GBN base 之后全部重传 累积确认 不需要
SR 只重传出错包 独立确认 需要
TCP 混合方案 累积确认 + SACK

Day 8 预告#

今天我们从 UDP 出发,一路走到了可靠数据传输的理论模型。但 rdt 只是理论模型——真实的互联网上,最广泛使用的可靠传输协议是 TCP

Day 8 我们将深入:

  • 🔸 TCP 首部详解:20 字节里藏了多少玄机?
  • 🔸 TCP 连接管理:三次握手、四次挥手,为什么不是两次?
  • 🔸 TCP 流量控制:发送方怎么知道接收方”吃不下”了?
  • 🔸 TCP 拥塞控制:网络堵了怎么办?慢启动、拥塞避免、快速重传、快速恢复

TCP 是 rdt 理论的集大成者,也是在真实网络环境中最优雅的实用协议。明天见!


📖 读书进度:第 3 章 3.3(UDP)+ 3.4(可靠数据传输原理)

🎯 推荐实验:用 Wireshark 抓一个 DNS 查询,看看 UDP 数据报的结构;再抓一个 TCP 流,感受一下流水线传输。

下篇见 👋