计算机网络:自顶向下方法 - 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 | 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 | 你的电脑 → DNS服务器:UDP报文(目的端口 53) |
整个过程没有握手,没有建立连接,一发一收,干净利落。如果丢了?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 引入了三个机制:
- 差错检测:校验和
- 接收方反馈:ACK(确认正确)或 NAK(确认有错)
- 重传:收到 NAK 就重传
发送方状态机:
1 | [等待上层数据] |
接收方状态机:
1 | [等待下方数据] |
🎯 生活类比:你网购收到快递,当面拆开检查。东西没问题就说”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 | [等待上层调用 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)技术。
流水线需要两个新机制:
- 序号范围扩大:不再是 0/1 交替,而是每个包有唯一的序号(比如 0, 1, 2, …, N)
- 缓冲:发送方需要缓存已发送但未确认的包(以备重传),接收方可能需要缓存乱序到达的包
🎯 生活类比:你不再是一个一个发快递、收到确认再发下一个。而是一次发出去 10 个快递(流水线),然后等确认。效率直接拉满。
流水线协议有两种经典实现:回退 N 步(GBN) 和 选择重传(SR)。
3.2 回退 N 步(Go-Back-N, GBN)#
核心规则#
GBN 允许发送方在不等待确认的情况下连续发送最多 N 个数据包(N 称为”窗口大小”)。但它的容错策略比较粗暴——只要有一个包丢了,从那个包开始后面全都要重传。
关键参数:
- 窗口大小 N:最多允许 N 个未确认的包在途
- 基号 base:最早未确认的包的序号
- 下一个序号 nextseqnum:下一个要发的包的序号
窗口范围:[base, base + N - 1]
1 | 已确认 窗口内(已发送未确认) 不能发 |
发送方行为#
收到上层新数据:
- 如果 nextseqnum < base + N → 在窗口内,发送数据,nextseqnum++
- 如果 nextseqnum >= base + N → 窗口满了,拒绝或缓存
收到 ACK n:GBN 使用累积确认——ACK n 表示”序号 n 及之前的所有包都正确收到了”。收到 ACK n 后,base = n + 1(窗口滑动)
超时:只对最早的未确认包(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 的优化版——只重传出错的包,不再”株连九族”。
关键设计:
- 发送方和接收方各自维护一个窗口
- 窗口内的每个包都有独立的定时器
- 接收方可以缓存乱序到达的包
发送方行为#
收到上层新数据:
- 窗口未满 → 发送数据,启动该包的定时器
收到 ACK n:
- 标记包 n 为已确认
- 如果 n == base → 窗口向前滑动到第一个未确认的包
- 停止包 n 的定时器
某个包超时:只重传那一个包,重启该包定时器
接收方行为#
- 收到期望范围内的包(无论是否按序)→ 缓存,发 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 | frpc(客户端) → frps(服务端) |
这其实就是 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 流,感受一下流水线传输。
下篇见 👋