CS144 - lab 1

date
Nov 29, 2022
slug
cs144-lab1
status
Published
tags
计算机网络
summary
滑动窗口的实现
type
Post

简介

本次lab 对应到 week 3。
notion image
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)
  • 缓冲区
网络环境是有波动的,如下图:
notion image
发送的bytes和时间的曲线可能会产生波动,为此缓冲区可以缓解这些波动带来的影响。缓冲区可以将部分数据缓存起来,避免丢失。
这次lab的任务是完成StreamReassembler部分
notion image
同时说明了我们每个 lab 需要做的任务
notion image

2 Getting started

这边要求我们合并远程的 lab1-startercode 代码再继续工作。
我一开始没搞清楚这个 lab 的 git 分支逻辑,然后尝试了几次才搞清楚。
这个 lab 的每一个分支都对应一个lab
notion image
我们每完成一个分支,都要保存下来,然后在完成的工作基础上合并新的labx-startercode 进行开发。
比如说我自己的仓库有 master,我开了一个 lab0 的分支,然后完成了 lab0 的测试。
接着我从 lab0 checkout 出来一个 lab1 的分支,然后 merge lab1-startercode 到这个新的 lab1 分支上,完成 lab1 的任务,以此类推
notion image
示意图大概就是这样
当然这是我自己的 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 数
比如下面这种图
notion image
蓝色部分指的是已经处理完的数据,绿色部分指的是在 ByteStream 中的数据,红色部分指的是还没有放入 ByteStream 的部分,后面的部分就是还没开始处理的数据

分析和设计

我们最终要设计的结构如下所示:
  • 有一个 buffer 帮助我们缓存不能立即放入 ByteStream 的数据
  • 一个 ByteStream 供读写
notion image
特别的我们需要注意这种情况:
notion image
假设我们读入 5 和 6,那么后面的三个 bytes 也应该一并放入 ByteStream 中
然后我们需要合并 buffer 中的 bytes,例如:
notion image
如果某次输入中带有10、11,例如 index = 7,data = “aaaa”,那么 7 ~ 12 的数据应该立即合并

实验结果

第一次尝试
第一个版本,用 set 来标记出现过的 index,用 map 来作为缓冲区,每次push_substring的时候都先往 map 里面放,然后再尝试从 map 里面取 byte 放到 ByteStream 里面
notion image
虽然通过了本地测试,但是性能比较糟糕。
首先是按照 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的流程
  1. 检查是否越界
    1. notion image
      假设 recv_index_ 是期望接收的第一个 index,而 last_store_index_ 是最后不能接收的字符(index = 0,cap = 8 时,last_store_index_ = 8)
      那么从图中来看,我们需要丢弃 1 和 5 两种情况
  1. 裁剪线段
    1. 根据上图,2 和 4 都需要裁剪出处于区间的部分,3不用裁剪
      特别的,4 需要我们进行 eof 的判断,如果 4 是 eof 的,那么此时这个 eof 指令是无效的,因为最后的字符被裁剪掉了
      另外我们还需要考虑一种完全覆盖区间的情况,即 2 + 3 + 4,这中情况即需要裁剪头部,也需要裁剪尾部,因此我们不能用 if … else … 进行处理
  1. 在树中合并
    1. 由于最终指向的 bytes 是一致的,我们处理起来的难度大大降低了
      现在我们得到了经过裁剪后的 node,我们想要把它插入到树中,保持树中 node 无重叠的性质,那么就需要我们对树的 node 进行合并。
      先是向后合并
      我们根据lower_bound,找到结构中对应的下一个 node,判断两个 node 的位置关系
      notion image
      由上图我们可以总结出三种类型的位置关系,分别是重叠、不重叠、覆盖
    2. case 1,如果不重叠,退出向后合并
    3. case 3,如果覆盖,把被覆盖的线段移除,因为当前线段能够表示被覆盖的线段了,继续向后合并
    4. case 2,如果重叠,那么合并两条线段,移除下一条线段,继续向后合并
    5. 向后合并完毕后,考虑向前合并
      notion image
      情况也是一样的
      那么经过最终的前后合并,树中的 node 都是无重叠的了
      notion image
  1. 尝试写入 ByteStream
    1. 当我们树中第一个 node 的 index,恰好等于 recv_index_ 的时候,就能进行写入了
  1. eof 的处理
    1. 我们先考虑一下 eof 什么时候是有效的,当且仅当 eof = true 且 最后一个字符被读入的时候就是有效的。那么回到最原始的这张图
notion image
对于 2 和 3,它们的 eof 都是有效的,对于 4 和 5,它们的 eof 都是无效的,因为最后一个字符被丢弃了,而对于 1 呢,测试用例并没有体现,我也把它视为无效的情况了。
最终通过了测试,满足了 lab 1 的 0.5 s 性能需求
notion image

一些容易忽略的点

一定要认真看 3.2 FAQs
  • 切片表示的最终的 bytes 是一致的
在 3.2 FAQs 中提到了
notion image
  • buffer 中的最好不要存储重叠的子串
notion image
  • 注意计算 first unassembled 和 first unacceptable
notion image
  • 有些不确定的地方,看测试用例确定应该怎么处理
如果还不能确定,就自己约定一个一致的风格好了

总结

其实这个 lab1 总结出来后就是一个算法问题了,比较考验学生的算法能力。
整个StreamReassembler实现的功能更能很像上计网课的时候讲到的滑动窗口,没想到是这种数据结构实现的。
debug
coding 途中我遇到了不稳定复现的 bug,而且由于没有好用的调试工具(或者说自己暂时没学会如何更好地调试 cpp),导致 debug 非常困难 不过好在最终成功 debug 了,下面是我的一些心得:
  • 计算机不会骗人,不要怀着“我的代码没问题啊”这种心态去 debug,不然自己很容易崩溃
  • 高效的 debug 工具非常重要,能帮你节省很多时间
    • 尽量让自己的工作自动化,能够节省很多时间
  • 想办法弄到导致 bug 的数据,如果没办法弄到,观察日志分析走到哪一步导致不符合预期的结果,然后根据调用链跟进
 

© hhmy 2019 - 2024