CS144 Lab2: TCPReceiver实现全解析
前言
Lab2是CS144课程中的重要里程碑,要求我们实现TCP接收方的核心功能。经过反复调试和优化,我深刻体会到了TCP协议的精妙设计。本文将详细记录实现过程中的关键思路、常见陷阱以及解决方案。
任务概述
Lab2的核心任务是实现TCPReceiver
类的三个关键方法:
segment_received()
- 处理接收到的TCP段ackno()
- 计算确认序列号window_size()
- 计算接收窗口大小
核心概念理解
1. 三种序列号空间
这是整个Lab2最容易混淆的概念:
// 1. 32位包装序列号 (WrappingInt32) - TCP头部中的序列号
WrappingInt32 seqno = seg.header().seqno;// 2. 64位绝对序列号 (uint64_t) - 用于比较和计算,解决回绕问题
uint64_t abs_seqno = unwrap(seqno, isn, checkpoint);// 3. 流索引 (uint64_t) - StreamReassembler使用,从0开始的数据索引
uint64_t stream_index = abs_seqno - isn_abs - 1;
关键理解:
- 32位序列号:TCP协议层面,会回绕
- 64位绝对序列号:内部计算使用,不会回绕
- 流索引:应用层面,纯数据索引
2. 序列号占用规律
元素 | 序列号占用 | 流索引占用 | 示例 |
---|---|---|---|
SYN | 1 | 0 | 建立连接,不传输数据 |
数据字节 | 1 | 1 | 每字节占用一个位置 |
FIN | 1 | 0 | 结束连接,不传输数据 |
实现思路详解
1. segment_received() 实现
这是整个Lab2的核心方法,需要处理多个复杂逻辑:
1.1 ISN处理(特殊情况)
bool TCPReceiver::segment_received(const TCPSegment &seg) {// 特殊情况:ISN未设置时if (!_isn_set) {if (seg.header().syn) {_isn = seg.header().seqno;_isn_set = true;// 关键:第一个SYN段直接接受,不做窗口检查if (seg.payload().size() > 0 || seg.header().fin) {_reassembler.push_substring(seg.payload().copy(), 0, seg.header().fin);}_checkpoint = _isn.raw_value() + seg.length_in_sequence_space();return true;} else {return false; // 没有SYN,拒绝}}// 正常情况:ISN已设置,进行窗口检查...
}
关键点:
- ISN未设置时,只接受带SYN标志的段
- 第一个SYN段不需要窗口检查,直接接受
- SYN段可能同时包含数据和FIN
1.2 窗口检查逻辑
// 计算窗口大小
size_t window_size = _capacity - _reassembler.stream_out().buffer_size();
if (window_size == 0) window_size = 1; // 零窗口特殊处理// 计算窗口边界(绝对序列号空间)
uint64_t next_expected_offset = _reassembler.stream_out().bytes_written() + 1;
if (_reassembler.stream_out().input_ended()) {next_expected_offset += 1; // FIN已处理
}uint64_t window_start_abs = unwrap(wrap(next_expected_offset, _isn), _isn, _checkpoint);
uint64_t window_end_abs = window_start_abs + window_size - 1;// 计算段边界
size_t seg_length = seg.length_in_sequence_space();
if (seg_length == 0) seg_length = 1; // 零长度段特殊处理uint64_t seg_start = abs_seqno;
uint64_t seg_end = seg_start + seg_length - 1;// 重叠检查:只要有部分重叠就接受
bool in_window = !(seg_start > window_end_abs || seg_end < window_start_abs);
窗口检查要点:
- 零窗口处理:大小为0时当作1处理
- 零长度段:长度为0时当作1处理
- 重叠判断:部分重叠即可接受
- 坐标统一:都使用绝对序列号比较
1.3 流索引计算与数据处理
if (in_window) {uint64_t stream_index;if (seg.header().syn) {stream_index = 0; // SYN段数据从索引0开始} else {// 关键:正确的流索引转换uint64_t isn_abs = unwrap(_isn, _isn, _checkpoint);stream_index = abs_seqno - isn_abs - 1; // -1去除SYN占位}_reassembler.push_substring(seg.payload().copy(), stream_index, seg.header().fin);_checkpoint = abs_seqno + seg.length_in_sequence_space();
}
2. ackno() 实现
确认序列号的计算是TCP协议的精髓:
optional<WrappingInt32> TCPReceiver::ackno() const {if (!_isn_set) {return nullopt; // 没有SYN就没有ackno}// 计算下一个期望的相对偏移uint64_t next_expected_offset = _reassembler.stream_out().bytes_written() + 1; // +1为SYN占位// 如果流结束,还要为FIN确认if (_reassembler.stream_out().input_ended()) {next_expected_offset += 1; // +1为FIN占位}return wrap(next_expected_offset, _isn);
}
ackno计算公式:
ackno_offset = 已写入字节数 + 1(SYN占位) + (流结束 ? 1(FIN占位) : 0)
3. window_size() 实现
这个相对简单:
size_t TCPReceiver::window_size() const {return _capacity - _reassembler.stream_out().buffer_size();
}
常见陷阱与解决方案
陷阱1:直接使用 _isn.raw_value()
错误做法:
// ❌ 危险:忽略了序列号回绕
uint64_t abs_seqno = _isn.raw_value() + offset;
正确做法:
// ✅ 安全:通过wrap/unwrap处理回绕
uint64_t abs_seqno = unwrap(wrap(offset, _isn), _isn, _checkpoint);
陷阱2:wrap函数参数错误
错误理解:
// ❌ 错误:传入绝对序列号
return wrap(absolute_seqno, _isn);
正确理解:
// ✅ 正确:传入相对偏移
return wrap(offset_from_isn, _isn);
wrap函数本质:wrap(n, isn) = isn + n
,其中n是相对偏移量。
陷阱3:abs()函数用于无符号整数
问题代码:
// ❌ 危险:无符号数下溢 + abs()类型转换问题
uint64_t dist = abs(a - b);
正确做法:
// ✅ 安全:手动计算绝对值差
uint64_t dist = (a >= b) ? (a - b) : (b - a);
陷阱4:窗口检查逻辑错误
错误逻辑:
// ❌ 逻辑反了
bool in_window = !(seg_start <= window_end && seg_end >= window_start);
正确逻辑:
// ✅ 德摩根定律正确应用
bool in_window = !(seg_start > window_end || seg_end < window_start);
// 等价于:(seg_start <= window_end && seg_end >= window_start)
陷阱5:序列号空间混淆
常见错误:
// ❌ 错误:直接把绝对序列号当流索引
_reassembler.push_substring(data, abs_seqno, eof);
正确做法:
// ✅ 正确:转换为流索引
uint64_t stream_index = abs_seqno - isn_abs - 1;
_reassembler.push_substring(data, stream_index, eof);
总结
Lab2的核心挑战在于序列号空间的正确理解和转换。关键要点:
- 三种序列号空间要区分清楚
- wrap/unwrap函数要正确使用
- 窗口检查逻辑要处理各种边界情况
- 特殊情况处理(SYN、FIN、零窗口、零长度)
- 调试技巧帮助快速定位问题
扩展思考
- 为什么TCP需要序列号? - 保证数据的顺序性和完整性
- 为什么要用32位序列号? - 平衡空间效率和回绕周期
- 如何处理序列号回绕? - 使用相对比较和窗口机制
- 窗口机制的作用是什么? - 流量控制,防止接收方溢出
这些思考将帮助我们更深入地理解TCP协议的设计智慧。