本文讲解了如何逐步实现在不可靠的信道上实现可靠的数据传输,对于具有比特差错的信道,使用停等协议实现可靠传输,对于具有比特差错和分组丢失的信道,使用 GBN 或 SR 协议实现可靠传输。
1. 在可靠信道上的可靠数据传输 rdt1.0
可靠数据传输(Reliable Data Transmission,RDT)指的是在存在传输错误、丢包、重复或乱序等不可靠因素的通信信道上,通过一系列机制和协议确保数据能够无误且按顺序到达接收方。
在一个完全可靠的信道上(即不会发生位错误、丢包或乱序),我们可以用非常简单的有限状态机(FSM)来描述发送方和接收方的行为。在可靠信道上实现的可靠传输协议通常称为 rdt1.0。
在可靠信道中:
- 数据从发送方送出后一定能正确、按序到达接收方;
- 因此无需添加重传、确认或错误检测机制;
- 协议的功能仅仅是将上层交来的数据封装为数据包后发送出去,同时在接收端将接收到的数据包解封装后交付给上层。
这种情况下,发送方和接收方都可以只用一个状态来表示"等待(或就绪)",因为每次操作后都能立即回到初始状态。

(1)发送方 FSM
我们用 rdt_send()
函数表示可靠数据传输层的接口,由上层(如应用层)调用,用于将数据交给可靠传输协议处理。RDT 层会对数据进行封装(例如添加序列号、校验和等),以保证传输的可靠性。同理用 udt_send()
函数表示不可靠数据传输层的接口,用于实际把封装好的数据包发送到信道上。
- 状态:S0(等待上层调用):表示发送方处于空闲状态,等待上层调用发送数据。
- 状态转移
-
事件:上层调用
rdt_send(data)
。 -
动作:调用
make_pkt(data)
封装数据生成数据包;调用udt_send(packet)
将数据包发送到信道(因为信道可靠,无需等待反馈,直接返回初始状态)。 -
转移:无论何时发送完数据包,都立即回到 S0 状态,等待下一次上层调用。
rdt_send(data) / {pkt = make_pkt(data); udt_send(pkt)}
┌────────────────────────────────────────────────────────┐
│ │
│ ▼
[S0:等待上层调用] ──────────────────────────────────────> [S0:等待上层调用]
-
(2)接收方 FSM
- 状态:R0(等待接收数据包):表示接收方处于就绪状态,等待从信道中接收到数据包。
- 状态转移
-
事件:下层调用
rdt_rcv(packet)
(即接收到一个数据包)。 -
动作:调用
extract(packet)
将数据从数据包中提取出来;调用deliver_data(data)
将数据交付给上层应用;返回初始状态 R0,继续等待下一个数据包。 -
转移:每次接收、处理完一个数据包后,都回到 R0 状态。
rdt_rcv(packet) / {data = extract(packet); deliver_data(data)}
┌────────────────────────────────────────────────────────┐
│ │
│ ▼
[R0:等待接收数据包] ────────────────────────────────────> [R0:等待接收数据包]
-
2. 具有比特差错的信道
2.1 rdt2.0
现在我们假设下层的信道在传输过程中比特可能会发生翻转(即比特错误),那么为了实现可靠数据传输,我们需要在数据传输协议中引入以下几个关键机制:
- 差错检测:在发送数据时,协议会在数据中加入冗余信息(如校验和)来检测传输过程中是否发生了比特错误。接收方在收到数据后,使用相同的校验方法检测数据是否正确。如果检测出错误,就认为数据包已损坏。
- 反馈机制 (ACK/NAK):由于信道会引入比特错误,接收方需要将检测结果反馈 给发送方。常见做法是采用自动重传请求 (ARQ)机制,其中:
- 当接收方正确接收一个数据包时,发送肯定确认(ACK)。
- 当接收方检测到数据包存在错误时,发送否定确认(NAK)。
- 重传机制 :发送方在发送数据包后进入等待反馈状态 :
- 如果收到 ACK,则认为数据已正确传送,可以处理下一个数据包;
- 如果收到 NAK 或长时间未收到反馈,则重传当前数据包。
这种方案就是基于停等协议 (Stop-and-Wait ARQ)的实现,即发送方在等待反馈期间不发送新的数据。

(1)发送方 FSM
- 状态
- S0(等待上层调用):处于空闲状态,等待上层应用调用
rdt_send(data)
来发送新数据。 - S1(等待反馈):在发送完数据包后,进入等待接收方反馈(ACK/NAK)的状态,此时不能接受新的数据发送请求。
- S0(等待上层调用):处于空闲状态,等待上层应用调用
- 状态转移
- 从 S0 开始:
- 事件:上层调用
rdt_send(data)
。 - 动作:调用
make_pkt(data, checksum)
生成数据包(这里添加了校验和用于差错检测);调用udt_send(packet)
将数据包传送到信道中。 - 转移:进入 S1 状态,等待反馈。
- 事件:上层调用
- 在 S1 状态:
-
事件:调用
rdt_rcv(packet)
收到反馈包。 -
动作:检查反馈包是否正确(利用校验和检测),以及反馈内容是 ACK 还是 NAK。如果反馈是 ACK(且包正确),则认为数据包正确送达,停止等待反馈,转移回 S0 状态,等待上层数据;如果反馈是 NAK 或反馈包检测出错误,则再次调用
udt_send(packet)
重传数据包,状态保持在 S1,继续等待正确反馈。rdt_send(data) / {pkt = make_pkt(data, checksum); udt_send(pkt)}
┌────────────────────────────────────────────────────────────┐
│ ▼
[S0: 等待上层调用] [S1: 等待反馈]<───┐
▲ rdt_rcv(packet) && isACK(packet) │ │
└────────────────────────────────────────────────────────────┘ │
│ NAK │
└──────────────┘
-
- 从 S0 开始:
(2)接收方 FSM
- 状态:R0(等待接收数据包):处于空闲状态,等待从信道接收到数据包。
- 状态转移
- 在 R0 状态:
-
事件:下层调用
rdt_rcv(packet)
接收到数据包。 -
动作:使用校验和检测包是否受损,如果数据包无错误,先调用
extract(packet)
解封装得到数据,接着调用deliver_data(data)
将数据交给上层,最后调用udt_send(ACK)
发送肯定确认反馈给发送方;如果数据包受损,则调用udt_send(NAK)
发送否定确认反馈给发送方。 -
转移:执行完上述动作后,仍保持在 R0 状态,等待下一个数据包。
接收到数据包: rdt_rcv(packet)
┌───────────────────────────────────┐
▼ │
│ [R0: 等待接收数据包]
│
校验包是否受损?
├─> 若无错误: extract(packet) → deliver_data(data) → udt_send(ACK)
│
└─> 若有错误: udt_send(NAK)
│
▼
继续保持在 R0 状态
-
- 在 R0 状态:
2.2 rdt2.1
这样做有个致命的问题,那就是如果 ACK/NAK 出错怎么办?发送方不知道接收方发生了什么事情,如果重传可能重复,如果不重传可能死锁(或出错)。
可以给每个数据包加上一个序列号,反馈包(ACK)也包含被确认的序列号。这样,即使由于反馈出错导致重复重传,接收方也能根据序列号检测到重复数据并丢弃冗余包。


使用两个序列号 0 和 1 就足够了,一次只发送一个未经确认的分组,因此 FSM 的状态数多了一倍,因为每个状态必须记住当前分组的序列号为 0 还是 1。两种情况如下:

接收方不知道它最后发送的 ACK/NAK 是否被正确地收到,发送方不对收到的 ACK/NAK 给确认,也就是没有所谓确认的确认,接收方发送 ACK,如果后面接收方收到的是老分组 P0,则之前的 ACK 错误,如果收到的是下一个分组 P1,则之前的 ACK 正确。
2.3 rdt2.2
我们可以给 ACK 也进行编号,接收方对最后正确接收的分组发 ACK,以替代 NAK,即接收方必须显式地包含被正确接收分组的序号。发送方如果收到重复的 ACK(如再次收到 ACK0)时,与收到 NAK 采取相同的动作:重传当前分组。
这样修改能够为后面一次发送多个数据单位做准备,如果每一个的应答都有 ACK 和 NAK 就会很麻烦,使用对前一个数据单位的 ACK,代替本数据单位的 NAK,这样使得确认信息减少一半,协议处理简单。


3. 具有比特差错和分组丢失的信道
3.1 rdt3.0
如果信道可能丢失整个数据包,这样就会导致死锁(发送方在等待确认,接收方在等待数据包),我们需要设计一种能够同时检测错误和检测丢包的可靠数据传输协议,最经典的就是 rdt3.0,其关键机制如下:
- 错误检测(校验和):在发送端,对每个数据包计算校验和,并附加在包头中。接收端收到数据包后重新计算校验和,与附带的值比对,判断数据包是否在传输过程中出错。
- 反馈机制
- 肯定确认(ACK):接收端在成功接收到并校验正确的、且按序的数据包时,发送一个 ACK 包,该 ACK 包中携带了该数据包的序号。
- 重复 ACK:如果接收端检测到数据包出错或收到重复包(例如由于发送方重传而导致),它不会发送专门的否定确认(NAK),而是会再次发送上一次正确接收数据包的 ACK,从而让发送方得知当前数据包没有被正确接收。
- 超时与重传:发送端在发送一个数据包后启动一个定时器。如果在预设时间内未收到预期的 ACK(或收到的 ACK 包校验失败),发送端将触发重传机制,重新发送当前数据包。
- 序列号机制:为了区分新数据和重传数据,每个数据包都附加一个序列号(在停等协议中一般只需 1 位,交替使用 0 和 1)。接收端通过比较接收到的数据包序列号与期望值,判断数据包是否为新数据。
发送方的 FSM 如下图所示:

发送方的 FSM 通常包含两个主要状态:
- 状态 S0:等待上层数据,发送方处于空闲状态,等待应用层调用
rdt_send(data)
传来数据。- 动作:当收到
rdt_send(data)
时,执行动作:调用make_pkt(data, seq, checksum)
生成数据包,其中包含当前序列号和校验和;调用udt_send(packet)
将数据包送入不可靠信道;启动定时器,开始计时等待 ACK。 - 转移:转入状态 S1(等待 ACK)。
- 动作:当收到
- 状态 S1:等待反馈,发送方已发送数据包,正在等待接收方反馈 ACK。
-
事件 1:调用
rdt_rcv(feedback)
收到反馈包- 检查校验和:若反馈包出错,则视同未收到有效 ACK,保持 S1 状态(此时与之前的方法不同之处在于不立刻重传,而是最终依赖定时器超时触发重传)。
- 检查反馈内容: 若反馈包携带的 ACK 序号与当前数据包序号相符,则说明数据包已被正确接收,停止定时器,更新序列号(例如由 0 变为 1),并转回状态 S0,等待下一次数据发送。若反馈包中的 ACK 序号与当前数据包不符(例如重复 ACK,表示接收方仍在等待当前包),则忽略该反馈,保持 S1 状态(通常选择等待定时器超时)。
-
事件 2:定时器超时
- 动作:调用
udt_send(packet)
重传当前数据包,重启定时器,状态仍保持在 S1。
[S0: 等待上层数据]
| rdt_send(data) / {pkt = make_pkt(data, seq, checksum); udt_send(pkt); start_timer()}
▼
[S1: 等待反馈]
|-- 收到反馈 →
| if (ACK 有效 && seq 匹配) then { stop_timer(); update(seq); goto S0 }
| else { ignore feedback; remain in S1 }
|-- 定时器超时 → { udt_send(pkt); restart_timer(); remain in S1 } - 动作:调用
-
接收方的 FSM 通常只有一个主要状态,表示一直处于等待接收数据包的状态,其 FSM 如下:
- 状态 R0:接收方持续监听信道,等待接收数据包。
-
事件:下层调用
rdt_rcv(packet)
接收到一个数据包。 -
动作:通过校验和检测数据包是否被破坏,并判断序列号是否符合所等待的序号。若数据包无错误且序列号等于期望值,调用
extract(packet)
提取数据;调用deliver_data(data)
将数据交付给上层;更新期望的序列号(交替 0/1);调用udt_send(ACK_packet)
发送 ACK 包,ACK 包中包含当前数据包的序号。若数据包出错或序列号不符(例如重复数据),不向上层交付数据(避免重复交付);发送一个重复 ACK,即再次发送上一次正确接收数据包的 ACK,以通知发送方继续重传当前数据包。 -
转移:无论哪种情况,处理完后保持在 R0 状态,继续等待下一个数据包。
[R0: 等待接收数据包]
| rdt_rcv(packet)
▼
if (packet 校验和正确 && seq == expected) then
{ extract(packet); deliver_data(data); update(expected); udt_send(ACK(expected)) }
else
{ udt_send(ACK(last_correct_seq)) }
▼
[R0: 继续等待数据包]
-


最后一种情况表示定时器的时间设置不是很合理,过早超时也能够正常工作;但是效率较低,会导致一半的分组和确认是重复的,设置一个合理的超时时间也是比较重要的。
3.2 流水线协议
停等协议在可靠数据传输中虽然实现简单,但存在明显的效率问题。发送方每发送一个数据包后必须等待接收方返回 ACK 才能发送下一个数据包。这意味着如果数据包传输时间较短而往返时延(RTT)较长,那么发送方大部分时间都在等待,链路利用率极低,当信道带宽较大但传播延时较长时,发送方不能充分利用可用带宽,导致吞吐量受限。
如果由于错误或丢包导致反馈丢失或错误,发送方可能会触发超时重传,重复发送同一个数据包,这也进一步降低了效率。由于每个数据包都必须等待确认,整个传输过程中的空闲时间(等待 ACK 的时间)显著增加。


流水线协议(也称为滑动窗口 协议)旨在克服停等协议的低效率问题,其核心思想是允许发送方在等待反馈时连续发送多个数据包。这样可以让多个数据包在信道中同时传输,从而大大提高链路利用率和吞吐量。
在发送方/接收方要有缓冲区,发送方未得到确认的数据包可能需要重传,接收方上层用户取用数据的速率不等于接收到的数据速率,接收到的数据可能乱序,需要排序交付(可靠)。
发送缓冲区中可能存在未发送的分组 ,这些分组可以连续发送出去;也可能存在已经发送出去的、等待对方确认的分组,发送缓冲区的分组只有得到确认才能删除。
发送方维护一个发送窗口 ,允许连续发送多个数据包,窗口大小决定了同时在信道中未确认的数据包数目。接收方则维护一个接收窗口,表示期望接收的数据包序号范围。
发送窗口是发送缓冲区内容的一个范围(子集),这个窗口通常由两个指针定义,用 base
表示窗口中最早(最小序号)且尚未确认的数据包(后沿),用 nextSeqNum
表示下一个可用的序列号,也就是下一个待发送数据包的序号(前沿)。只要满足条件 nextSeqNum < base + WindowSize
发送方就可以继续发送数据包。

每发送一个分组,前沿前移一个单位,前沿移动的极限不能超过发送缓冲区,收到老分组的确认时后沿前移一个单位,同时窗口前移一个单位,此时发送缓冲区罩住新的分组,来了分组后前沿可以继续发送,后沿移动的极限不能超过前沿:

接收窗口等于接收缓冲区,用于控制哪些分组可以接收,只有收到的分组序号落入接收窗口内才允许接收,若序号在接收窗口之外则丢弃。如果接收窗口尺寸等于 1,则只能顺序接收,如果大于 1 则可以乱序接收(但提交给上层的分组要按序)。
在 Go-Back-N(GBN,回退 N 步)协议中,接收方通常只接受恰好等于期望序号的数据包,也就是接收窗口大小为 1,其他乱序数据包则被丢弃,并重复发送上一次正确收到数据包(也就是连续收到的最大的分组)的 ACK(累计确认)。
在 Selective Repeat(SR,选择重传)协议中,接收方可以接受乱序数据包,并将其缓存,也就是接收窗口大小大于 1。同时,它对每个正确接收的数据包都独立地发送 ACK,收到哪个分组就发送哪个分组的确认(非累计确认),当所有连续数据包齐全时再将它们交付给上层。

如果在一定时间内未收到 ACK(可能由于丢包或反馈错误),发送方将触发重传机制。在 GBN 中,通常重传整个窗口中所有尚未确认的数据包;而在 SR 中,只重传那些未被确认的数据包。
GBN 协议与 SR 协议的区别在于:
- GBN:接收窗口尺寸为 1,接收端只能顺序接收,发送端一旦一个分组没有发成功,如:
[0, 1, 2, 3, 4]
假如1
未成功,[2, 3, 4]
都发送出去了,那么还是要返回1
再发送(GB1); - SR:接收窗口尺寸大于 1,接收端可以乱序接收,发送端只需要选择性重发
1
即可。
GBN 协议的 FSM 如下:


GBN 协议运行过程如下:

SR 协议运行过程如下:
