CS144 - lab 1
date
Nov 29, 2022
slug
cs144-lab1
status
Published
tags
计算机网络
summary
滑动窗口的实现
type
Post
简介
本次lab 对应到 week 3。
week 3 的 slide 讲的都是包交换(packet switching)技术,这里记录一下基本概念:
- 包交换技术
数据从http ⇒ tcp ⇒ ip层层封装然后交给路由器转发到目的地
- Delay
- 链接上固定的传播延迟。 Propagation delay along the links (fixed)
- 数据打包的固定延迟。Packetization delay to place packets onto links (fixed)
- 路由器数据包缓冲区中的排队延迟. Queueing delay in the packet buffers of the routers (variable)
使用路由交换技术不可避免的延迟:
- 缓冲区
网络环境是有波动的,如下图:
发送的bytes和时间的曲线可能会产生波动,为此缓冲区可以缓解这些波动带来的影响。缓冲区可以将部分数据缓存起来,避免丢失。
这次lab的任务是完成StreamReassembler部分
同时说明了我们每个 lab 需要做的任务
2 Getting started
这边要求我们合并远程的 lab1-startercode 代码再继续工作。
我一开始没搞清楚这个 lab 的 git 分支逻辑,然后尝试了几次才搞清楚。
这个 lab 的每一个分支都对应一个lab
我们每完成一个分支,都要保存下来,然后在完成的工作基础上合并新的
labx-startercode
进行开发。比如说我自己的仓库有 master,我开了一个 lab0 的分支,然后完成了 lab0 的测试。
接着我从 lab0 checkout 出来一个 lab1 的分支,然后
merge lab1-startercode
到这个新的 lab1 分支上,完成 lab1 的任务,以此类推示意图大概就是这样
当然这是我自己的 git 风格,你可以在原始的 master 上进行迭代开发,而我习惯每进行一个 lab 都创建一个新的分支
ok,搞定之后我们看需求
3 Putting substrings in sequence
在 lab1 和 lab2 我们将要实现 TCP receiver,它能够接受数据报文并且将它们转换为可靠的字节流供应用程序从 socket 中读取数据。
TCP sender 将字节流分割为多个 segments,每个子串最大长度不超过 1460 bytes。网络是不稳定的,可能会产生乱序、丢失、重复提交等这些工作。receiver 必须要充足这些 segments 以实现可靠的字节流(reliable byte stream)
本次 lab 的工作就是实现 StreamReassmbler
- 接收 substrings,包括了组成 string 的 bytes,还有 string 在 stream 中 的唯一 index。这些数据可能是乱序的,需要我们组装起来
- 使用 ByteStream 作为输出,有序的把 bytes 写入到 ByteStream 中
然后文档也给出了需要实现的接口,同时强调了 StreamReassmbler 实现的是 TCP 的可靠性:从任意字节数据中恢复出原有的数据流,用此来对抗网络波动产生的丢失、复制等结果。
接口说明在
stream_reassembler.hh
3.1 What’s the “capacity”?
主要是定义了 capacity 的含义:
- 一方面是限制了 ByteStream 的大小
- 另一方面限制了从 substrings 中读取的最大 bytes 数
比如下面这种图
蓝色部分指的是已经处理完的数据,绿色部分指的是在 ByteStream 中的数据,红色部分指的是还没有放入 ByteStream 的部分,后面的部分就是还没开始处理的数据
分析和设计
我们最终要设计的结构如下所示:
- 有一个 buffer 帮助我们缓存不能立即放入 ByteStream 的数据
- 一个 ByteStream 供读写
特别的我们需要注意这种情况:
假设我们读入 5 和 6,那么后面的三个 bytes 也应该一并放入 ByteStream 中
然后我们需要合并 buffer 中的 bytes,例如:
如果某次输入中带有10、11,例如 index = 7,data = “aaaa”,那么 7 ~ 12 的数据应该立即合并
实验结果
第一次尝试
第一个版本,用 set 来标记出现过的 index,用 map 来作为缓冲区,每次
push_substring
的时候都先往 map 里面放,然后再尝试从 map 里面取 byte 放到 ByteStream 里面虽然通过了本地测试,但是性能比较糟糕。
首先是按照 byte 插入 buffer 性能太低了,然后用 set 标记使用过的 index 是没有必要的,因为最终表示的字节流是一致的,我当时写的时候没有注意到这一点
第二次尝试
经过第一次尝试,现在问题清晰很多了,问题本质就是一个算法问题:
对于一大段的 bytes,有多个切片(线段),切片之间可能重叠,每次输入会提供一个切片和起点,对于能够立即写入的切片,应该马上写入到 ByteStream 中,不能写入的切片应该暂存,但是需要丢弃容量之外的 bytes。
显然我们从线段的角度考虑更优,这里的线段可能会重叠、乱序、重复,但是它们表示的 bytes 都是一致的,这个性质很重要。
对于这个角度,我最先想到用堆来处理,每次都把一个 node: <index, slice> 放置到堆中,然后尝试将堆顶写入到 ByteSteam 中。但是用堆经过若干次插入后可能会变得很冗余,也就是线段重叠的部分会很多,这是不符合设计的(lab1 的 FAQs 也有说明)。
于是可以在堆上进一步尝试,我们根据 node 的 index 作为第一索引,存放到一个有序的数据结构中,我们对这个数据结构的要求是:node 之间没有重叠的部分。
那么在
push_substring
的时候,对于当前的 node,我们可以检查它前后的 node 是否能合并,如果能合并则消除重叠部分,直到不能合并为止。这样能够保证数据结构中的 node 都是无重叠的。显然用二叉排序树能够很好的满足我们的要求,还可以用二分来加快查找过程。在 cpp 里面,set 的底层是红黑树,使用起来很方便。这个核心数据结构想明白之后,接下来的实现就简单了。
push_substring
的流程- 检查是否越界
假设 recv_index_ 是期望接收的第一个 index,而 last_store_index_ 是最后不能接收的字符(index = 0,cap = 8 时,last_store_index_ = 8)
那么从图中来看,我们需要丢弃 1 和 5 两种情况
- 裁剪线段
根据上图,2 和 4 都需要裁剪出处于区间的部分,3不用裁剪
特别的,4 需要我们进行 eof 的判断,如果 4 是 eof 的,那么此时这个 eof 指令是无效的,因为最后的字符被裁剪掉了
另外我们还需要考虑一种完全覆盖区间的情况,即 2 + 3 + 4,这中情况即需要裁剪头部,也需要裁剪尾部,因此我们不能用
if … else …
进行处理- 在树中合并
- case 1,如果不重叠,退出向后合并
- case 3,如果覆盖,把被覆盖的线段移除,因为当前线段能够表示被覆盖的线段了,继续向后合并
- case 2,如果重叠,那么合并两条线段,移除下一条线段,继续向后合并
由于最终指向的 bytes 是一致的,我们处理起来的难度大大降低了
现在我们得到了经过裁剪后的 node,我们想要把它插入到树中,保持树中 node 无重叠的性质,那么就需要我们对树的 node 进行合并。
先是向后合并
我们根据lower_bound,找到结构中对应的下一个 node,判断两个 node 的位置关系
由上图我们可以总结出三种类型的位置关系,分别是重叠、不重叠、覆盖
向后合并完毕后,考虑向前合并
情况也是一样的
那么经过最终的前后合并,树中的 node 都是无重叠的了
- 尝试写入 ByteStream
当我们树中第一个 node 的 index,恰好等于 recv_index_ 的时候,就能进行写入了
- eof 的处理
我们先考虑一下 eof 什么时候是有效的,当且仅当 eof = true 且 最后一个字符被读入的时候就是有效的。那么回到最原始的这张图
对于 2 和 3,它们的 eof 都是有效的,对于 4 和 5,它们的 eof 都是无效的,因为最后一个字符被丢弃了,而对于 1 呢,测试用例并没有体现,我也把它视为无效的情况了。
最终通过了测试,满足了 lab 1 的 0.5 s 性能需求
一些容易忽略的点
一定要认真看 3.2 FAQs
- 切片表示的最终的 bytes 是一致的
在 3.2 FAQs 中提到了
- buffer 中的最好不要存储重叠的子串
- 注意计算 first unassembled 和 first unacceptable
- 有些不确定的地方,看测试用例确定应该怎么处理
如果还不能确定,就自己约定一个一致的风格好了
总结
其实这个 lab1 总结出来后就是一个算法问题了,比较考验学生的算法能力。
整个
StreamReassembler
实现的功能更能很像上计网课的时候讲到的滑动窗口,没想到是这种数据结构实现的。debug
coding 途中我遇到了不稳定复现的 bug,而且由于没有好用的调试工具(或者说自己暂时没学会如何更好地调试 cpp),导致 debug 非常困难
不过好在最终成功 debug 了,下面是我的一些心得:
- 计算机不会骗人,不要怀着“我的代码没问题啊”这种心态去 debug,不然自己很容易崩溃
- 高效的 debug 工具非常重要,能帮你节省很多时间
- 尽量让自己的工作自动化,能够节省很多时间
- 想办法弄到导致 bug 的数据,如果没办法弄到,观察日志分析走到哪一步导致不符合预期的结果,然后根据调用链跟进