3. Go-Back-N(GBN)协议:滑动窗口的朴素哲学

聊完停等协议,你可能会想:一个包一个包地发,信道利用率也太低了。没错,我在早期做嵌入式网络模块时,就吃过这个亏——带宽明明有 10Mbps,实际吞吐量连 1Mbps 都跑不到。原因很简单:停等协议让链路大部分时间都在空等。

GBN 协议的出现,就是为了解决这个问题。它允许发送方连续发送多个数据包,而不必等待每个包的确认。说白了,就是让管道里同时跑多个包。

3.1 GBN 发送方状态机

发送方的逻辑其实不复杂。我习惯把它拆成三个核心事件来处理:

  • 上层调用(发送数据):如果窗口没满,就发;满了就缓存或拒绝。
  • 收到 ACK:滑动窗口,向前移动 base。
  • 超时事件:重传窗口中所有已发送但未确认的包。

这里有个关键点:GBN 使用的是累积确认。你收到 ACK(n),意味着序号 n 及之前的所有包都被正确接收了。我当年第一次实现时,误以为每个包都需要单独确认,结果窗口逻辑写得一团糟。

核心规则:发送方只维护一个定时器。超时后,整个窗口回退到 base 位置,全部重发。

伪代码看起来是这样的:

// 发送方核心逻辑
base = 0
nextseqnum = 0
window_size = N

// 发送数据
function rdt_send(data):
    if nextseqnum < base + window_size:
        make_pkt(nextseqnum, data)
        udt_send(packet)
        if base == nextseqnum:
            start_timer()
        nextseqnum++
    else:
        refuse_data()  // 窗口满了

// 收到 ACK
function rcv_ack(acknum):
    base = acknum + 1
    if base == nextseqnum:
        stop_timer()
    else:
        start_timer()

// 超时处理
function timeout():
    start_timer()
    for i in base to nextseqnum-1:
        udt_send(packet[i])

个人经验:我在项目中遇到过定时器粒度太粗的问题。如果定时器精度是 100ms,而 RTT 只有 10ms,超时重传会浪费大量时间。建议定时器粒度至少是 RTT 的 1/10。

3.2 GBN 接收方状态机

接收方的设计就简单多了。它只做一件事:按序接收。

  • 收到期望的序号(expectedseqnum):交付上层,发送 ACK,期望序号加 1。
  • 收到乱序包:直接丢弃,并重发当前期望序号的 ACK。

你想想看,为什么接收方不缓存乱序包?因为 GBN 的设计哲学就是「接收方不负责排序」。一旦出现丢包或乱序,发送方会重传整个窗口。接收方缓存了也没用,反正后面还会重来。

避坑指南:我曾经在实现接收方时,自作聪明地缓存了乱序包。结果发现,当发送方超时重传时,接收方收到了重复包,反而需要额外的去重逻辑。最后我老老实实按 RFC 规范来——乱序包直接丢。

接收方伪代码:

// 接收方核心逻辑
expectedseqnum = 0

function rcv_packet(packet):
    if packet.seqnum == expectedseqnum:
        deliver_data(packet.data)
        udt_send(ACK, expectedseqnum)
        expectedseqnum++
    else:
        udt_send(ACK, expectedseqnum - 1)  // 重发上一个 ACK

3.3 窗口大小与序列号

窗口大小 N 和序列号空间的关系,是 GBN 最容易踩坑的地方。我见过不少人在面试时被这个问题问倒。

假设序列号用 k 位表示,范围是 [0, 2^k - 1]。窗口大小 N 必须满足:

N ≤ 2^k - 1

为什么会这样?举个例子:如果 k=2,序列号范围是 0~3。如果 N=4,发送方发了 0,1,2,3 四个包,接收方全部收到并确认。但 ACK 全部丢失了。发送方超时重传 0,1,2,3。接收方此时 expectedseqnum=4(即 0),它会把重传的 0 当作新包接收——这就出大问题了。

序列号位数 (k) 最大窗口 (N) 说明
1 1 退化为停等协议
2 3 常用配置
3 7 典型值
k 2^k - 1 通用公式

关键结论:窗口大小不能超过序列号空间的一半(对于 SR 协议)或减一(对于 GBN)。这是避免接收方无法区分新旧包的根本保障。

3.4 GBN 的优缺点分析

任何协议都有取舍。GBN 的优势和劣势都很明显。

优点:

  • 实现简单。发送方和接收方的状态机都很清晰。
  • 接收方不需要缓存,内存开销小。
  • 累积确认机制减少了 ACK 的数量。

缺点:

  • 丢包时重传效率低。一个包丢了,整个窗口都要重传。
  • 在高延迟、高丢包率的链路上,性能急剧下降。
  • 窗口大小受序列号空间限制。

我记得有一次在卫星链路上测试 GBN,RTT 接近 600ms,丢包率 2%。结果窗口里经常有包丢失,每次超时都要重传几十个包。吞吐量惨不忍睹。后来换成了选择性重传(SR),情况才好转。

适用场景:GBN 适合低丢包率、低延迟的局域网环境。如果你在做数据中心内部通信,GBN 完全够用。但如果是广域网或无线网络,建议直接上 SR 或 TCP 的 SACK。

3.5 GBN 状态机全景图

下面这张图展示了 GBN 发送方和接收方的完整交互流程。我建议你仔细看看超时重传那部分——这是 GBN 的核心行为。

GBN 协议状态机与交互流程 发送方 空闲 / 窗口未满 发送数据包 等待 ACK 超时重传窗口 超时 接收方 等待 expectedseqnum 收到期望包 收到乱序包 → 丢弃 发送 ACK 数据包 ACK 说明:发送方超时后重传整个窗口,接收方只接受按序到达的包

从图中你可以看到,GBN 的交互模式其实很「暴力」——要么全对,要么重来。这种设计在简单环境中很有效,但一旦环境变差,代价就大了。

再次提醒:如果你在实现 GBN 时发现吞吐量上不去,先检查窗口大小是否设置合理。我见过有人把窗口设得比序列号空间还大,结果接收方一直在丢包,发送方一直在重传,死循环。