涵盖内容:显式拥塞通知(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
0110 发送方声明"我支持 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)的流程说明:

  1. Host A(发送方) 发出 IP 数据报,其中 ECN 比特设为 0110,表示"我支持 ECN"
  2. 中间路由器 检测到自己拥塞,将 IP 数据报头部的 ECN 比特改为 11
  3. Host B(接收方) 收到带有 ECN=11 的数据报后,在返回给 Host A 的 TCP ACK 中设置 ECE(Explicit Congestion Notification Echo)比特 = 1
  4. 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}}吞吐量RTTL 1.22MSS
其中 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}}吞吐量RTTp 1.22MSS
    其中 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:

收到包且未损坏且seq=0
─────────────────
提取数据/向上交付
计算校验和/发送ACK

收到包但损坏或seq=1
─────────────────
计算校验和/发送NAK

收到包且未损坏且seq=1
─────────────────
提取数据/向上交付
计算校验和/发送ACK

收到包但损坏或seq=0
─────────────────
计算校验和/发送NAK

等待seq=0的包
(Wait for 0 from below)

等待seq=1的包
(Wait for 1 from below)

每条状态转移的含义

从"等待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=22914(向下取整)
第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}}平均吞吐量RTTL 1.22MSS
其中 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+βSampleRTTEstimatedRTT
通常 β=0.25\beta = 0.25β=0.25

TCP 超时间隔

TimeoutInterval=EstimatedRTT+4⋅DevRTTTimeoutInterval = EstimatedRTT + 4 \cdot DevRTTTimeoutInterval=EstimatedRTT+4DevRTT

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×∣106100∣=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×∣120100.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位字节:010100110110011001110100,求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+1N+1 ≥2N\geq 2N2N
丢包重传 只重传丢失包 重传窗口内所有包 只重传丢失包
信道利用率
接收方缓冲 不需要 不需要 需要

序列号空间大小要求推导

GBN:接收窗口=1,所以序列号空间需要至少 N+1N+1N+1NNN 为发送窗口大小)。
SR:发送和接收窗口均为 NNN,序列号空间需要至少 2N2N2N,否则接收方无法区分新包和重传的旧包。

P26 题:TCP 序列号空间与文件大小

(a) TCP 序列号字段是4字节(32位)

最大序列号 = 232−1=4,294,967,2952^{32} - 1 = 4,294,967,2952321=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/536232/5368,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μs248.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 分析流程:

打开Wireshark
开始抓包

用浏览器下载文件
触发TCP连接

停止抓包
过滤 tcp 流量

找到SYN包
确定连接建立

分析序列号
追踪数据流

查找重复ACK
定位丢包事件

计算RTT
seq发出时间到ACK到达时间

绘制时序图
理解拥塞控制行为

实验二:探索 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的补码求和):

  1. 将 UDP 伪首部 + UDP 头部 + 数据 全部按16位分组
  2. 累加所有16位字(超出16位的进位回卷加到最低位)
  3. 结果取1的补码
  4. 如果与包中校验和字段相加等于 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 给出了几个生动的类比,说明网络研究与日常生活的联系:

日常现象 对应的网络概念
蚂蚁觅食、蜜蜂舞蹈 协议设计(分布式通信与信息传递)
交通拥堵 网络拥塞(本质是一样的!)
体育场散场 拥塞控制(大量流量如何有序疏散)
感恩节后学生抢机票回家 动态路由(在约束条件下寻找最优路径)

核心建议
网络是一个连接一切的领域。学习它会帮助你建立跨学科的思维连接。如果你对很多事情都感兴趣,并且想要产生影响力,很难找到比这更好的领域了。

四、三个实验的知识联系图

理论:rdt协议
ACK/定时器/序列号

编程实验
实现ABP和GBN

Wireshark TCP实验
观察真实TCP行为

Wireshark UDP实验
分析UDP头部和校验和

理解:可靠传输的四大机制
在代码中体现

理解:TCP拥塞控制
窗口变化/重传/RTT估算

理解:UDP的简洁性
校验和是唯一的错误检测

Van Jacobson的贡献
ACK时钟机制
TCP拥塞控制算法

五、核心公式与概念回顾

信道利用率公式

停止等待协议(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=2dprop+dtransdtrans
流水线协议(GBN/SR)的信道利用率:
Usender=N⋅dtransRTT+dtransU_{sender} = \frac{N \cdot d_{trans}}{RTT + d_{trans}}Usender=RTT+dtransNdtrans
其中 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\%U98%
    N⋅dtransRTT+dtrans≥0.98\frac{N \cdot d_{trans}}{RTT + d_{trans}} \geq 0.98RTT+dtransNdtrans0.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 2451N0.0120.98×(30+0.012)0.0120.98×30.0122451
    所以窗口大小至少需要 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"参数开启详细日志
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐