科技 > 操作系统 > Windows

CS144(2024 Winter)Lab Checkpoint 0: networking warmup

28人参与 2024-08-02 Windows

0.前言 & collaboration policy

听说 cs144 的代码量不大,难度也不高,正好前几天刚发现今年的 cs144 github 仓库已经开放了,所以打算写一下新的。

如果你不知道如何快速搭建一个适用于 c++20 的环境,可以参考本文。

课程主页
check0.pdf

提示:本文建立在你有一个良好的代理环境的前提下。

课程本身对 ai 工具的态度比较有意思,就是把 gpt/github copilot 这样的工具当成已经做过往年的 cs144 的学生。其他的要求就是别公开你的代码、也不要抄袭别人的代码之类的。

ai-tools

1.set up gnu/linux on your computer

课程推荐了 vm 虚拟机、google cloud 还有 ubuntu 原生系统什么的;显然在国内 google cloud 什么的就不用考虑了,我也没有双系统打算,vm 也很笨重,所以就打算使用 wsl2 + docker 搭建一个镜像,然后使用 vscode + docker 的方式编写代码。

check0.pdf 告诉我们:

envir-requires

评测时使用的系统至少是 ubuntu:23.10,为了避免麻烦我就从 ubuntu:23.10 开始搭建镜像。然后需要注意的是前面那串 apt-get 的指令中有部分软件是文档文件(带 doc 的),为了加快安装我去除了这部分内容。

考虑到我们有可能在玩弄调试环境时搞坏容器环境,所以我使用 dockerfile 搭建一个相对客制化一点的镜像出来。下面是我用到的 dockerfile:

from ubuntu:23.10

arg usr=你自己的 github 名
arg email=你自己使用 git 时的邮箱

env tz=asia/shanghai \
    debian_frontend=noninteractive

# 部署环境
run echo 'root:为 root 用户设置一个密码,不在这里设也可以,但记得进入系统后要设置' | chpasswd \
    && apt-get update -y \
    && apt-get install -y sudo vim wget git zsh \
    && apt-get clean -y \
    && userdel -rf ubuntu \
    && useradd -ms /bin/zsh ${usr} \
    && usermod -ag sudo root \
    && usermod -ag sudo ${usr} \
    && echo '你的 git 用户名:你的用户在 ubuntu 中的密码' | chpasswd

user ${usr}

copy --chown=${usr}:${usr} ./*.sh /home/${usr}/
run sh -c "$(wget -o- https://install.ohmyz.sh/)" \
    && git config --global user.name "${usr}" \
    && git config --global user.email "${email}" \
    && mkdir -p /home/${usr}/cs144

workdir /home/${usr}/

可以看到这个镜像预先安装了 sudovimwgetgitzsh,你也可以自己定义自己想要的软件。我用 zsh 纯粹是因为它比 bash 好看,并且还在最下面的 run 语句中从 https://install.ohmyz.sh/ 安装 oh-my-zsh 配置 zsh 样式。

具体其他的指令细节不在这里阐述,可以参考 docker 的官方文档。

构建得到的镜像最终包含以下两个用户:

  1. root 用户;
  2. 一个以你自己的 git 用户名为用户名的、带有 sudo 权限的普通用户。

此外,还需要注意到在 dockerfile 中有一个 copy 部分,这里表示从宿主机拷贝一个文件到镜像中。这是因为如果在 dockerfile 中直接执行 apt-get 安装课程要求的一大堆环境依赖,就很容易受到网络环境波动导致安装失败,进而导致镜像的构建失败。

所以环境依赖的安装需要在建立容器后再进行。在与 dockerfile 同目录下创建一个文本文件 start-install.sh,并写入以下内容(注意记得把文件的行尾序列转为 lf):

sudo apt-get -y install cmake gdb build-essential clang clang-tidy clang-format pkg-config tcpdump tshark;

directory="/home/$(whoami)/cs144/minnow"
repository="https://github.com/cs144/minnow"

if [ ! -d "$directory" ]; then
    mkdir "$directory"
    git clone "$repository" "$directory"
    echo "repository cloned successfully into '$directory'"
else
    echo "directory '$directory' already exists. skipping cloning."
fi

这个 shell 脚本的功能是先使用 sudo apt-get 安装依赖项,然后在家目录下创建目录结构 cs144/minnow,如果这个目录结构已经存在,就不会执行 git 拉取仓库。如果你的仓库来自其他仓库,就需要把 repository 的网址替换掉。

把 dockerfile 的中文内容改成你自己想要的内容后,在 dockerfile 所在的文件夹下使用命令行执行 docker build -t cs144-image:2024 . 并等待出现以下信息,就表示镜像已经构建完成。

docker-building

如果构建失败了,那就多执行几次 docker build 直到成功为止(一般都是 502 错误导致的)。

顺便一提,ubuntu:23.10 的国内镜像源截止目前只有中科大的 https://mirrors.ustc.edu.cn/repogen/,但是我在切换到这个镜像源时 apt 一直告诉我软证书验证失败,所以我干脆一直开着代理算了。

另外,docker 允许我们将容器中的某个目录挂载到宿主机上,也就是说我们可以将容器内的某些东西保存在我们的 windows 文件系统中。为了防止每次跑容器的时候都需要重新拉取仓库,我就把 /home/你的用户名/cs144 这个文件夹挂载到了我自己的电脑上。挂载需要在运行容器时指定,具体使用的指令是 docker run -itd --name cs144-lab --mount type=bind,source="你的 windows 文件系统路径",target="/home/把这里替换成你的用户名/cs144" cs144-image:2024

不懂这部分或者不想这么干,可以执行这个:docker run -itd --name cs144-lab cs144-image:2024,这样就不会挂载数据了,不过记得要更改 shell 脚本中的仓库名称,还要记得及时 commit。

接下来我们进入容器中完成那一大串 apt-get 的环境依赖配置。怎么进入容器就不说了,我使用了 vsc 的 docker 插件访问容器,你也可以用命令行连接到容器中。进阶一点的话可以配置一下 ssh,然后用 ssh 连接到容器。详情可以参阅 docker 文档。

进去后可能会显示在 cs144 目录下,总之先回到家目录下执行刚刚编写的脚本。

docker-running

一定记得不要使用 sudo ./start-install.sh,这样会导致拉取的仓库被存放在 /home/root/cs144 而不是当前文件夹下,这里直接使用 ./start-install.sh 执行就 ok 了。

envir-building

可以看出来环境需要占用将近 1.5 gb 的空间,而且类似于 tshark 的软件包在安装时还需要进行额外的确认工作(不过由于 dockerfile 中设置了环境变量 debian_frontend,实际上是不会出现交互选项的),所以不把这部分内容放在 dockerfile 中操作是正确的。

如果这部分内容显示 502 之类的错误,那就多执行几次脚本,直到软件包全部安装完毕。

配置完成后尝试 cmake 编译一下,可以发现编译工作丝滑地完成了。

cmake-build

由于 cs144/ 文件夹被挂载在宿主机中,所以也可以在 windows 的文件资源管理器中看到拉取的仓库文件。

docker-file-in-windows

喜欢折腾的话到了这里就可以开始装饰 zsh 了。

2.networking by hand

这一节主要是使用 telnet 访问远程终端之类的东西,要注意的是如果是使用 docker 搭建的镜像,这里要自己安装 telnetnetcat

send yourself an email 部分由于我没有 stanford 的邮箱(笑),也懒得使用我自己的邮箱服务倒腾,这里略过就好了。

listening and connecting 部分比较有意思点,使用 netcat 在本地建立一个双工通信。先安装一下 netcat,然后跟着指南走就可以看到结果。

netcat-listening

再开一个终端窗口,使用 telnet 连接到本地 9090 端口,随便发点什么过去。

telnet-sending

可以看到 netcat 窗口已经收到了刚刚发送的东西。

netcat-received

3. writing a network program using an os stream socket

3.1 环境配置

如果是从镜像搭建起的环境的话,直接使用 cmake 编译是毫无问题的,而且所有的环境配置已经在 start-install.sh 中完成了。 比如这里先进入 build/ 目录编译一下:

build-make

3.2 代码规范

课程本身很鼓励使用 modern c++ 的特性,而且我正因为喜欢这点所以才入坑的。

代码规范方面看 check0.pdf 就好,简要概括就是:

正常遵循 modern c++ 编码规范就好,总而言之就是使用 raii、更加面向对象。

此外课程推荐尽量频繁地提交每次的工作进度,并且用心写好 commit messages 方便辨识每个 commit 的作用(也方便你回滚代码)。

3.3 writing webget

如果不知道怎么做的话,先去查看实验文档,然后还可以在这个文档中查看所有的 minnow support code 细节(工具函数一般都在 minnow/util 中)。

注意一下上面的文档提到: tcpsocket 继承自 socket,所以别忘了一起把父类的共有接口看一下。

在 writing webget 中需要补充 /apps/webget.cc 内的拼接 http 请求的函数,补充完毕后在 build 文件夹下 make 编译一下(这里前面已经做过了),然后执行 ./apps/webget cs144.keithw.org /hello

get_url-notices

文档这里告诉我们 http 协议本身是基于 tcp 协议传输数据的,所以在 get_url 中需要使用一个 socket 将拼接好的 http 请求传输给接收方。并且 http 需要的行尾序列是 crlf 而非 lf,而 socket 接收到的信息的结尾是一个 eof 符。所以参照 session 2.1 的请求命令把函数补充完整就行了。

要小心的是:给好的 tcpsocket 的一整条继承链上的所有类都没有实现析构函数,也就是说你不能依靠 raii 式语义要求 tcpsocket 对象自动释放连接。简单点说:记得手动调用 socket 对象的 close() 方法。

get_url

为了避免第一次写的人被迫看到我直接贴在文章里的代码,所以我的实现代码会以短链形式给出,仅供参考

web_get.cc

3.4 an in-memory reliable byte stream

到了 check0 中最有意思的部分:课程需要我们根据给定的代码框架,在文件 src/byte stream.hhsrc/byte stream.cc 中完成一个可靠字节流对象,并且在补充完成后测试实现能够到达的速度(bit/s)。

byte-stream_requires

根据 check0.pdf 的要求,将要完成的字节流对象需要做到以下几点:

  1. 对象能够存储的字节数不能超过 capacity 的值(这里的存储是指“存入但没被读取”);
  2. 写端能够关闭写入,并且写入关闭后除了拒绝接受任何字节以外,还需要添加一个 eof 字符标识字节流的结束;
  3. 读端只要读取了一个字节,那么字节流对象所能接纳的字节数就应当 +1;
  4. 承接上条,只要写端写入后读端可以立即读出,即使 capacity 的值为 1,实现的对象理论上也应该允许容纳无限多次的字节序列输入。

然后 pdf 还提到了这里只有单线程,不用考虑对象的读写抢占和加锁问题。接下来看一下给好的代码框架:

bytestream_details

比较有意思的是代码用了一个持有所有状态变量的基类 bytestream,然后从这个基类中派生了两个实现了不同功能侧写的类 readerwriter;不难猜出程序会在 reader()writer() 方法中将基类做类型转换变为派生类。

并且从注释里可以看到课程要求我们把所有私有变量放在基类 bytestream,显然就是为了防止强制类型转换时因为类的大小不同导致潜在的 ub 行为。

writerreader_details

在派生类部分,可以注意到 writerreader 的字节存取都基于 std::string,那很明显我们要补充的私有变量的底层数据类型至少也是 char(而且 char 的大小恰好是 1 字节)。而且很不同寻常的是,writerpush() 方法的参数是一个值类型的 std::string

正常来说如果我们想要传递一个对象类型,一般都会使用只读引用语义 const t&,或者说对于 std::string 可以使用视图语义 std::string_view;在 c++ 中,对象的传递使用值语义时一般都表示“在函数内部无论如何都会产生一次不可避免的复制”,或者是“对象必然需要在函数内部被修改,且修改操作与外界无关”。很显然,我们在管理传入的字节流时不需要对字节本身做任何修改,那么这只剩下一个可能:无论如何我们都需要将外界的 std::string 拷贝并存储在我们自己的内部容器中。也就是说课程在要求我们存储外界传进来的 std::string 对象本身。

并且我们都知道 c++11 后对于所有数据类型都存在一个“左右值”语义之分,那么很明显对于这个在函数参数内的左值类型 std::string 对象,我们需要将它右值转发(使用 std::move)给稍后我们要实现的内部容器,以避免无意义且耗时的拷贝工作。

再来分析一下字节读取时需要做的操作。根据需求,所有存入对象的字节都必须被按序取出,并且读端一旦取出一个字节,写端就必须立刻能够多接受一个字节,除非它被关闭。这听起来这很像一个“在无限长字节序列上滑动的窗口”,被窗口包围的部分就是能够被读端读取的字节(也是被写端写入的字节);这个窗口在处理输入的字节流时有个很特殊的性质:fifo。

到这里就已经可以知道我们实际上要补充什么了:一个 fifo 语义、能接受右值形式(因为需要避免拷贝开销)的 std::string 对象的容器,一个“滑动窗口”的实现,以及一些状态量。前面的容器估计提到 fifo 就知道 stl 有什么满足这个条件了,这里需要关注的是滑动窗口这个东西怎么实现。

peek() 方法要求我们返回一个 std::string_view 对象,用于查看字节序列中未读取的部分,实际上就是返回一个窗口。并且已知所需的内部实现是存储了多个 std::string 的容器,所以必然需要在 peek() 中利用当前容器顶部的 std::string 给出一个在某一范围内的 std::string_view 视图。为了和 pop() 的操作保持一致,我们还需要在内部维护一个指向容器顶部的 std::string 的首字节的偏移量,使得我们能够知道应该给出的窗口范围,并且在该范围减少到 0 时立即丢弃容器顶部的 std::string

所谓的“偏移量”的维护方式既可以是手动控制一个数值类型游标,也可以使用 std::string_view 配合它的成员方法 remove_prefix() 实现。考虑到降低心智负担,我实际上使用了 std::string_view 实现这个窗口。

补充完所有细节,并且确认已经考虑到大部分边界条件后,就可以在 minnow/ 文件夹下执行 cmake --build build --target check0 测试代码,并查看一下测速是多少。

test_notices

文档提到“大于 0.1 gbit/s 的速度都是可以接受的”,而且这里测速的结果和机器性能强相关(所以测个两三次取个平均值看个乐就好了)。因为我使用了 docker,所以测得的速度相对会比较高:

speed_test

byte_stream

4.submit

最后别忘了把文件推送到一个私有仓库里(推送前记得先清除原始的远端仓库)。

git_commit


  1. 升级包程序来自微软↩︎

(0)

您想发表意见!!点此发布评论

推荐阅读

鸿蒙Harmony应用开发—ArkTS声明式开发(通用属性:背景设置)

08-02

OpenHarmony实战:命令行工具hdc安装应用指南

08-02

VSCode搭建ARM开发环境

08-02

qemu虚拟机 安装银河麒麟V10 arm架构系统 桌面版_vmware支持arm吗

08-02

Windows Arm软件合集2024

08-02

Windows10使用wsl2安装Ubuntun20.04子系统并配置图形化界面

08-02

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论