2.3.1 Test 语句
send_extra.cc
中,助教给了很多用例。这些用例我认为如果可以在编码之前就看的比较明白,就可以帮助我们节省很多时间,也可以帮我们理解 sender 发挥的作用~
首先我们需要理解这几个 test 语句:
test.execute(ExpectSegment{}.with_no_flags().with_syn(true).with_payload_size(0).with_seqno(isn));
TCPSender 发送 SYN 段(序列号为 isn),无数据负载。
ExpectSegment{}
表示的是当前发送了一个 segment,这个 segment 在这个时候已经被 push
到了 this->_segments_out
中。
它表示当前 sender 发送了一个段。发送了一个 segment。
后面一系列的 with 方法实际上就是验证当前 segment 的属性是否正确,如果不符合预期则会抛出异常。
test.execute(ExpectNoSegment{});
同理可得,当前没有 segment 被发送。也就是说,test 无法在 this->_segments_out
中取到新的 segment。
这里要额外说明一种情况就是:
test.execute(ExpectSegment{}.with_no_flags().with_syn(true).with_payload_size(0).with_seqno(isn));
test.execute(ExpectNoSegment{});
实际上它表示的就是 segment 从 this->_segments_out
中被取出来之后,this->_segments_out
里面就再也没有了其他数据。这种情况下适合检查的是,this->_segments_out
里面有好几个 segment 要连着发送的情况。
test.execute(WriteBytes("abc"));
TCPSender 将 "abc"
写入发送缓冲区。它实际上会执行一次 sender.fill_window()
这意味着通过写入缓冲区内的数据可以在这个过程中被立马放入 this->_segments_out
。
sender_harness.cc
struct WriteBytes : public SenderAction {
void execute(TCPSender &sender, std::queue<TCPSegment> &) const {
sender.stream_in().write(std::move(_bytes));
if (_end_input) {
sender.stream_in().end_input();
}
sender.fill_window();
}
}
test.execute(Tick{rto - 5});
时间流逝 RTO-5 个时间单位,未触发超时。
Tick 都表示当前时间是逐步在流逝的。测试框架会主动触发时间的流逝这一「行为」。和现实世界中的流逝是毫无关系的。
一般来说,Tick 常常用于测试重传机制相关的内容。如果有如下过程:
test.execute(Tick{rto - 5});
test.execute(6);
这表示过了 RTO-5 个时间单位后,再过 6 个时间单位。这其中过了一个 RTO 单位,一般情况下会触发重传一次,也会触发一次回退。
test.execute(AckReceived{WrappingInt32{isn + 1}}.with_win(1000));
接收方确认 SYN,ACK 号为 isn+1
(表示期望下一个字节是 isn+1
)。
这表示 sender 接受到了一个 Ack,实际上会执行 this->ack_received()
方法。z之后也会直接开始执行 sender.fill_window();
方法。
struct AckReceived : public SenderAction {
void execute(TCPSender &sender, std::queue<TCPSegment> &) const {
sender.ack_received(_ackno, _window_advertisement.value_or(DEFAULT_TEST_WINDOW));
sender.fill_window();
}
};
test.execute(ExpectState{TCPSenderStateSummary::SYN_ACKED});
这个就真的是顾名思义了,它需要检查你当前的 sender 处于哪个状态。
你可以通过 tcp_helpers/tcp_state.cc
来得知 test 框架判断 sender 处于哪个状态的依据。
2.3.2 初始情况
一般来说,测试环境都会有一个 SYN —> ACK 的过程,其实就是我们熟悉的三次握手。它大概可以用以下过程来描述:
发送 SYN 段(建立连接)
TCPSender 发送 SYN 段(序列号为 isn),无数据负载。接收 SYN-ACK 确认
接收方确认 SYN,ACK 号为 isn+1(表示期望下一个字节是 isn+1)。窗口大小设为 1000,允许发送方传输数据。
对应代码为:
test.execute(ExpectSegment{}.with_no_flags().with_syn(true).with_payload_size(0).with_seqno(isn));
test.execute(AckReceived{WrappingInt32{isn + 1}}.with_win(1000));
test.execute(ExpectState{TCPSenderStateSummary::SYN_ACKED});
2.3.3 情况 1
过程描述:
验证当 TCPSender 已启动且定时器在运行时,发送新数据段是否会干扰现有定时器。
根据测试名称,正确行为是:定时器应继续运行,不会被新发送的段重置,从而保证超时重传的可靠性。
详细过程:
(从初始情况开始)
写入数据
"abc"
并发送- 将
"abc"
写入发送缓冲区。 - 发送数据段,序列号为
isn + 1
,负载为 3 字节的"abc"
。此时启动重传定时器(RTO 初始值)。
- 将
时间流逝
- 时间流逝
RTO - 5
个时间单位,未触发超时。 - 确认没有重传发生,即没有新的数据段被发送。
- 时间流逝
写入新数据
"def"
并发送- 将
"def"
写入发送缓冲区。 - 发送新数据段
"def"
,序列号从isn + 1 + 3
开始(这里代码未显式体现序列号,默认按照顺序)。新数据发送后,原有定时器仍在运行,未被重置。
- 将
时间流逝
- 时间流逝 200 个时间单位,假设
RTO - 5 + 200 > RTO
,超过 RTO 触发超时。 - 重传最早的未确认段
"abc"
,序列号为isn + 1
,证明定时器未被新数据段重置。 - 确认没有其他段被发送(如
"def"
尚未超时)。
- 时间流逝 200 个时间单位,假设
关键结论:
- 定时器独立性:TCPSender 在发送新数据时,不会重置已有的重传定时器。定时器始终跟踪最早未确认的段(此处为 “abc”)。
- 重传优先级:超时后优先重传最早的未确认数据,确保可靠性,避免旧数据丢失导致传输中断。
- 窗口管理与多段处理:新数据 “def” 在窗口允许下被发送,但其确认与否不影响 “abc” 的定时器逻辑。
2.3.2 情况 2
过程描述:验证 fill_window()
方法是否能正确填充一个较大的窗口。根据测试名称,正确行为是:fill_window()
方法应根据窗口大小和数据量,合理地将数据分段发送,确保每个数据段的大小符合配置要求,且序列号连续,以充分利用窗口空间。
这个过程比较清晰的描述了一次 fill_window()
最大可发送量为:
const size_t expected_size = min(TCPConfig::MAX_PAYLOAD_SIZE, min(bigstring.size(), window_size) - i);
假设此时发送端缓冲区很长,超出 expected_size 的部分仍然不能发送。
详细过程:
(从初始情况开始)
- 循环发送数据段:
- 开始一个循环,循环条件是
i + TCPConfig::MAX_PAYLOAD_SIZE < min(bigstring.size(), window_size)
,其中i
是当前已经处理的数据偏移量。 - 在每次循环中,计算期望的数据段大小
expected_size
,取TCPConfig::MAX_PAYLOAD_SIZE
和剩余可发送数据量(min(bigstring.size(), window_size) - i
)中的较小值。 - 发送一个数据段,该数据段没有额外标志,负载大小为
expected_size
,数据为bigstring
从偏移量i
开始长度为expected_size
的子串,序列号为isn + 1 + i
。
- 开始一个循环,循环条件是
关键结论:
- 窗口填充正确性:
fill_window()
方法能够根据窗口大小和数据量,正确地将数据分段发送,确保每个数据段的大小不超过配置的最大负载大小,充分利用了窗口空间。 - 序列号连续性:每个数据段的序列号是连续的,保证了接收方能够正确地按顺序接收和重组数据,维护了数据传输的有序性。
- 数据完整性:通过循环发送数据段,将整个
bigstring
数据按要求分段发送,确保了数据的完整性,避免了数据丢失或错误传输。
2.3.3 情况 3
一句话:FIN 和 SYN 标志位都同样需要重传机制。含有 FIN 标志位的 segment 中,无论是否携带了其它数据,也都需要有重传机制。
过程描述:验证包含 FIN 标志的数据段是否能像其他数据段一样进行重传。根据测试名称,正确行为是:当包含 FIN 标志的数据段未被确认且超时发生时,应像普通数据段一样进行重传,以确保连接的正常关闭。
详细过程:
(从初始情况开始)
- 写入数据并发送含 FIN 的数据段:
- 将
"abc"
写入发送缓冲区,并标记输入结束(with_end_input(true)
)。 - 发送一个数据段,负载为 3 字节的
"abc"
,序列号为isn + 1
,同时带有 FIN 标志,表示连接关闭请求。
- 将
- 时间流逝(未触发超时):
- 时间流逝
RTO - 1
个时间单位,未触发超时。 - 确认没有重传发生,即没有新的数据段被发送。
- 时间流逝
- 时间继续流逝(触发超时):
- 时间再流逝 2 个时间单位,此时总时间超过了 RTO,触发超时。
- 重传之前带有 FIN 标志的数据段,负载为 3 字节的
"abc"
,序列号为isn + 1
,FIN 标志保持为true
。
关键结论:
- FIN 段重传机制:包含 FIN 标志的数据段在未被确认且超时发生时,会像普通数据段一样进行重传。这确保了即使在连接关闭阶段,数据的可靠传输也能得到保障。
- 可靠性保障:通过重传 FIN 段,保证了连接关闭请求能够被接收方正确收到,避免因丢包导致连接无法正常关闭,维护了 TCP 连接的可靠性。
- 定时器一致性:对包含 FIN 标志的数据段使用与普通数据段相同的定时器和重传逻辑,体现了 TCPSender 重传机制的一致性和稳定性。
2.3.4 情况 4
什么情况下需要添加 FIN 标志位?
过程描述:
验证在发送数据段时,如果添加 FIN 标志会使数据段超出接收方窗口大小,是否不会添加 FIN 标志。根据测试名称,正确行为是:当添加 FIN 标志会导致数据段大小超过接收方窗口时,不添加 FIN 标志,直到窗口大小足够容纳含 FIN 标志的数据段。
验证当接收方窗口已满时,是否不会单独发送仅含 FIN 标志的数据段。根据测试名称,正确行为是:在窗口空间不足的情况下,不会单独发送 FIN 段,直到窗口有足够空间,以保证数据传输的合理性和可靠性。
详细过程 1:
(从初始情况开始)
- 写入数据并准备关闭连接:
- 将
"abc"
写入发送缓冲区,并标记输入结束(with_end_input(true)
),表示有关闭连接的需求(即要添加 FIN 标志)。
- 将
- 接收 ACK 并发送普通数据段:
- 模拟接收到对 SYN 段的 ACK,确认号为
isn + 1
,窗口大小为 3。 - 发送一个数据段,负载为 3 字节的
"abc"
,序列号为isn + 1
,无 FIN 标志,因为此时添加 FIN 可能会使数据段超出窗口大小。
- 模拟接收到对 SYN 段的 ACK,确认号为
- 多次接收 ACK 但不发送新段:
- 模拟接收到确认号为
isn + 2
,窗口大小为 2 的 ACK,此时窗口大小不足以添加 FIN 标志,确认没有新的数据段被发送。 - 模拟接收到确认号为
isn + 3
,窗口大小为 1 的 ACK,同样窗口大小不够,确认没有新的数据段被发送。
- 模拟接收到确认号为
- 接收合适 ACK 并发送含 FIN 段:
- 模拟接收到确认号为
isn + 4
,窗口大小为 1 的 ACK,此时窗口大小足够容纳一个仅含 FIN 标志(无数据负载)的数据段。 - 发送一个仅含 FIN 标志、无数据负载的数据段,序列号为
isn + 4
。
- 模拟接收到确认号为
详细过程 2:
(从初始情况开始)
- 写入数据但不发送:
- 将
"abc"
写入发送缓冲区,但由于此时还未收到合适的 ACK 来确定窗口大小,所以没有数据段被发送。
- 将
- 接收 ACK 并发送普通数据段:
- 模拟接收到对 SYN 段的 ACK,确认号为
isn + 1
,窗口大小为 3。 - 检查 TCPSender 状态,期望状态为
SYN_ACKED
。 - 发送一个数据段,负载为 3 字节的
"abc"
,序列号为isn + 1
,无特殊标志。
- 模拟接收到对 SYN 段的 ACK,确认号为
- 请求关闭连接但不发送 FIN 段:
- 调用
Close
操作,表示要关闭连接,即需要发送含 FIN 标志的数据段。 - 但由于此时窗口空间可能不足以单独发送 FIN 段,确认没有新的数据段被发送。
- 调用
- 多次接收 ACK 仍不发送 FIN 段:
- 模拟接收到确认号为
isn + 2
,窗口大小为 2 的 ACK,窗口空间仍不足,确认没有新的数据段被发送。 - 模拟接收到确认号为
isn + 3
,窗口大小为 1 的 ACK,窗口空间还是不足,确认没有新的数据段被发送。
- 模拟接收到确认号为
- 接收合适 ACK 并发送含 FIN 段:
- 模拟接收到确认号为
isn + 4
,窗口大小为 1 的 ACK,此时窗口有足够空间容纳一个仅含 FIN 标志(无数据负载)的数据段。 - 发送一个仅含 FIN 标志、无数据负载的数据段,序列号为
isn + 4
。
- 模拟接收到确认号为
关键结论:
- 窗口限制机制:TCPSender 会根据接收方的窗口大小来决定是否添加 FIN 标志。当添加 FIN 会使数据段超出窗口大小时,会等待窗口大小足够,体现了窗口管理对数据段发送的限制作用。
- 窗口限制下的 FIN 发送策略:TCPSender 会根据接收方的窗口大小来决定是否单独发送 FIN 段。当窗口已满或空间不足时,不会单独发送 FIN 段,避免不必要的数据段发送,体现了窗口管理对数据传输的控制作用。
- 可靠性与效率平衡:通过不添加 FIN 避免数据段溢出窗口,保证了数据传输的可靠性,同时在窗口允许时及时发送含 FIN 段,实现了连接关闭的高效性。
- ACK 与窗口管理协同:ACK 的接收不仅更新了确认号,还影响了窗口大小,从而决定了数据段的发送策略,体现了 ACK 处理与窗口管理的协同工作。
2.3.5 情况 5
TCPConfig::MAX_PAYLOAD_SIZE
仅对数据段的负载部分起限制作用,SYN 和 FIN 标志不受此限制。以确保数据段的正常发送和连接的正确关闭。
过程描述:验证 MAX_PAYLOAD_SIZE
仅对数据段的负载部分起限制作用。根据测试名称,正确行为是:在发送数据段时,数据段的负载大小应受 MAX_PAYLOAD_SIZE
限制,但其他标志(如 FIN 标志)不受此限制,以确保数据段的正常发送和连接的正确关闭。
详细过程:
(从初始情况开始)
- 写入大量数据并准备关闭连接:
- 将一个较大的字符串
bigstring
写入发送缓冲区,并标记输入结束(with_end_input(true)
),表示要关闭连接(添加 FIN 标志)。
- 将一个较大的字符串
- 接收 ACK 并发送数据段:
- 模拟接收到对 SYN 段的 ACK,确认号为
isn + 1
,窗口大小为 40000。 - 发送一个数据段,负载大小为
TCPConfig::MAX_PAYLOAD_SIZE
,数据为bigstring
,序列号为isn + 1
,同时带有 FIN 标志。这表明即使数据量较大,但负载大小被MAX_PAYLOAD_SIZE
限制,而 FIN 标志正常添加。
- 模拟接收到对 SYN 段的 ACK,确认号为
- 检查状态:
- 检查 TCPSender 状态,期望状态为
FIN_SENT
,确认包含 FIN 标志的数据段已成功发送。
- 检查 TCPSender 状态,期望状态为
- 接收 ACK 并再次检查状态:
- 模拟接收到确认号为
isn + 2 + TCPConfig::MAX_PAYLOAD_SIZE
的 ACK。 - 检查 TCPSender 状态,期望状态为
FIN_ACKED
,确认包含 FIN 标志的数据段已被正确确认,连接关闭流程正常完成。
- 模拟接收到确认号为
关键结论:
MAX_PAYLOAD_SIZE
限制作用:MAX_PAYLOAD_SIZE
仅对数据段的负载部分起限制作用,确保负载大小不会超过设定值,维护了数据段的合理大小。- 标志独立性:FIN 标志等控制标志不受
MAX_PAYLOAD_SIZE
的限制,可以正常添加到数据段中,保证了连接关闭等控制功能的正常实现。 - 状态转换正确性:通过正确的 ACK 接收和状态检查,验证了 TCPSender 在发送和确认含 FIN 标志数据段过程中的状态转换是正确的,确保了连接管理的可靠性。
2.3.6 情况 6
验证在填充窗口时,将窗口大小为 0
的情况视为窗口大小为 1
进行处理。
过程描述:验证在填充窗口时,将窗口大小为 0
的情况视为窗口大小为 1
进行处理,同时不进行重传超时时间(RTO)的回退操作。根据测试名称,正确行为是:当窗口大小为 0
时,仍能按顺序发送数据段,且每次超时后重传未确认的数据段,RTO 保持不变。
但是,非 0 窗口则需要严格限制,一旦 flight 中的 data 容量大于窗口容量,这个时候必须处于停止发送的状态中。
详细过程:
(从初始情况开始)
- 接收窗口大小为 0 的 ACK 并发送数据段:
- 模拟接收到对 SYN 段的 ACK,确认号为
isn + 1
,窗口大小为0
。 - 检查 TCPSender 状态,期望状态为
SYN_ACKED
。 - 发送一个负载大小为 1 字节、数据为
"a"
、序列号为isn + 1
的数据段,无其他特殊标志,体现了将窗口大小0
视为1
来发送数据。
- 模拟接收到对 SYN 段的 ACK,确认号为
- 请求关闭连接但不发送新段:
- 调用
Close
操作,表示要关闭连接,但由于窗口大小限制,此时没有新的数据段被发送。
- 调用
- 对
"a"
数据段进行多次超时重传:- 进行 5 次循环,每次先让时间流逝
RTO - 1
个时间单位,期间没有数据段发送。 - 再让时间流逝 1 个时间单位,触发超时,重传负载为
"a"
、序列号为isn + 1
的数据段,且每次重传的 RTO 没有回退。
- 进行 5 次循环,每次先让时间流逝
- 接收新 ACK 并发送
"b"
数据段及重传:- 模拟接收到确认号为
isn + 2
、窗口大小为0
的 ACK。 - 发送一个负载大小为 1 字节、数据为
"b"
、序列号为isn + 2
的数据段。 - 同样进行 5 次循环,每次先让时间流逝
RTO - 1
个时间单位,无数据段发送,再流逝 1 个时间单位触发超时,重传"b"
数据段,RTO 保持不变。
- 模拟接收到确认号为
- 接收新 ACK 并发送
"c"
数据段及重传:- 模拟接收到确认号为
isn + 3
、窗口大小为0
的 ACK。 - 发送一个负载大小为 1 字节、数据为
"c"
、序列号为isn + 3
的数据段。 - 进行 5 次循环,每次先让时间流逝
RTO - 1
个时间单位,无数据段发送,再流逝 1 个时间单位触发超时,重传"c"
数据段,RTO 不回退。
- 模拟接收到确认号为
- 接收新 ACK 并发送含 FIN 段及重传:
- 模拟接收到确认号为
isn + 4
、窗口大小为0
的 ACK。 - 发送一个负载大小为 0 字节、序列号为
isn + 4
、带有 FIN 标志的数据段。 - 进行 5 次循环,每次先让时间流逝
RTO - 1
个时间单位,无数据段发送,再流逝 1 个时间单位触发超时,重传含 FIN 标志的数据段,RTO 保持不变。
- 模拟接收到确认号为
关键结论:
- 窗口大小处理策略:当接收方窗口大小为
0
时,TCPSender 将其视为窗口大小为1
来处理,能够按顺序发送数据段,保证了数据的持续传输。 - RTO 稳定性:在多次超时重传过程中,RTO 没有进行回退操作,保持了定时器的稳定性,确保了重传机制的可靠性。
- 数据和控制信息传输:无论是普通数据段(如
"a"
、"b"
、"c"
)还是含 FIN 标志的控制数据段,都能在窗口大小为0
的情况下正常发送和重传,保证了数据和连接管理的正常进行。
2.3.7 情况 7
过程描述:验证重复的 ACK 和过时的 ACK 对 TCPSender 的正常工作没有影响。根据测试名称,正确行为是:发送方在接收到重复或过时的 ACK 时,不会产生异常行为,仍能按照正常逻辑处理数据发送和连接关闭操作。
详细过程:
(从初始情况开始)
- 写入数据并发送数据段:
- 将
"abcdefg"
写入发送缓冲区。 - 发送一个负载大小为 7 字节、数据为
"abcdefg"
、序列号为isn + 1
的数据段。
- 将
- 接收正确 ACK 及重复 ACK:
- 模拟接收到确认号为
isn + 8
的 ACK,窗口大小为 1000,表明"abcdefg"
已被正确接收。 - 多次重复接收确认号为
isn + 8
的 ACK,期间没有新的数据段被发送,确认重复 ACK 不影响发送方。
- 模拟接收到确认号为
- 接收过时 ACK:
- 多次接收确认号为
isn + 1
的过时 ACK,没有新的数据段被发送,说明过时 ACK 不会干扰发送方的正常状态。
- 多次接收确认号为
- 写入新数据并发送含 FIN 段:
- 将
"ijkl"
写入发送缓冲区,并标记输入结束(with_end_input(true)
)。 - 发送一个负载大小为 4 字节、数据为
"ijkl"
、序列号为isn + 8
且带有 FIN 标志的数据段。
- 将
- 多次接收各种 ACK:
- 多次接收确认号为
isn + 1
、isn + 8
、isn + 12
的 ACK,包括重复和过时的 ACK,发送方未产生异常。
- 多次接收确认号为
- 超时重传:
- 时间流逝
5 * RTO
,触发超时,重传负载为"ijkl"
、序列号为isn + 8
且带有 FIN 标志的数据段。 - 之后确认没有新的数据段被发送。
- 时间流逝
- 接收最终 ACK 并检查状态:
- 模拟接收到确认号为
isn + 13
的 ACK,以及再次接收确认号为isn + 1
的过时 ACK。 - 时间流逝
5 * RTO
,没有新的数据段被发送。 - 检查 TCPSender 状态,期望状态为
FIN_ACKED
,确认连接正常关闭。
- 模拟接收到确认号为
多次接收确认号为
isn + 1
、isn + 8
、isn + 12
的 ACK,包括重复和过时的 ACK,发送方未产生异常。如何验证?
- 多次接收确认号为
isn + 8
的 ACK 时的验证
test.execute(AckReceived{WrappingInt32{isn + 8}}.with_win(1000));
test.execute(ExpectNoSegment{});
test.execute(AckReceived{WrappingInt32{isn + 8}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 8}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 8}}.with_win(1000));
test.execute(ExpectNoSegment{});
这里先接收了一次确认号为 isn + 8
的 ACK,然后使用 ExpectNoSegment{}
确认没有新的数据段被发送。之后又多次接收确认号为 isn + 8
的 ACK,最后再次使用 ExpectNoSegment{}
确认在这一系列重复 ACK 接收过程中没有异常的数据段发送。
- 多次接收确认号为
isn + 1
的 ACK 时的验证
test.execute(AckReceived{WrappingInt32{isn + 1}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 1}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 1}}.with_win(1000));
test.execute(ExpectNoSegment{});
这里多次接收确认号为 isn + 1
的过时 ACK,最后使用 ExpectNoSegment{}
确认在接收这些过时 ACK 期间没有异常的数据段发送。
- 后续多次接收不同 ACK 时的验证
test.execute(AckReceived{WrappingInt32{isn + 1}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 8}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 8}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 8}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 12}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 12}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 12}}.with_win(1000));
test.execute(AckReceived{WrappingInt32{isn + 1}}.with_win(1000));
// 这里虽然没有紧跟 ExpectNoSegment{},但后续通过超时重传等操作的正常执行也侧面验证没有异常
在这一系列多次接收不同确认号(包括重复和过时的)的 ACK 过程中,后续代码里的超时重传等操作正常执行,也从侧面验证了发送方在接收这些 ACK 时没有产生异常。例如后续的超时重传:
test.execute(Tick{5 * rto});
test.execute(ExpectSegment{}.with_payload_size(4).with_data("ijkl").with_seqno(isn + 8).with_fin(true));
test.execute(ExpectNoSegment{});
如果在接收那些 ACK 时发送方产生了异常,这里的超时重传可能就无法正常执行。
综上所述,通过 ExpectNoSegment{}
以及后续操作的正常执行,验证了发送方在接收多次 ACK(包括重复和过时的 ACK)时未产生异常。
关键结论:
- ACK 处理的健壮性:TCPSender 能够正确处理重复的 ACK 和过时的 ACK,不会因为这些异常 ACK 而产生错误的行为,保证了系统的健壮性。
- 重传机制正常:在超时情况下,发送方能够正常重传未确认的数据段,确保了数据传输的可靠性。
- 连接关闭正常:即使在接收到各种异常 ACK 的情况下,发送方仍能正确处理连接关闭请求,最终达到
FIN_ACKED
状态,完成连接的正常关闭。