计算机网络自顶向下方法第九版学习:TCP 网络拥塞控制详解
在之前的章节里,TCP 的拥塞控制完全靠"自己猜"——通过观察丢包、RTT 和吞吐量来判断网络是否拥塞。这种方式叫做端到端拥塞控制。让网络中的路由器直接告诉发送方"我堵了!,而不是等到丢包才知道。ECN 是 IP 和 TCP 的联合扩展,定义在 RFC 3168 中。应用开发者现在有三个选择:选择1:原生 TCP(操作系统提供)- 可靠传输、流量控制、拥塞控制- 更新慢(需要操作系统升级)选择2:
涵盖内容:显式拥塞通知(ECN)、TCP公平性、QUIC协议
3.7.3 网络辅助的显式拥塞通知(ECN)
什么是 ECN?
在之前的章节里,TCP 的拥塞控制完全靠"自己猜"——通过观察丢包、RTT 和吞吐量来判断网络是否拥塞。这种方式叫做端到端拥塞控制。
ECN(Explicit Congestion Notification,显式拥塞通知)是一种更聪明的方式:让网络中的路由器直接告诉发送方"我堵了!",而不是等到丢包才知道。
ECN 是 IP 和 TCP 的联合扩展,定义在 RFC 3168 中。
ECN 如何工作?
ECN 用到了 IP 数据报头部的 Type of Service 字段中的 2 个比特(共 4 种取值):
| ECN 比特值 | 含义 |
|---|---|
00 |
不支持 ECN |
01 或 10 |
发送方声明"我支持 ECN" |
11 |
路由器标记"我正在拥塞!" |
ECN 完整流程图(ASCII 示意)
Host A (发送方) 路由器 Host B (接收方)
| | |
|------- IP 数据报(ECN=01)----->| |
| |(路由器拥塞,把ECN改为11) |
| |------- IP 数据报(ECN=11)->|
| | |
|<------ TCP ACK(ECE=1)------------------------------------|
|(收到ECE,拥塞窗口减半,发送CWR=1) |
| | |
对应图1(Figure 3.54)的流程说明:
- Host A(发送方) 发出 IP 数据报,其中 ECN 比特设为
01或10,表示"我支持 ECN" - 中间路由器 检测到自己拥塞,将 IP 数据报头部的 ECN 比特改为
11 - Host B(接收方) 收到带有 ECN=11 的数据报后,在返回给 Host A 的 TCP ACK 中设置 ECE(Explicit Congestion Notification Echo)比特 = 1
- Host A 收到带有 ECE=1 的 ACK 后:
- 将拥塞窗口减半(和收到丢包信号时的处理一样)
- 在下一个发送的数据包中设置 CWR(Congestion Window Reduced)比特 = 1,告诉接收方"我已经降速了"
ECN 的优势
传统 TCP:
网络拥塞 --> 队列溢出 --> 丢包 --> 发送方超时或收到三个冗余ACK --> 降速
↑
(已经太晚了)
ECN 方式:
网络拥塞 --> 路由器提前标记 ECN=11 --> 接收方回传 ECE=1 --> 发送方提前降速
↑
(在丢包前就处理了!)
ECN 的核心价值:在丢包发生之前就通知发送方降速,避免了队列溢出和丢包的损失。
哪些协议支持 ECN?
- TCP:直接支持(RFC 3168)
- DCCP(数据报拥塞控制协议,RFC 4340):面向消息的 UDP 式协议,使用 ECN
- DCTCP(数据中心 TCP):专为数据中心设计,大量使用 ECN
- DCQCN(数据中心量化拥塞通知):专为数据中心设计
根据 2022 年测量数据,超过 80% 的热门 Web 服务器及其到客户端的路径都支持 ECN 能力。
3.7.4 公平性(Fairness)
什么叫"公平"?
设想有 KKK 条 TCP 连接,每条连接都经过同一个瓶颈链路(bottleneck link),该链路传输速率为 RRR bps。
所谓公平的拥塞控制机制,是指每条连接平均能获得约 RK\frac{R}{K}KR 的带宽,即均等分享链路容量。
两个 TCP 连接的公平性分析
以最简单的情况为例(对应图2、图3):两条 TCP 连接共享一条速率为 RRR 的链路。
假设:
- 两条连接的 MSS(最大报文段大小)和 RTT 相同
- 都有大量数据要发送
- 链路上没有 UDP 流量
- 忽略慢启动阶段,始终处于拥塞避免(AIMD)模式
AIMD 如何保证公平?
AIMD = Additive Increase(加法增大) + Multiplicative Decrease(乘法减小)
假设初始点在 A:
- 两条连接的总带宽 < R,没有丢包
- 每个 RTT 各增加 MSSRTT\frac{MSS}{RTT}RTTMSS(AIMD 的加法增大),沿 45 度方向移动(两者同步增大)
- 到达 B 点时超过 RRR,发生丢包
- 两条连接各自将窗口减半,移动到 C 点(B 点到原点连线的中点)
- 从 C 再次沿 45 度方向增大…
- 如此反复,最终收敛到等分线与满负载线的交点
这就是为什么 TCP 的 AIMD 算法能保证公平性的数学直觉。
现实中的不公平因素
虽然理论上 AIMD 保证公平,但现实中有三类情况会破坏公平性:
1. RTT 不同的连接
RTT 小的连接,拥塞窗口增长更快(每秒能做更多次 AIMD 增大),因此抢到更多带宽:
吞吐量≈1.22⋅MSSRTT⋅L\text{吞吐量} \approx \frac{1.22 \cdot MSS}{RTT \cdot \sqrt{L}}吞吐量≈RTT⋅L1.22⋅MSS
其中 LLL 是丢包率。RTT 越小,吞吐量越大。
2. UDP 流量不公平
多媒体应用(网络电话、视频会议)常用 UDP,不受 TCP 拥塞控制约束:
- UDP:恒定速率发送,偶尔丢包但不降速
- TCP:遇到拥塞主动降速
结果:UDP 可能挤占 TCP 流量,TCP 让步,UDP 不让步。
3. 并行 TCP 连接
Web 浏览器常为一个网页开多条 TCP 连接(并行下载多个对象)。
举例: - 现有 9 条 TCP 连接共享速率 RRR 的链路
- 每条连接理论上得到 R10\frac{R}{10}10R(如果再来一条新连接的话)
- 但如果新应用开了 11 条并行连接,它能拿到超过 R2\frac{R}{2}2R 的带宽
这是因为并行连接等效于在竞争中占了更多"席位",是完全合法但不公平的行为。
3.8 传输层功能的演进
为什么需要新的传输层协议?
TCP 和 UDP 服务了四十多年,但各有不足:
| 场景 | TCP 的问题 | UDP 的问题 |
|---|---|---|
| 实时多媒体 | 拥塞控制导致延迟大 | 没有可靠传输 |
| 数据中心 | HOL 阻塞,连接建立慢 | 没有拥塞控制 |
| 移动网络 | 连接重建成本高 | – |
DCCP(数据报拥塞控制协议)
- 类似 UDP 的不可靠、面向消息服务
- 但具有应用可选的、与 TCP 兼容的拥塞控制
- 代表拥塞控制方案:TFRC(TCP友好速率控制)
TFRC 使用公式(方程驱动):
吞吐量≈1.22⋅MSSRTT⋅p\text{吞吐量} \approx \frac{1.22 \cdot MSS}{RTT \cdot \sqrt{p}}吞吐量≈RTT⋅p1.22⋅MSS
其中 ppp 是测量到的丢包率。TFRC 的目标发送速率 = 如果一个 TCP 连接遇到相同丢包率时的吞吐量。
TFRC 比 TCP 的"锯齿形"发送曲线平滑,更适合流媒体。
QUIC 协议详解
QUIC 是什么?
QUIC(Quick UDP Internet Connections)是目前最重要的传输层演进成果:
把 TCP 的可靠传输、流量控制、拥塞控制搬到了应用层,运行在 UDP 之上。
这样做的好处:可以像更新 App 一样快速更新协议,而不必等待操作系统内核更新 TCP。
QUIC 的协议栈对比(对应图4 Figure 3.57)
HTTP/1.1 over TCP(传统方式):
+------------------+ +------------------+
| HTTP request | | HTTP request |
| HTTP request | | HTTP request |
| HTTP request | | HTTP request |
| TLS encryption | | TLS encryption |
+------------------+ +------------------+
| TCP RDT |<--->| TCP RDT | ← 传输层
| TCP CC | | TCP CC |
+------------------+ +------------------+
HTTP/3 over QUIC(新方式):
+--------+--------+--------+ +--------+--------+--------+
| HTTP | HTTP | HTTP | | HTTP | HTTP | HTTP |
| req | req | req | | req | req | req |
+--------+--------+--------+ +--------+--------+--------+
| QUIC | QUIC | QUIC | | QUIC | QUIC | QUIC |
| enc | enc | enc | | enc | enc | enc |
+--------+--------+--------+ +--------+--------+--------+
| QUIC | QUIC | QUIC | | QUIC | QUIC | QUIC |
| RDT | RDT | RDT | | RDT | RDT | RDT |
+--------+--------+--------+ +--------+--------+--------+
| QUIC 拥塞控制 |<--->| QUIC 拥塞控制 |
+---------------------------+ +---------------------------+
| UDP | | UDP | ← 传输层
+---------------------------+ +---------------------------+
关键区别:QUIC 在应用层实现了可靠传输和拥塞控制,每个 HTTP 请求是独立的 Stream,互不阻塞。
QUIC 的四大核心特性
特性一:面向连接且加密(Connection-Oriented & Secure)
传统 TCP + TLS 需要多次握手:
TCP 三次握手(1.5 RTT)
↓
TLS 握手(1~2 RTT)
↓
开始传输数据
QUIC 将连接建立和安全握手合并:
QUIC 握手(1 RTT,甚至 0 RTT)
↓
直接传输数据(已加密)
所有 QUIC 数据包都强制加密,安全性内置而非可选。
特性二:流(Streams)
QUIC 允许在一条连接上多路复用多个独立的流(Stream):
单条 QUIC 连接
├── Stream 1 → HTTP 请求 A(图片)
├── Stream 2 → HTTP 请求 B(CSS 文件)
└── Stream 3 → HTTP 请求 C(JS 文件)
每个 Stream 有独立的 Stream ID,数据可以打包在同一个 QUIC 数据段中(通过 UDP 传输)。
特性三:解决 HOL 阻塞问题
HOL(Head-of-Line,队头阻塞)是 HTTP/1.1 over TCP 的痛点:
TCP 的问题:
发送顺序:[请求A的数据] [请求B的数据] [请求C的数据]
↓
请求A的某个包丢失
↓
请求B和C的数据已到达,但必须等请求A重传完成
↓
全部被阻塞!(HOL 阻塞)
QUIC 的解决方案:
Stream 1(请求A):[数据] [丢失!] [等待重传...]
Stream 2(请求B):[数据] [数据] [数据] → 正常接收,不受影响
Stream 3(请求C):[数据] [数据] [数据] → 正常接收,不受影响
QUIC 对每个 Stream 独立进行可靠、有序的传输,某个 Stream 丢包只影响该 Stream,其他 Stream 照常继续。
特性四:可靠且友好的拥塞控制
QUIC 使用的拥塞控制基于 TCP NewReno(RFC 6582),这是 TCP Reno 的改进版本。
QUIC 的确认机制也与 TCP 类似(基于 RFC 5681),熟悉 TCP 拥塞控制的读者可以直接阅读 QUIC 的 RFC 9002 规范。
应用层选择协议总结
应用开发者现在有三个选择:
选择1:原生 TCP(操作系统提供)
- 可靠传输、流量控制、拥塞控制
- 更新慢(需要操作系统升级)
选择2:原生 UDP(操作系统提供)
- 轻量、无连接
- 需要应用自己处理可靠性
选择3:QUIC over UDP(应用层实现)
- 继承 TCP 的可靠性和拥塞控制
- 额外获得:多路复用、0-RTT、内置加密
- 更新快(应用层更新即可)
关键概念总结
| 概念 | 核心要点 |
|---|---|
| ECN | 路由器主动标记拥塞,避免丢包才知道拥塞 |
| ECE 比特 | 接收方在 ACK 中回传拥塞信号 |
| CWR 比特 | 发送方确认已降低拥塞窗口 |
| AIMD 公平性 | 两条连接最终收敛到等分带宽 |
| RTT 不公平 | RTT 小的连接拿到更多带宽 |
| UDP 不公平 | UDP 不降速,挤占 TCP 空间 |
| 并行连接不公平 | 多开连接等于多占带宽份额 |
| QUIC 流 | 单连接多路复用,各 Stream 独立可靠传输 |
| QUIC 握手 | 连接建立 + 加密握手合并,速度更快 |
| HOL 阻塞 | TCP 有,QUIC 解决了这个问题 |
第三章 传输层:总结与重点习题详解
3.9 本章总结
传输层协议能提供什么服务?
传输层协议的服务能力范围很广:
最简单端:UDP
- 仅提供多路复用/解复用
- 不保证可靠、不保证顺序、不保证带宽
最复杂端:TCP
- 可靠数据传输
- 流量控制
- 拥塞控制
- 连接管理
重要约束:传输层能提供的服务受限于底层网络层。如果网络层无法保证延迟或带宽,传输层也做不到。
可靠数据传输的四大核心机制
| 机制 | 作用 |
|---|---|
| 确认(ACK) | 接收方告知发送方"我收到了" |
| 定时器(Timer) | 超时后触发重传 |
| 重传(Retransmission) | 丢包或超时后重新发送 |
| 序列号(Sequence Number) | 区分不同包,检测重复 |
这四个机制组合在一起,就能在不可靠的网络层之上实现可靠的数据传输。
TCP 的核心功能回顾
TCP = 连接管理 + 流量控制 + RTT 估算 + 可靠数据传输 + 拥塞控制
对于应用开发者来说,这一切都是透明的——只需打开 socket、往里塞数据即可。
TCP 拥塞控制演进
经典 TCP(AIMD)
├─ 拥塞空闲 → 加法增大(+1 MSS/RTT)
└─ 丢包发生 → 乘法减小(窗口减半)
新变体
├─ TCP CUBIC → 更快探测带宽(三次函数增长)
├─ TCP Vegas → 基于延迟(RTT变大即降速)
├─ TCP BBR → 基于带宽和延迟建模
└─ ECN辅助 → 路由器主动通知,避免丢包才知道
rdt2.1 接收方 FSM 详解(图1)
什么是 rdt2.1?
rdt2.1 是可靠数据传输协议第 2.1 版,解决的问题是:数据包可能损坏(bit error),但不会丢失。
它在 rdt2.0 的基础上增加了序列号,解决了 ACK/NAK 包本身损坏时发送方无法判断的问题。
图1 对应的接收方 FSM
图1 展示的是 rdt2.1 接收方的有限状态机(FSM):
状态说明:
- "Wait for 0 from below":期待收到序列号为0的包
- "Wait for 1 from below":期待收到序列号为1的包
用 Mermaid 重新画出该 FSM:
每条状态转移的含义
从"等待0"状态出发:
- 收到正确的0号包 → 提取数据,向上层交付,回复ACK,切换到"等待1"
- 收到损坏的包 或 错误的1号包 → 回复NAK,留在"等待0"
从"等待1"状态出发: - 收到正确的1号包 → 提取数据,向上层交付,回复ACK,切换回"等待0"
- 收到损坏的包 或 错误的0号包 → 回复NAK,留在"等待1"
P6 题分析:接收方错误导致死锁
题目说的"错误的接收方"(Figure 3.58)会导致死锁(deadlock)。
错误设计的接收方问题在于:在某些情况下,接收方发出的 ACK/NAK 与发送方期待的不匹配,使得双方都在等待一个永远不会发生的事件。
用 ASCII 时序图说明死锁场景(假设错误接收方在等待0时,对损坏包回复了 ACK 而非 NAK):
发送方 错误接收方
| |
|-------- pkt(seq=0) ----------->| (包损坏了)
| | 错误!发出了 ACK(0) 而非 NAK
|<-------- ACK(0) 损坏 ----------| ACK 本身也损坏了
| |
| (发送方收到损坏的ACK,不知道该 |
| 重发还是继续,陷入困惑) |
| |
| (接收方以为0已收到,在等seq=1) |
| (发送方不敢发1,在等确认0的ACK) |
| |
| === 双方都在等,死锁!=== |
对应的 C++ 模拟代码
#include <iostream>
#include <string>
// 模拟 rdt2.1 接收方的状态机
// 注意:这是正确版本,非图3.58的错误版本
enum ReceiverState {
WAIT_FOR_0, // 等待序列号为0的数据包
WAIT_FOR_1 // 等待序列号为1的数据包
};
// 模拟一个数据包
struct Packet {
int seq_num; // 序列号(0或1)
std::string data; // 数据内容
bool corrupted; // 是否损坏
};
// 模拟接收方处理一个包,返回发出的响应
std::string receiver_process(ReceiverState& state, const Packet& pkt) {
switch (state) {
case WAIT_FOR_0:
// 情况1:包未损坏 且 序列号正确(seq=0)
if (!pkt.corrupted && pkt.seq_num == 0) {
std::cout << "[接收方] 正确收到seq=0的包,数据: "
<< pkt.data << std::endl;
state = WAIT_FOR_1; // 切换到等待1的状态
return "ACK"; // 发送确认
} else {
// 情况2:包损坏 或 序列号错误
std::cout << "[接收方] 包损坏或序列号错误,发送NAK" << std::endl;
// state 保持 WAIT_FOR_0,不切换
return "NAK";
}
case WAIT_FOR_1:
// 情况3:包未损坏 且 序列号正确(seq=1)
if (!pkt.corrupted && pkt.seq_num == 1) {
std::cout << "[接收方] 正确收到seq=1的包,数据: "
<< pkt.data << std::endl;
state = WAIT_FOR_0; // 切回等待0的状态
return "ACK";
} else {
// 情况4:包损坏 或 序列号错误
std::cout << "[接收方] 包损坏或序列号错误,发送NAK" << std::endl;
return "NAK";
}
}
return "NAK"; // 不应该到这里
}
int main() {
ReceiverState state = WAIT_FOR_0; // 初始状态:等待seq=0的包
// 模拟一系列数据包到达
Packet packets[] = {
{0, "Hello", false}, // 正常包,seq=0
{1, "World", false}, // 正常包,seq=1
{0, "!!!", true}, // 损坏的包,seq=0(接收方将拒绝)
{0, "Retry", false}, // 重传的正常包,seq=0
};
for (auto& pkt : packets) {
std::cout << "\n--- 到达的包: seq=" << pkt.seq_num
<< " corrupted=" << (pkt.corrupted ? "是" : "否") << " ---" << std::endl;
std::string response = receiver_process(state, pkt);
std::cout << "[接收方响应] " << response << std::endl;
}
return 0;
}
运行输出:
--- 到达的包: seq=0 corrupted=否 ---
[接收方] 正确收到seq=0的包,数据: Hello
[接收方响应] ACK
--- 到达的包: seq=1 corrupted=否 ---
[接收方] 正确收到seq=1的包,数据: World
[接收方响应] ACK
--- 到达的包: seq=0 corrupted=是 ---
[接收方] 包损坏或序列号错误,发送NAK
[接收方响应] NAK
--- 到达的包: seq=0 corrupted=否 ---
[接收方] 正确收到seq=0的包,数据: Retry
[接收方响应] ACK
P40 题详解:TCP 拥塞窗口分析(图2)

图2 展示了 TCP Reno 连接的拥塞窗口随传输轮次变化的曲线。
从图中读取关键数据点:
轮次 1 → cwnd = 1
轮次 2 → cwnd = 2
轮次 3 → cwnd = 4
轮次 4 → cwnd = 8
轮次 5 → cwnd = 16 ← 慢启动阶段
轮次 6 → cwnd = 32 ← 超过 ssthresh(=32?),进入拥塞避免?不对...
实际是到16后翻倍到32(慢启动),然后线性增加
轮次 6 → cwnd = 32(慢启动翻倍到的)
轮次 7 → cwnd = 33
...(线性增加,每轮+1)
轮次 16 → cwnd = 42(丢包)
轮次 17 → cwnd = 24(减半)
...(线性增加)
轮次 22 → cwnd = 29(丢包,超时!)
轮次 23 → cwnd = 1(超时重置为1)
...(慢启动)
(a) 慢启动(Slow Start)阶段
慢启动特征:每个 RTT,窗口翻倍(指数增长)。
从图中看:
- 第 1~5 轮:cwnd = 1, 2, 4, 8, 16(指数翻倍)→ 慢启动
- 第 23~26 轮:cwnd = 1, 2, 4, 8(超时后重新慢启动)→ 慢启动
所以慢启动区间为:第1~5轮 和 第23~26轮。
(b) 拥塞避免(Congestion Avoidance)阶段
拥塞避免特征:每个 RTT,窗口仅加1(线性增长)。
- 第 6~16 轮:从 32 开始,每轮 +1(线性增长)→ 拥塞避免
- 第 17~22 轮:从 24 开始,每轮 +1(线性增长)→ 拥塞避免
© 第16轮后:三次重复ACK 还是 超时?
从图中看,第16轮 cwnd=42,第17轮 cwnd=24(约为42的一半)。
- 如果是超时:cwnd 会降到 1
- 如果是三次重复ACK:cwnd 降到原来的一半
第17轮 cwnd=24 ≈ 42/2,所以是三次重复ACK(fast retransmit),不是超时。
(d) 第22轮后:三次重复ACK 还是 超时?
第22轮 cwnd=29,第23轮 cwnd=1。
降到1,说明是超时(Timeout)。
(e) 第1轮的 ssthresh 初始值
慢启动在第5轮结束(cwnd=16),第6轮开始线性增长(拥塞避免),说明 ssthresh = 32(因为第5轮翻倍后才触发拥塞避免的切换,实际上根据图形,第6轮是32,这是慢启动的最后一次翻倍,然后进入线性)。
更准确地看:第1~5轮翻倍,第5轮达到16,第6轮变为32(仍在翻倍?还是已进入CA?)。
从图形判断,第6轮=32后开始线性增加,说明 ssthresh = 32。
(f) 第18轮的 ssthresh
第16轮发生了三次重复ACK,此时 cwnd=42,所以:
ssthresh=422=21ssthresh = \frac{42}{2} = 21ssthresh=242=21
第17轮起,cwnd=21(设为ssthresh),然后线性增加,第18轮=22…
实际图中第17轮=24,第18轮=25,说明丢包时窗口=42,ssthresh=⌊42/2⌋=21ssthresh = \lfloor 42/2 \rfloor = 21ssthresh=⌊42/2⌋=21,但图中直接设 cwnd=24,可能是实现细节。
根据题目图形,第18轮的 ssthresh = 21(由第16轮的 42/2 得到)。
(g) 第24轮的 ssthresh
第22轮超时,此时 cwnd=29,所以:
ssthresh=292≈14(向下取整)ssthresh = \frac{29}{2} \approx 14 \text{(向下取整)}ssthresh=229≈14(向下取整)
第24轮的 ssthresh = 14(或15,取决于取整方式)。
(h) 第70个段在第几轮发送?
累计发送段数(每轮发送 cwnd 个段):
| 轮次 | cwnd | 累计发送 |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 3 |
| 3 | 4 | 7 |
| 4 | 8 | 15 |
| 5 | 16 | 31 |
| 6 | 32 | 63 |
| 7 | 33 | 96 |
第70个段在第7轮(第6轮累计63,第7轮累计96,70在63和96之间)。
所以第70个段在第7轮发送。
(i) 第26轮收到三次重复ACK 后的窗口和ssthresh
假设第26轮 cwnd=8(从图中读取约为7-8),若为8:
cwndnew=82=4cwnd_{new} = \frac{8}{2} = 4cwndnew=28=4
ssthreshnew=82=4ssthresh_{new} = \frac{8}{2} = 4ssthreshnew=28=4
若图中第26轮=7:
ssthresh=⌊7/2⌋=3,cwnd=3ssthresh = \lfloor 7/2 \rfloor = 3, \quad cwnd = 3ssthresh=⌊7/2⌋=3,cwnd=3
(j) TCP Tahoe 在第16轮三次重复ACK 时
TCP Tahoe 与 TCP Reno 的区别:Tahoe 遇到三次重复ACK也降到1(不像Reno只减半)。
第16轮 cwnd=42,三次重复ACK:
- ssthresh = 42/2 = 21
- cwnd = 1(Tahoe 特有!降回1)
从第17轮开始慢启动(1,2,4,8,16),到第21轮 cwnd=16,然后21时进入CA(线性增长):
第19轮:cwnd = 4
第19轮的 ssthresh=21,cwnd=4。
(k) TCP Tahoe 第22轮超时,第17~22轮共发送多少包?
从第17轮开始(cwnd=1)到第22轮:
| 轮次 | cwnd(Tahoe慢启动,ssthresh=21) | 本轮发包数 |
|---|---|---|
| 17 | 1 | 1 |
| 18 | 2 | 2 |
| 19 | 4 | 4 |
| 20 | 8 | 8 |
| 21 | 16 | 16 |
| 22 | 21(达到ssthresh,进入CA,+1=22?实际图中是29) | … |
按慢启动规则(从1翻倍直到ssthresh=21):
1 → 2 → 4 → 8 → 16 → 21(到达ssthresh停止翻倍)→ 22(线性)…
第17~22轮共发送:1+2+4+8+16+21=521 + 2 + 4 + 8 + 16 + 21 = \mathbf{52}1+2+4+8+16+21=52 个数据包。
P41 题:AIAD(加法增大,加法减小)能否保证公平?
问题
如果把 TCP 的乘法减小改为加法减小(减去常数 ddd),会怎样?
分析
AIMD(乘法减小)收敛到公平的关键:
- 增大:沿 45度线 移动(两个连接同步增大)
- 减小:指向原点方向减半(两者比例不变)
AIAD(加法减小)的减小方向:
如果在点 B = (x1, x2) 发生丢包,
AIAD 减小后到达 (x1-d, x2-d),即沿 -45度方向移动
AIAD 的问题:减小是平移(两者各减去d),方向平行于等分线,而不是指向原点。
因此:
- 如果 B 在等分线右侧(连接1占更多带宽),减小后 C 仍在等分线右侧
- 永远不会穿越到等分线的另一侧
- 不会收敛到等分线上!
结论:AIAD 算法不能保证公平性,两条连接的带宽分配取决于它们的初始位置,不会自动均衡。
重要公式汇总
TCP 吞吐量宏观公式
平均吞吐量≈1.22⋅MSSRTT⋅L\text{平均吞吐量} \approx \frac{1.22 \cdot MSS}{RTT \cdot \sqrt{L}}平均吞吐量≈RTT⋅L1.22⋅MSS
其中 LLL 是丢包率(loss rate)。
EstimatedRTT 指数加权移动平均
EstimatedRTT=(1−α)⋅EstimatedRTT+α⋅SampleRTTEstimatedRTT = (1-\alpha) \cdot EstimatedRTT + \alpha \cdot SampleRTTEstimatedRTT=(1−α)⋅EstimatedRTT+α⋅SampleRTT
通常 α=0.125\alpha = 0.125α=0.125。
DevRTT(RTT偏差估计)
DevRTT=(1−β)⋅DevRTT+β⋅∣SampleRTT−EstimatedRTT∣DevRTT = (1-\beta) \cdot DevRTT + \beta \cdot |SampleRTT - EstimatedRTT|DevRTT=(1−β)⋅DevRTT+β⋅∣SampleRTT−EstimatedRTT∣
通常 β=0.25\beta = 0.25β=0.25。
TCP 超时间隔
TimeoutInterval=EstimatedRTT+4⋅DevRTTTimeoutInterval = EstimatedRTT + 4 \cdot DevRTTTimeoutInterval=EstimatedRTT+4⋅DevRTT
P31 题计算示例
初始:EstimatedRTT0=100EstimatedRTT_0 = 100EstimatedRTT0=100 ms,DevRTT0=5DevRTT_0 = 5DevRTT0=5 ms,α=0.125\alpha=0.125α=0.125,β=0.25\beta=0.25β=0.25
第1个样本:SampleRTT = 106 ms
EstimatedRTT1=0.875×100+0.125×106=87.5+13.25=100.75 msEstimatedRTT_1 = 0.875 \times 100 + 0.125 \times 106 = 87.5 + 13.25 = 100.75 \text{ ms}EstimatedRTT1=0.875×100+0.125×106=87.5+13.25=100.75 ms
DevRTT1=0.75×5+0.25×∣106−100∣=3.75+1.5=5.25 msDevRTT_1 = 0.75 \times 5 + 0.25 \times |106 - 100| = 3.75 + 1.5 = 5.25 \text{ ms}DevRTT1=0.75×5+0.25×∣106−100∣=3.75+1.5=5.25 ms
Timeout1=100.75+4×5.25=100.75+21=121.75 msTimeout_1 = 100.75 + 4 \times 5.25 = 100.75 + 21 = 121.75 \text{ ms}Timeout1=100.75+4×5.25=100.75+21=121.75 ms
第2个样本:SampleRTT = 120 ms
EstimatedRTT2=0.875×100.75+0.125×120=88.16+15=103.16 msEstimatedRTT_2 = 0.875 \times 100.75 + 0.125 \times 120 = 88.16 + 15 = 103.16 \text{ ms}EstimatedRTT2=0.875×100.75+0.125×120=88.16+15=103.16 ms
DevRTT2=0.75×5.25+0.25×∣120−100.75∣=3.94+4.81=8.75 msDevRTT_2 = 0.75 \times 5.25 + 0.25 \times |120 - 100.75| = 3.94 + 4.81 = 8.75 \text{ ms}DevRTT2=0.75×5.25+0.25×∣120−100.75∣=3.94+4.81=8.75 ms
Timeout2=103.16+4×8.75=103.16+35=138.16 msTimeout_2 = 103.16 + 4 \times 8.75 = 103.16 + 35 = 138.16 \text{ ms}Timeout2=103.16+4×8.75=103.16+35=138.16 ms
P3 题:UDP/TCP 校验和(1的补码)
题目
三个8位字节:01010011,01100110,01110100,求1的补码校验和。
计算步骤
步骤1:将三个字节相加
01010011 (83)
+ 01100110 (102)
-----------
10111001 (185)
10111001
+ 01110100 (116)
-----------
100101101 (301) ← 超过8位!产生进位
处理进位(wrap-around):将溢出的最高位加回到最低位:
100101101
最高位 1 溢出
→ 00101101 + 00000001 = 00101110
实际正确加法:
01010011
+ 01100110
= 10111001 (没有溢出,继续)
10111001
+ 01110100
= 100101101 (9位,最高位溢出)
wrap-around: 00101101 + 1 = 00101110
步骤2:取1的补码(取反)
校验和=∼00101110=11010001\text{校验和} = \sim 00101110 = 11010001校验和=∼00101110=11010001
为什么用1的补码而非直接求和?
接收方收到所有数据(包括校验和字段),将所有字段相加:
数据和+校验和=11111111(全1)\text{数据和} + \text{校验和} = 11111111 \text{(全1)}数据和+校验和=11111111(全1)
如果结果全是1,说明无误;如果有0,说明有错误。这样接收方只需检查结果是否全1,非常简便。
能检测所有错误吗?
- 1位错误:可以检测到(结果不全1)
- 2位错误:理论上如果两个位恰好对称翻转可能检测不到(概率很低,但存在)
关键概念对比表
| 特性 | Stop-and-Wait | Go-Back-N (GBN) | Selective Repeat (SR) |
|---|---|---|---|
| 发送窗口 | 1 | N | N |
| 接收窗口 | 1 | 1 | N |
| 序列号空间 | 2 | ≥N+1\geq N+1≥N+1 | ≥2N\geq 2N≥2N |
| 丢包重传 | 只重传丢失包 | 重传窗口内所有包 | 只重传丢失包 |
| 信道利用率 | 低 | 高 | 高 |
| 接收方缓冲 | 不需要 | 不需要 | 需要 |
序列号空间大小要求推导
GBN:接收窗口=1,所以序列号空间需要至少 N+1N+1N+1(NNN 为发送窗口大小)。
SR:发送和接收窗口均为 NNN,序列号空间需要至少 2N2N2N,否则接收方无法区分新包和重传的旧包。
P26 题:TCP 序列号空间与文件大小
(a) TCP 序列号字段是4字节(32位)
最大序列号 = 232−1=4,294,967,2952^{32} - 1 = 4,294,967,295232−1=4,294,967,295 字节
Lmax=232 字节=4 GBL_{max} = 2^{32} \text{ 字节} = 4 \text{ GB}Lmax=232 字节=4 GB
(b) 传输时间计算
MSS = 536 字节,每个段附加 66 字节头部,共 602 字节/段。
总段数 =⌈L/536⌉≈232/536≈8,012,999= \lceil L / 536 \rceil \approx 2^{32} / 536 \approx 8,012,999=⌈L/536⌉≈232/536≈8,012,999 个段
链路速率 = 155 Mbps = 155×106155 \times 10^6155×106 bps
每个段传输时间 =602×8/(155×106)≈31.07μs= 602 \times 8 / (155 \times 10^6) \approx 31.07 \mu s=602×8/(155×106)≈31.07μs
总传输时间:
T=8,012,999×31.07μs≈248.9 秒≈4.15 分钟T = 8,012,999 \times 31.07 \mu s \approx 248.9 \text{ 秒} \approx 4.15 \text{ 分钟}T=8,012,999×31.07μs≈248.9 秒≈4.15 分钟
第三章 编程实验、Wireshark实验与大师访谈
一、编程实验:实现可靠传输协议
实验目标
自己动手写发送方和接收方的传输层代码,实现一个简单的可靠数据传输协议。
有两个版本:
- 交替位协议版本(Alternating-Bit Protocol,即 rdt2.x/3.0)
- GBN版本(Go-Back-N,滑动窗口)
模拟环境结构
因为没有真实的独立操作系统可以修改,实验在一个模拟的软硬件环境中运行:
+---------------------------+
| 你写的代码 |
| A_output() B_output() | ← 应用层调用(从上往下)
| A_input() B_input() | ← 网络层调用(从下往上)
| A_timerinterrupt() | ← 定时器中断处理
+---------------------------+
| 模拟器框架 | ← 已提供,模拟网络延迟、丢包、损坏
+---------------------------+
你只需要实现上面那一层,模拟器会:
- 模拟包的发送和接收
- 模拟定时器的启动和停止
- 当定时器超时,自动调用你的中断处理函数
交替位协议(ABP)完整 C++ 实现
// =====================================================
// 交替位协议(Alternating Bit Protocol)完整实现
// 对应 rdt3.0:处理比特错误 + 丢包
// =====================================================
#include <iostream>
#include <string>
#include <cstring>
#include <queue>
// ==================== 数据结构定义 ====================
// 模拟网络数据包(与模拟器框架对接的结构体)
struct Packet {
int seq; // 序列号(交替位协议只用 0 和 1)
int ack; // 确认号
int checksum; // 校验和
char payload[20]; // 数据载荷
};
// 模拟消息(来自应用层)
struct Message {
char data[20];
};
// ==================== 全局状态变量 ====================
// 发送方状态
int sender_seq = 0; // 当前发送方序列号(0或1)
bool sender_waiting = false; // 发送方是否在等待ACK
Packet sender_pkt; // 当前正在发送(等待确认)的包
std::queue<Message> send_buffer; // 应用层缓冲的消息队列
// 接收方状态
int receiver_expected_seq = 0; // 接收方期待的序列号
// ==================== 辅助函数 ====================
// 计算校验和:将包的所有字段累加后取反(简单模拟1的补码)
int compute_checksum(const Packet& pkt) {
int sum = pkt.seq + pkt.ack;
// 将 payload 的每个字符的 ASCII 值累加
for (int i = 0; i < 20; i++) {
sum += (unsigned char)pkt.payload[i];
}
// 取1的补码(取反)
return ~sum;
}
// 检查包是否损坏:重新计算校验和并比对
bool is_corrupt(const Packet& pkt) {
int expected = compute_checksum(pkt);
// 如果重新计算的校验和与包中携带的不一致,说明损坏
// 注意:这里简化处理,实际比较方式依模拟器而定
return (expected + pkt.checksum != -1); // 1的补码:sum + checksum = 全1
}
// 检查是否是期待的 ACK
bool is_expected_ack(const Packet& pkt, int expected_seq) {
return (!is_corrupt(pkt) && pkt.ack == expected_seq);
}
// 打包:构造一个要发送的数据包
Packet make_packet(int seq, int ack, const char* data) {
Packet pkt;
pkt.seq = seq;
pkt.ack = ack;
memset(pkt.payload, 0, sizeof(pkt.payload));
strncpy(pkt.payload, data, 19); // 最多19字节,留1字节给'\0'
pkt.checksum = compute_checksum(pkt);
return pkt;
}
// ==================== 发送方函数 ====================
// 模拟"从上层(应用层)收到数据"的回调
// 当应用层有数据要发送时,模拟器会调用此函数
void A_output(const Message& msg) {
if (sender_waiting) {
// 发送方正在等待ACK,还不能发新包,先缓存起来
std::cout << "[发送方] 正在等待ACK,将消息加入缓冲队列" << std::endl;
send_buffer.push(msg);
return;
}
// 可以发送:构造数据包
sender_pkt = make_packet(sender_seq, 0, msg.data);
sender_waiting = true; // 进入等待ACK状态
std::cout << "[发送方] 发送包 seq=" << sender_seq
<< " 数据=" << msg.data << std::endl;
// 调用模拟器提供的发送函数(实际实验中由模拟器提供)
// udt_send(sender_pkt);
// 启动定时器(超时后会触发 A_timerinterrupt)
// start_timer(/* timeout_interval */);
}
// 模拟"从下层(网络层)收到ACK"的回调
// 当收到来自接收方的ACK包时,模拟器调用此函数
void A_input(const Packet& pkt) {
// 检查收到的ACK是否是我们期待的(未损坏 且 序列号正确)
if (is_expected_ack(pkt, sender_seq)) {
std::cout << "[发送方] 收到正确ACK=" << pkt.ack
<< ",停止定时器" << std::endl;
// 停止定时器(不再需要重传)
// stop_timer();
sender_waiting = false; // 退出等待状态
// 翻转序列号:0→1,1→0
sender_seq = 1 - sender_seq;
// 如果缓冲区还有待发消息,继续发送
if (!send_buffer.empty()) {
Message next_msg = send_buffer.front();
send_buffer.pop();
A_output(next_msg); // 递归调用,发送下一条
}
} else {
// 收到损坏的ACK或错误的ACK:忽略,等待定时器超时后重传
std::cout << "[发送方] 收到损坏/错误的ACK,忽略,等待超时重传" << std::endl;
}
}
// 定时器中断处理:超时后触发,说明包可能丢失
void A_timerinterrupt() {
std::cout << "[发送方] 定时器超时!重传包 seq=" << sender_seq << std::endl;
// 重传之前发送的包(sender_pkt 保存着)
// udt_send(sender_pkt);
// 重启定时器
// start_timer(/* timeout_interval */);
}
// ==================== 接收方函数 ====================
// 模拟"从下层(网络层)收到数据包"的回调
void B_input(const Packet& pkt) {
// 情况1:包未损坏 且 序列号正确
if (!is_corrupt(pkt) && pkt.seq == receiver_expected_seq) {
std::cout << "[接收方] 正确收到 seq=" << pkt.seq
<< " 数据=" << pkt.payload << std::endl;
// 向上层(应用层)交付数据
// deliver_data(pkt.payload);
// 发送ACK,确认这个序号
Packet ack_pkt = make_packet(0, receiver_expected_seq, "");
std::cout << "[接收方] 发送ACK=" << receiver_expected_seq << std::endl;
// udt_send(ack_pkt);
// 翻转期待的序列号
receiver_expected_seq = 1 - receiver_expected_seq;
} else {
// 情况2:包损坏 或 序列号错误(重复包)
// 重发上一个ACK(上一个被确认的序号)
int last_ack = 1 - receiver_expected_seq;
std::cout << "[接收方] 包损坏或重复,重发上一个ACK=" << last_ack << std::endl;
Packet ack_pkt = make_packet(0, last_ack, "");
// udt_send(ack_pkt);
}
}
// ==================== 主函数(演示) ====================
int main() {
std::cout << "=== 交替位协议(ABP)演示 ===" << std::endl;
std::cout << "注意:实际实验中,main由模拟器提供,这里仅做功能演示" << std::endl;
// 模拟发送方发送第一条消息
Message msg1;
strncpy(msg1.data, "Hello", 19);
A_output(msg1); // 发送方发出 seq=0 的包
// 模拟接收方收到该包(未损坏)
Packet received_pkt = make_packet(0, 0, "Hello");
B_input(received_pkt); // 接收方处理 seq=0 的包,发出 ACK=0
// 模拟发送方收到 ACK=0
Packet ack_from_b = make_packet(0, 0, "");
A_input(ack_from_b); // 发送方确认,seq 翻转为 1
// 发送第二条消息
Message msg2;
strncpy(msg2.data, "World", 19);
A_output(msg2); // 发送方发出 seq=1 的包
// 模拟定时器超时(包丢失场景)
A_timerinterrupt(); // 超时,重传 seq=1 的包
return 0;
}
GBN 协议关键设计(ASCII 示意)
GBN(Go-Back-N)相比交替位协议,最大的区别是窗口大小 > 1,可以流水线发送多个包:
发送方窗口(大小 N=4):
已确认 | 已发未确认 | 可发未发 | 不可发
[0][1] | [2][3][4][5] | [6][7] | [8]...
↑base ↑nextseqnum
接收方:只接受按序到达的包(窗口=1)
如果包k丢失,包k+1到来会被丢弃,发送方需要从k重传所有包
GBN 重传流程示意:
发送方 网络 接收方
|---pkt0--->| |
|---pkt1--->| |
|---pkt2--->| X (丢失) |
|---pkt3--->| |
| |---pkt0----->| ACK0
|<---ACK0---| |
| |---pkt1----->| ACK1
|<---ACK1---| |
| |---pkt2 丢失 |
| |---pkt3----->| 接收方丢弃pkt3(不是期望的pkt2)
|<---ACK1---| | 重发上次ACK
| 超时!从pkt2重传 |
|---pkt2--->| |
|---pkt3--->| |
| |---pkt2----->| ACK2
| |---pkt3----->| ACK3
二、Wireshark 实验
实验一:探索 TCP
做什么:
用浏览器从 Web 服务器下载一个文件,同时用 Wireshark 抓包,分析 TCP 的行为。
能观察到什么:
Wireshark 抓到的 TCP 报文 → 可以分析:
1. 序列号和ACK号的变化
→ 理解 TCP 是如何追踪字节流的
2. 窗口大小(rwnd)的变化
→ 观察流量控制如何动态调整
3. 重传的包
→ 发现丢包事件
4. 拥塞窗口的变化(通过推断)
→ 观察慢启动、拥塞避免的锯齿形曲线
5. RTT 估算
→ 通过发送时间和ACK到达时间计算
典型的 Wireshark TCP 分析流程:
实验二:探索 UDP
做什么:
抓取你最喜欢的使用 UDP 的应用(比如 DNS 查询、Skype/视频通话)的数据包,分析 UDP 头部。
UDP 头部结构(只有 8 字节!):
0 7 8 15 16 23 24 31
+-----------+----------+-----------+----------+
| 源端口号 | 目的端口号 | 长度 | 校验和 |
| (16bit) | (16bit) | (16bit) | (16bit) |
+-----------+----------+-----------+----------+
| 数据载荷 |
+---------------------------------------------+
校验和计算演示(以 DNS 查询为例):
DNS 使用 UDP,端口 53。抓到的包大概长这样:
源端口: 12345
目的端口: 53
长度: 28 (8字节头 + 20字节数据)
校验和: 0xA4B2
验证校验和的方法(1的补码求和):
- 将 UDP 伪首部 + UDP 头部 + 数据 全部按16位分组
- 累加所有16位字(超出16位的进位回卷加到最低位)
- 结果取1的补码
- 如果与包中校验和字段相加等于 0xFFFF(全1),则无误
三、大师访谈:Van Jacobson
他是谁?
Van Jacobson 是互联网拥塞控制领域最重要的先驱之一。他的主要成就:
时间线:
1988年 → 与 Mike Karels 合作,提出 TCP 拥塞控制算法
(在此之前,互联网曾多次发生"拥塞崩溃"!)
2001年 → 获得 ACM SIGCOMM 奖(终身贡献奖)
2002年 → 获得 IEEE Kobayashi 奖
"理解网络拥塞并开发出使互联网成功扩展的拥塞控制机制"
2004年 → 入选美国国家工程院院士
他还先后在以下机构工作:Lawrence Berkeley国家实验室、Cisco(首席科学家)、PARC(研究员)、Google。
访谈精华解读
Q1: 你职业生涯中最令人兴奋的项目是什么?
Van Jacobson 的原话精华(意译):
学校教我们找答案,但真正有趣的问题,挑战在于找到正确的问题。
他和 Mike Karels 研究 TCP 拥塞时,花了数月时间盯着协议和报文追踪,问"为什么它会失败?"。
某天在办公室,他们意识到了一件事:
“我之所以搞不清楚它为什么失败,是因为我根本没搞清楚它是怎么运作的。”
这才是正确的问题!这个问题逼着他们搞清楚了 TCP 中的 “ACK时钟”(ack clocking) 机制——这是 TCP 能正常工作的根本原因。
什么是 ACK 时钟?
发送方把多个包发出去后,ACK 会以和数据包进入网络相同的节奏返回来:
发送方 网络(有带宽限制) 接收方
|=pkt1=pkt2=pkt3==>瓶颈链路==>|
| 每个包通过后 |==ACK1==>
|<==ACK1==========按节奏返回 |==ACK2==>
|<==ACK2=== |==ACK3==>
|<==ACK3===
ACK 返回的节奏 = 网络能承受的速率
发送方可以用 ACK 到来的节奏来控制自己的发送速率!
→ 这就是"ACK时钟":网络自动告诉发送方该以多快速度发
Q2: 网络和互联网的未来在哪里?
Van Jacobson 提出了一个深刻的洞察:
普通人认为:Web = 互联网
技术人认为:Web 只是运行在互联网上的一个应用
但是 Van 说:普通人可能是对的!
互联网的原始设计 → 成对主机之间的对话(点对点)
Web 的本质 → 分布式信息的生产和消费(一对多/多对多)
他指出当前网络的低效之处:
- 广播媒介(无线电、光纤网络)被当作点对点链路处理 → 极度浪费
- 大量数据通过U盘、手机物理传输 → 网络协议完全管不到
- CDN和缓存已经成为必要,但现有网络理论还没有告诉我们如何工程化地设计和部署它们
他的愿景:网络需要进化到能够拥抱信息传播这个更大的视角,而不仅仅是"两台主机对话"。
Q3: 谁在职业上激励了你?
Richard Feynman(诺贝尔物理奖得主)来做学术报告,把 Van 一学期都没搞懂的量子力学解释得简单、清晰、不可抗拒。
Van 的感悟:
“看透复杂世界背后的简单本质,并传达给别人——这是一种罕见而美妙的天赋。”
这也是他研究网络的方式:不是堆砌复杂性,而是找到最本质的机制。
Q4: 对想从事计算机和网络的学生有什么建议?
Van Jacobson 给出了几个生动的类比,说明网络研究与日常生活的联系:
| 日常现象 | 对应的网络概念 |
|---|---|
| 蚂蚁觅食、蜜蜂舞蹈 | 协议设计(分布式通信与信息传递) |
| 交通拥堵 | 网络拥塞(本质是一样的!) |
| 体育场散场 | 拥塞控制(大量流量如何有序疏散) |
| 感恩节后学生抢机票回家 | 动态路由(在约束条件下寻找最优路径) |
核心建议:
网络是一个连接一切的领域。学习它会帮助你建立跨学科的思维连接。如果你对很多事情都感兴趣,并且想要产生影响力,很难找到比这更好的领域了。
四、三个实验的知识联系图
五、核心公式与概念回顾
信道利用率公式
停止等待协议(ABP)的信道利用率:
设传播时延为 dpropd_{prop}dprop,包传输时间为 dtransd_{trans}dtrans,则:
Usender=dtransRTT+dtrans=dtrans2⋅dprop+dtransU_{sender} = \frac{d_{trans}}{RTT + d_{trans}} = \frac{d_{trans}}{2 \cdot d_{prop} + d_{trans}}Usender=RTT+dtransdtrans=2⋅dprop+dtransdtrans
流水线协议(GBN/SR)的信道利用率:
Usender=N⋅dtransRTT+dtransU_{sender} = \frac{N \cdot d_{trans}}{RTT + d_{trans}}Usender=RTT+dtransN⋅dtrans
其中 NNN 是窗口大小。
当 NNN 足够大时,UsenderU_{sender}Usender 趋近于 100%。
P15 题:使信道利用率超过98%所需的窗口大小
设:
- 链路速率 R=1R = 1R=1 Gbps
- 包大小 L=1500L = 1500L=1500 字节
- 单向传播时延 dprop=15d_{prop} = 15dprop=15 ms(横贯美国)
- 则 RTT=30RTT = 30RTT=30 ms
包传输时间:
dtrans=L×8R=1500×8109=0.012 msd_{trans} = \frac{L \times 8}{R} = \frac{1500 \times 8}{10^9} = 0.012 \text{ ms}dtrans=RL×8=1091500×8=0.012 ms
要求 U≥98%U \geq 98\%U≥98%:
N⋅dtransRTT+dtrans≥0.98\frac{N \cdot d_{trans}}{RTT + d_{trans}} \geq 0.98RTT+dtransN⋅dtrans≥0.98
N≥0.98×(30+0.012)0.012≈0.98×30.0120.012≈2451N \geq \frac{0.98 \times (30 + 0.012)}{0.012} \approx \frac{0.98 \times 30.012}{0.012} \approx 2451N≥0.0120.98×(30+0.012)≈0.0120.98×30.012≈2451
所以窗口大小至少需要 2451 个包,才能在跨美国链路上达到 98% 的利用率。
六、编程实验设计思路总结
你需要实现的函数接口(模拟器会调用它们):
发送方(A端):
A_output(message) ← 应用层有新数据要发
A_input(packet) ← 收到来自B的ACK/NAK包
A_timerinterrupt() ← 定时器超时,需要重传
A_init() ← 初始化发送方状态
接收方(B端):
B_input(packet) ← 收到来自A的数据包
B_init() ← 初始化接收方状态
模拟器提供给你用的函数:
tolayer3(side, packet) ← 发包(经过模拟的不可靠网络)
tolayer5(side, data) ← 向上层交付数据
starttimer(side, time) ← 启动定时器
stoptimer(side) ← 停止定时器
调试技巧:
1. 先测试无丢包、无损坏的正常情况
2. 再测试 ACK 损坏的情况(发送方应超时重传)
3. 再测试数据包丢失的情况(触发定时器超时机制)
4. 最后测试连续丢包和乱序情况
5. 用模拟器的"trace level"参数开启详细日志
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)