CS144 学习笔记 01
个人见解,不一定是最优解
0. 目标
众所周知 TCP 需要向上层提供可靠的字节流,其发送端会把需要发送的数据分成几段发过来,这些数据报可能会在发送途中丢失、重复、顺序混乱(损坏的数据会被丢弃,所以这次不考虑损坏),这次的目标是把这些数据报重新组合成原来的整段数据。
1. 考虑
这次需要实现的接口主要是:
1 | void insert( uint64_t first_index, std::string data, bool is_last_substring ); |
| 参数 | 作用 |
|---|---|
first_index | 表示这段数据的第一个 byte 相对于整个数据的下标,整个数据的下标从 0 开始。 |
data | 表示这段数据,是一串二进制数据,可能包含 \0 。 |
is_last_substring | 表示这段数据为整个数据中的最后一段, i.e. 整个数据的长度为 first_index + data.length() - 1 。 |
我遇到的问题:
1.a. 缓冲区设计
这次的缓冲区和上次差不多,都类似一个 circlar_buffer<char> ,我选择了 std::string buf_ 作为原型,我以为接口还是和上次一样都是 std::string_view ,用 std::string 做原型可以避免再构造一个新的 std::string ,但回过头来看 ByteStream 的 void push(std::string) 接口不接受 std::string_view ,无论如何都需要构造一个新的 std::string ,所以 std::deque<char> 更方便一些?
这里有一个问题,
ByteStream的 public 接口没有知道它的完整容量的方法(available_capacity返回的是当前可用容量,而Reassembler使用ByteStream&&构造自己,这个ByteStream&&可能已经被用了),也就不能直接知道缓冲区应该多大,我用了一种丑陋的解决方案,每次insert都检测缓冲区长度,如果比ByteStream的available_capacity小就用\0填充到一样大。
1.b. 标记缓冲区
收到的数据可能是不连续的,需要先缓存,等继续收到数据形成一段连续的数据发给 ByteStream ,而 TCP 是字节流,数据可能包含 \0 ,所以直接在缓冲区标记数据是不可行的,我选择额外加了一个和缓冲区一样大小的 std::string indicator_ ,用 1 表示对应位置写入了数据、 \0 表示空。
这里还有个小问题, uint64_t Reassembler::bytes_pending() const 这个接口如何设计,如果每次 insert 都更新的话需要反复遍历 indicator_ ,所以我选择了用 #include <algorithm> 的 count(indicator_.cbegin(), indicator_.cend(), '1') ,也省了在 insert 里计算插入数据的真实长度的麻烦。
1.c. 推数据的逻辑
我的设计是记录下一个需要的 byte (以形成一段连续的数据)所在整个数据的下标,如果 insert 的 data 包含了这个 byte ,则通过 indicator_ 计算可以推给 ByteStream 的数据长度。
2. 我的答案
点击展开:我的解决方案
1 | diff --git a/src/reassembler.cc b/src/reassembler.cc |
1 | /* reassembler.hh */ |
1 | /* reassembler.cc */ |
测试结果:
1 | cmake --build ./build --target check1 |