CS144_2020_Fall_lab0(准备工作,webget,bytestream实现)
碎碎念开头:三年竞赛无人问,一朝面试全盘输,大三的寒假过的并不是那么舒服,准备春招实习,筹备项目,面对满纸漏洞的简历,决定去做一下这个闻名已久的计算机网络实验:CS144-基于UDP实现TCP。虽说已经做完了,但是对于其中一些知识点扔不是很牢固,有些测试点仅仅也是面向样例编程,不明所以,仅以此博客来巩固自己对于CS144中的知识体系以及实现方法。
拿到这个实验的时候,我想大多数人可能会有点蒙,我是谁?我在哪?我要干什么?
既然不知道干什么,那我们先来看项目文档吧!
tip,一些有用的资源(参考集合:
1.Stanford CS144: Computer Network - CS自学指南
2.CS144 Fall2020 Sponge 备份
3.CS144 Fall2020 Course Page
4.参考视频讲解
项目文档对应在3号链接lab0
对于英文文档,我们一定要耐得住寂寞,一字一句将他翻译过来。
首先是一大段废话,但是也不妨你来看看
tip:本博客中的所有翻译将使用chatgpt进行翻译后我个人再进行细节校对
欢迎来到CS144:计算机网络入门。在这个热身实验中,您将在计算机上安装Linux,学习如何手动执行一些互联网任务,编写一个在C++中获取互联网上网页的小程序,并在内存中实现网络的一个关键抽象:在写入者和读取者之间的可靠字节流。我们预计这个热身实验会花费您2到6个小时的时间(未来的实验会占用更多时间)。关于这些实验任务有三个要点:
1.在着手实验之前,先快速浏览整个实验!
2.在整个课程中,您将逐步构建自己的互联网部分实现——一个路由器、一个网络接口和TCP协议(将不可靠的数据报转换成可靠的字节流)。大多数实验都会在之前的工作基础上进行,也就是说,您将在本学期逐步构建自己的实现,并且将在将来的实验中继续使用您的工作。这使得跳过一个实验变得困难。
3.实验任务不是规范,意味着它们不是为了单向使用而编写的。它们更接近于软件工程师从上司或客户那里得到的详细级别。我们期望您参加实验课程,并在发现某些内容模糊或您认为答案重要时提出澄清性问题。(如果您认为某些内容可能没有被完全规定,有时事实是这并不重要,您可以尝试一种方法或另一种方法,看看会发生什么。)
然后是
0 合作政策
一些对他们学生的要求,没用的,继续往下看。
1 安装GNU/Linux
1 在你的计算机上安装GNU/Linux
CS144的作业要求使用GNU/Linux操作系统和一个支持C++ 2017标准的最新C++编译器。请从以下三个选项中选择一个:
推荐:安装CS144 VirtualBox虚拟机镜像(详细说明请参见 https://stanford.edu/class/cs144/vm howto/vm-howto-image.html)。
运行Ubuntu 18.04 LTS版本,然后运行我们的设置脚本。你可以在实际计算机上、VirtualBox内部、EC2或另一个虚拟机上执行此操作(详细步骤请参见 https://stanford.edu/class/cs144/vm howto/vm-howto-iso.html)。
使用其他GNU/Linux发行版,但请注意你可能会在途中遇到一些问题,需要具备调试的能力。你的代码将在Ubuntu 18.04 LTS上进行测试,使用g++ 8.2.0编译和运行,并且必须在这些条件下正确工作。有关提示,请参见 https://stanford.edu/class/cs144/vm howto/vm-howto-byo.html。
这里好像也没啥说的,主要是配置一下GNU/Linux,他这里说推荐使用虚拟机等等,我这里为了方便就选择了本地,WSL作为基础环境,毕竟本地也可以和本地进行TCP通信,编译器选择的
但是理论上并不需要这么高,直接用Ubuntu自带的提示,一站式安装g++的环境就可以了。
也可以软连接设置版本,这就是题外话了,配环境都不多说了,对于CS144_2020的环境没有那么多讲究,(2024就需要很奇葩的gcc环境...)
总之这是个很常规的环境,应该难不倒你。
2 手动进行网络操作
让我们开始使用网络。你将手动完成两个任务:获取一个网页(就像一个Web浏览器),并发送一封电子邮件(类似于邮件客户端)。这两个任务都依赖于一个称为可靠的双向字节流的网络抽象:你将在终端中输入一系列字节,相同的字节序列最终会按照相同的顺序传递到运行在另一台计算机上的程序(服务器)。服务器会以其自己的字节序列作出回应,然后传递回到你的终端。
2.1 Fetch一个网页
1. 在Web浏览器中,访问 [http://cs144.keithw.org/hello](http://cs144.keithw.org/hello) 并观察结果。
2. 现在,你将手动执行与浏览器相同的操作。
3. (a) 在你的虚拟机上运行 telnet cs144.keithw.org http。这告诉telnet程序在你的计算机和另一台计算机(名为cs144.keithw.org)之间打开一个可靠的字节流,并使用该计算机上运行的特定服务:用于World Wide Web的Hyper-Text Transfer Protocol(http)。 如果你的计算机已经正确设置并且连接到互联网,你会看到:
user@computer:~$ telnet cs144.keithw.org http
Trying 104.196.238.229...
Connected to cs144.keithw.org.
Escape character is ^].
If you need to quit, hold down ctrl and press ]
(b) 输入 GET /hello HTTP/1.1(从第三个斜杠开始的部分)
(c) 输入 Host: cs144.keithw.org(告诉服务器URL的路径部分,即http://和第三个斜杠之间的部分)。
(d) 输入 Connection: close(告诉服务器你已经完成请求,它应该在完成回复后关闭连接)。
(e) 再按一次回车键(发送一个空行,告诉服务器你已经完成HTTP请求)。
(f) 如果一切顺利,你将看到与你的浏览器看到的相同的响应,前面带有告诉浏览器如何解释响应的HTTP头。
作业:现在你知道如何手动获取网页了,来演示一下吧!使用上述技术获取URL http://cs144.keithw.org/lab0/sunetid,将sunetid替换为你自己的主要SUNet ID。你将在X-Your-Code-Is:头部中收到一个秘密代码。保存你的SUNet ID和代码以便在写作中使用。
这个小操作还是挺基础的,用到的是HTTP的两种请求方法之一的GET方法,
下面的Assignment就只需要把 \hello
换成 \lab0\sunetid
即可,这里就不多说了。
2.2 发送邮件给自己
现在你知道如何获取网页,是时候发送一封电子邮件了,再次使用可靠的字节流连接到运行在另一台计算机上的服务.............................。
不啦不啦说了一堆,其实是给他们学生玩的,咱们没那条件,略
2.3 Listening and connecting
你已经看到了telnet的使用方式:一个客户端程序,用于与运行在其他计算机上的程序建立出站连接。现在是时候尝试成为一个简单的服务器了:一种等待客户端连接的程序。
1. 在一个终端窗口中,在你的虚拟机上运行 `netcat -v -l -p 9090`。你应该会看到:
user@computer:~$ netcat -v -l -p 9090
Listening on [0.0.0.0] (family 0, port 9090)
2. 让 netcat 保持运行。在另一个终端窗口中,在你的虚拟机上运行 `telnet localhost 9090`。
3. 如果一切顺利,netcat 将打印类似 "Connection from localhost 53500 received!" 的内容。
4. 现在尝试在任一终端窗口中输入内容,无论是 netcat(服务器)还是 telnet(客户端)。注意,你在一个窗口中输入的任何内容都会在另一个窗口中显示,反之亦然。你需要按 Enter 键才能传输字节。
5. 在 netcat 窗口中,通过输入 Ctrl+C 退出程序。这将导致 telnet 窗口中的连接立即终止。
玩过网络编程的都应该能理解,其实就是一个简单的CS模型来进行两个进程之间的通信。使用netcat
(一种用于通过TCP或UDP读取和写入网络连接的工具)创建一个简单的服务器,netcat -v -l -p 9090
用 netcat
监听 9090 端口,然后在局域网中用 telnet
再和 127.0.0.1:9090
建立连接后就可以通信了。没什么说的,过!
3 使用操作系统流套接字编写网络程序
在这个热身实验的下一部分,你将编写一个简短的程序,通过互联网获取一个网页。你将利用Linux内核和大多数其他操作系统提供的一个功能:在两个程序之间创建一个可靠的双向字节流,一个运行在你的计算机上,另一个运行在互联网的另一台计算机上(例如,一个Web服务器,如Apache或nginx,或者netcat程序)。
这个功能被称为流套接字(stream socket)。对于你的程序和Web服务器而言,套接字看起来像是一个普通的文件描述符(类似于磁盘上的文件,或标准输入/输出流)。当两个流套接字连接在一起时,写入一个套接字的任何字节最终都会按相同的顺序从另一个套接字中出现在另一台计算机上。
然而,实际上,互联网并不提供可靠的字节流服务。相反,互联网真正做的事情只是尽最大努力将称为Internet datagrams的短数据片段传递到它们的目的地。每个数据报包含一些元数据(标头),指定源地址和目标地址,即它来自哪台计算机,以及它要传送到哪台计算机,以及一些负载数据(最多约1,500字节),以便传递到目标计算机。
虽然网络试图传递每个数据报,但在实践中,数据报可能会(1)丢失,(2)无序传递,(3)传递时内容发生更改,甚至(4)复制并传递多次。通常,连接两端的操作系统的工作是将最大努力的数据报(互联网提供的抽象)转换为可靠的字节流(应用程序通常需要的抽象)。
这两台计算机必须合作,以确保流中的每个字节最终以正确的顺序传递到另一侧的流套接字。它们还必须告诉对方它们准备从另一台计算机接受多少数据,并确保不发送超过对方愿意接受的量。所有这些都是使用在1981年确立的一种被称为传输控制协议(TCP)的协议来完成的。
在这个实验中,你将简单地使用操作系统对传输控制协议的预先存在的支持。你将编写一个名为`webget`的程序,它创建一个TCP流套接字,连接到一个Web服务器,并获取一个页面,就像你在这个实验的前面部分所做的那样。在将来的实验中,你将实现这个抽象的另一侧,通过自己实现传输控制协议,将不太可靠的数据报转换为可靠的字节流。
他说了这么多,目的只有一个,就是告诉你套接字的机制,然后现在让你用linux自带的套接字去实现这个机制来体验一下套接字传输。其中他给你对端也就是服务器端了相当于,你需要实现的是客户端的动作。
说了这么多你可能还是云里雾里,那我们继续往下看。
3.1 让我们开始获取和构建启动代码
1. 实验任务将使用一个名为Sponge的起始代码库。在你的虚拟机上运行 `git clone https://github.com/cs144/sponge` 来获取实验的源代码。
2. 可选:随时备份你的存储库到一个私有的 GitHub/GitLab/Bitbucket 存储库(例如,使用 https://stackoverflow.com/questions/10065526/github-how-to-make-a-fork-of-public-repository-private 上的说明),但请确保你的工作保持私有。
3. 进入 Lab 0 目录:`cd sponge`
4. 创建一个目录用于编译实验软件:`mkdir build`
5. 进入 build 目录:`cd build`
6. 配置构建系统:`cmake ..`
7. 编译源代码:`make`(你可以运行 `make -j4` 来使用四个处理器)
8. 在 build 目录外,打开并开始编辑 `writeups/lab0.md` 文件。这是你实验报告的模板,将包含在你的提交中。
其实也没有他说的那么复杂,git我们都用过,没用过install一个就行,然后我们把仓库clone下来,里面就是他做的一些基础框架demo,我们要做的就是在他这么一大坨里塞入TCP的灵魂,跑题了,回过来我们看怎么构建他这个项目。
clone下来之后我们新建一个build目录,作为我们的构建目录,然后cd到build目录中,使用cmake ..
,初始化他的构建文件。这里的cmake之前讲环境没有提到,
我这里版本比较高,理论上正常install的版本就可以了。
回过头来,我们在build目录下cmake好后,我们使用make命令来构建我们的项目。然后使用make check_webget
就可以测试用例了。当然,我们在没实现功能之前肯定是不会通过的。
3.2 标准C++代码风格
这里介绍了一下实验的代码规范诸如
禁用动态内存分配(malloc&free、new&delete)
禁用裸指针、如果要用指针就用智能指针,不过看提示说CS144不太用得到指针
避免模板、线程、锁、虚函数,说CS144用不到这些
避免使用C风格字符串和C的那一套str-函数,用std::string代替
避免使用C风格cast,用static_cast代替,不过CS144一般用不到
尽量使用const引用传参
函数和变量能用const修饰则用const修饰
避免使用全局变量,把变量限定在尽可能小的作用域里
使用cmake --build build --target tidy
获取代码建议,cmake --build build --target format
去格式化
都是比较常规的代码规范,没什么好说的,对了,如果你的IDE对某些函数报警(报错)(例如string_view),请把IDE的语言环境调成C++20以上,在设置里搜cppstandard就可以了,很简单。
翻译就不翻译了,大概就是这样,我刚开始做的时候以为这块没啥重要的,不就是正常的代码规范吗,但是做到最后才发现还是有用的,尤其是一些写法对于性能的影响,很多小细节集合起来就会导致比较严重的性能问题,所以还是要注意一下这一点。
3.3阅读 Sponge 文档
为了支持这种编程风格,Sponge 的类将操作系统函数(可以从 C 中调用的函数)用现代 C++ 进行封装。
1. 使用 Web 浏览器,阅读起始代码的文档,网址为 https://cs144.github.io/doc/lab0。
2. 特别关注 FileDescriptor、Socket、TCPSocket 和 Address 类的文档。(注意,Socket 是 FileDescriptor 的一种类型,而 TCPSocket 是 Socket 的一种类型。)
3. 现在,在 libsponge/util 目录中找到并阅读描述这些类接口的头文件:file descriptor.hh、socket.hh 和 address.hh。
当然,读文档是每个项目开启的必经之路,我们需要了解项目demo封装的各个函数的意思,只有了解了这些,才能注入灵魂。我当时做的时候是把代码中的注释逐字逐句翻译了下来,总体看下来不是特别困难,这部分就比较考研耐心了,据统计,程序员在参与一个项目开发的过程中五分之三的时间都在阅读文档,当然,现实确实是这样的,加油!
读完我们需要几个事情:使用TCPSocket和Address类,在HTTP中,每一行都必须以“\r\n”结尾,要包含“Connection: close”告诉服务器请求结束,否则服务器会进行回复,确保读取并打印来自服务器的所有输出,直到套接字达到“EOF”为止。客户端TCP模拟http发送GET的格式为 GET请求方式+空格+路径+空格+HTTP/1.1
没什么问题了我们开始写了就可以。
3.4Writing webget
到了实现 `webget` 的时候,这是一个用于通过操作系统的TCP支持和流套接字抽象从互联网获取网页的程序,就像你在这个实验的前面部分手动操作一样。
1. 从 build 目录中,使用文本编辑器或集成开发环境(IDE)打开文件 `../apps/webget.cc`。
2. 在 `get_URL` 函数中,找到以 `// Your code here` 开始的注释。
3. 按照该文件中的描述实现简单的 Web 客户端,使用你之前手动操作时使用的 HTTP(Web)请求格式。使用 `TCPSocket` 和 `Address` 类。
4. 提示:
- 请注意,在 HTTP 中,每一行都必须以 `rn` 结束(仅使用 `n` 或 `endl` 是不够的)。
- 不要忘记在你的客户端请求中包含 `Connection: close` 行。这告诉服务器在此请求后不应等待你的客户端再发送更多的请求。相反,服务器将发送一个回复,然后立即结束其传出字节流(从服务器的套接字到你的套接字)。当你读取来自服务器的整个字节流时,你的套接字将到达 EOF(文件结束),这是你的客户端将知道服务器已完成其回复的方式。
- 确保读取并打印从服务器接收的所有输出,直到套接字达到 EOF(文件结束)为止,一次 `read` 调用是不够的。
- 我们预计你可能需要编写大约十行代码。
5. 运行 `make` 编译你的程序。如果看到错误消息,需要在继续之前修复它。
6. 运行 `./apps/webget cs144.keithw.org /hello` 来测试你的程序。这与在 Web 浏览器中访问 `http://cs144.keithw.org/hello` 有什么区别?与第2.1节的结果相比如何?随意进行实验,尝试使用任何你喜欢的 http URL 进行测试!
7. 当程序似乎正常工作时,运行 `make check` 来运行自动化测试。在实现 `get_URL` 函数之前,你应该期望看到以下输出:
1/1 Test #25: lab0_webget ......................***Failed
Function called: get_URL(cs144.keithw.org, /hasher/xyzzy).
Warning: get_URL() has not been implemented yet.
0.00 sec
ERROR: webget returned output that did not match the tests expectations
4/4 Test #4: lab0_webget ...................... Passed
100% tests passed, 0 tests failed out of 4
0.14 sec
评分者将使用与 `make check` 运行不同的主机名和路径运行你的 `webget` 程序,所以确保它不仅仅适用于 `make check` 使用的主机名和路径。
代码就长这个样子,很简单,和我们正常网络编程差不多,唯一要注意的是我们在读取socket的时候一定要注意他的要求是确保读取并打印来自服务器的所有输出,直到EOF为止,这个位置可能会判断结束标志有问题?反正我是挺顺利的,大概就是这样。写完记得关闭读写和socket也就是close一下就行了。
然后我们在build目录下make check_webget来跑测试点就可以了。
正确的话是这样滴。
到这我们的webget就实现完了。
4 双向字节流
到目前为止,你已经看到了可靠字节流的抽象在通过互联网通信中的用途,即使互联网本身只提供了最佳努力(不可靠)数据报的服务。
为了完成本周的实验,你将在单台计算机上的内存中实现一个对象,提供这个抽象。在输入端写入字节,然后可以从输出端以相同的顺序读取。字节流是有限的:写入者可以结束输入,然后就不能再写入更多字节了。当读者读到流的末尾时,它将达到 EOF(文件结束),不能再读取更多字节。
你的字节流还将进行流量控制,以限制其在任何给定时间的内存消耗。该对象被初始化为特定的容量:在任何给定时刻,它愿意存储在自己的内存中的最大字节数。字节流将限制写入者在任何给定时刻可以写入多少字节,以确保流不超过其存储容量。随着读者读取字节并从流中排空它们,写入者将被允许写入更多。你的字节流用于单线程,你不必担心并发的写入者/读者、锁定或竞态条件。
明确一下:字节流是有限的,但在写入者结束输入并完成流之前,它几乎可以是任意长的。你的实现必须能够处理比容量长得多的流。容量限制了在给定时刻保存在内存中的字节数(已写入但尚未读取的字节),但不限制流的长度。具有只有一个字节容量的对象仍然可以携带非常长的流,只要写入者一次写入一个字节,读者在允许写入者写入下一个字节之前读取每个字节。
以下是写入者的接口:
// Write a string of bytes into the stream. Write as many
// as will fit, and return the number of bytes written.
size_t write(const std::string &data);
// Returns the number of additional bytes that the stream has space for
size_t remaining_capacity() const;
// Signal that the byte stream has reached its ending
void end_input();
// Indicate that the stream suffered an error
void set_error();
以下是读者的接口:
// Peek at next "len" bytes of the stream
std::string peek_output(const size_t len) const;
// Remove len bytes from the buffer
void pop_output(const size_t len);
// Read (i.e., copy and then pop) the next "len" bytes of the stream
std::string read(const size_t len);
bool input_ended() const; // true if the stream input has ended
bool eof() const; // true if the output has reached the ending
bool error() const; // true if the stream has suffered an error
size_t buffer_size() const; // the maximum amount that can currently be peeked/read
bool buffer_empty() const; // true if the buffer is empty
size_t bytes_written() const; // Total number of bytes written
size_t bytes_read() const; // Total number of bytes popped
请打开 `libsponge/byte stream.hh` 和 `libsponge/byte stream.cc` 文件,并实现一个提供此接口的对象。在开发字节流实现时,可以使用 `make check lab0` 运行自动化测试。
接下来是什么?在接下来的四周,你将实现一个系统,提供相同的接口,不再是在内存中,而是通过一个不可靠的网络。这就是传输控制协议(TCP)。
emmmm,AI的翻译可能有点生硬,我来解释一下吧。
接下来我们需要实现的是一个字节流,什么样的字节流呢?
我们都知道流水线,物品加工的过程需要这个流水线的运作来将原材料加工成成品。同样,在计算机中,我们有许许多多的流水线,在数据传输中,也有着几条重要的流水线,我们这里实现的字节流就是整个流水线的基类,他撑起来下面构造相关高级流水线的基础,所以个bytestream的实现特别重要,我在实现lab4的时候,就是因为这个bytestream写错了,而且lab0-3还都能过,到lab4就一直检查不出来错误,最后回过头来才恍然大悟发现是流写错了,所以这里一定要重视。
其实这个流很简单,我们完全可以类比流水线,文档让我们去读byte_stream.hh和byte_stream.cc的文档,我们一定要老老实实全读一遍,尤其是注释。
这里我用的是双端队列实现的,我这里给一个提示,尽量少用for循环,能用函数尽量用函数,我个人是竞赛出身喜欢用for,但是在实际内存调用中在每次循环中都进行了定义拷贝粘贴等操作,所以我们一定要尽可能用函数来实现功能,自带函数往往在底层进行了很多层面的优化,不要小看这些细节,尤其是屎山堆砌完后,这些小细节更会体现他们的重要性。
又跑题了,我们回过头了,看一看函数构成,这几个函数实现都比较简单,参考这个图
只有一个eof函数需要注意一下,我最开始是判断容器空就是到流结束了,但是我们需要考虑当流还未结束,流中的字节也可能空啊,所以这里需要多判断一个写入是否end,当然,这个写入是否end就取决于上层调用了,和我们没关系。
最后make && make check_lab0测试就可以了
代码奉上,尽量自己写!!!!!!
byte_stream.hh
byte_stream.cc
至此,lab0就完事了。