# 第 48 章实验:UDP 通信与"可靠通信层" 本目录有两组程序,都只在本机(localhost)上运行,不需要第二台机器。 | 文件 | 内容 | |------|------| | `udp.h` / `udp.c` | 原书 Figure 48.2 的简单 UDP 库:`UDP_Open`、`UDP_FillSockAddr`、`UDP_Write`、`UDP_Read` | | `server.c` / `client.c` | 原书 Figure 48.1:客户端发 `hello world`,服务端回 `goodbye world` | | `reliable.c` | 在 UDP 上实现 **ack + 超时/重传 + 序列号**,并人为按概率丢包,观察重传和重复消息 | ## 编译与运行 ```bash cd labs/48-distributed make # 编译 server、client、reliable make run # 后台启动 ./server 1(处理 1 条消息后退出),再运行 ./client localhost ``` 也可以开两个终端分别运行 `./server` 和 `./client`。 ### 可靠通信实验 ```bash ./reliable -p 0.3 -m 5 -s 1 # 丢包率 30%,发 5 条"存款"消息,带序列号 ./reliable -p 0.3 -m 5 -s 1 -n # 同样的丢包情况,但不用序列号 ./reliable -p 0 -m 5 # 不丢包:每条消息恰好发一次 ./reliable -p 0.5 -t 20 -m 10 -s 7 # 丢包更多、超时更短 ``` 参数:`-p` 丢包率(0~1),`-t` 超时时间(毫秒,默认 100),`-m` 消息条数,`-s` 随机种子,`-n` 关闭序列号。 程序会 `fork` 出一个接收方进程。接收方每收到一条存款消息就执行 `balance += 100`, 最后打印余额。**存款不是幂等操作**:同一条消息执行两次,余额就多了 100。 ## 应该观察到什么 1. `make run`:客户端发出 `hello world`,服务端从 `127.0.0.1:20000` 收到并回复。 注意 `client.c` 里的 `UDP_Read` 会一直阻塞——如果请求或回复在网络里丢了,客户端会永远等下去。 这就是"不可靠通信层":UDP 丢包时不会通知发送方。 2. `./reliable -p 0.3 -s 1`(带序列号): - 消息丢失 → 发送方等到 `TIMEOUT` 后重传(原书 Figure 48.4)。 - ACK 丢失 → 发送方同样超时重传,接收方收到 `seq` 小于期望值的消息,识别为 `DUPLICATE`, **仍然回 ACK,但不交给应用**(原书 Figure 48.5 + 序列号)。 - 最终余额正确:`final balance = 500 (expected 500) OK`。 3. 同样的种子加 `-n`(不带序列号):ACK 丢失后重传的消息被接收方当成新消息再执行一次, 余额变成 700,程序会打印 `WRONG! duplicate executed`。 4. 把超时调得比往返时间还短:`./reliable -p 0 -t 0 -m 3`(或 `-t 1`)。即使**一个包都没丢**, 发送方也会在 ack 回来之前一再超时重传;接收方收到一堆 `DUPLICATE`(作者机器上 `-t 1 -m 5` 时 `duplicates discarded=13, acks sent=18`)。因为有序列号,余额依然正确——超时太小浪费的是网络和 CPU, 这就是原书"小心设置超时值"提示所说的。再加上 `-n` 试试会怎样。 ## 为什么 - 丢包在网络中是**根本性**的(缓冲区满了,路由器/主机只能丢包),所以可靠性必须靠端点上的 ack + 超时/重传来补。 - 仅有超时/重传会导致**重复**:发送方无法区分"请求丢了"和"回复丢了"。 - 序列号(sequence counter)只需要两端各保存一个计数器,就能让接收方识别重复,做到"恰好一次"交付。 注:真实网络中的 TCP 还包括拥塞控制、滑动窗口(多个未确认的消息同时在途)等机制; 这里是最简单的"停止等待"(stop-and-wait)协议,每次只有一条消息在途。