前言
关于为什么选修这门课,为什么突然弃坑又为什么突然醒悟重新写这部分内容,请看 关于上个版本的 CS144 中的几篇文章
Envrionment Setting Up
关于我的环境,如下所示:
请注意文章创建的时间与工具版本之间的关系
- OS: Linux x86_64 (6.1.53-1-MANJARO)
- CPU: 13th Gen Intel i5-13500H (16) @ 4.700GHz
- GPU: Intel Raptor Lake-P [Iris Xe Graphics]
- Memory: 16GB
- Shell: zsh 5.9
- Editor: VS Code
- C Compiler: gcc(version 13.2.1) clang(version 16.0.6)
- C++ Compiler: g++(version 13.2.1) clang++(version 16.0.6)
- C/C++ build tools: cmake(version 3.27.6) xmake(version 2.8.3) make(version 4.4.1)
- Rust: rustc 1.73.0-nightly
- Python: Python 3.11.5
- Java: openjdk 21 2023-09-19
- Go: version go1.21.1 linux/amd64
不错,现在我可以用之前用不了的方案二了 :joy:
由于 sp23
使用的是 C++20
,所以保证你的 cmake
与 g++/clang++
支持 C++20
(建议直接和我用一个版本的)
拉取 github
上的代码,建议根据 How to make a fork of repo private 进行设置,然后拉取到本地:
How to debug
如何使用 VS Code
进行调试?
答案当然是自己写 launch.json
了,但由于本人水平不够,所以为每个过不去的测试都会写一个简单的配置,例如 check_webget
:
需要注意的点实际上就是 program
部分要写对,每个测试对应的可执行文件的名称叫什么能知道就行。如果需要多配置,在 configurations
中添加类似的配置即可。
你需要知道的一些预定义变量
- ${workspaceFolder} - 当前工作目录(根目录)
- ${workspaceFolderBasename} - 当前文件的父目录
- ${file} - 当前打开的文件名(完整路径)
- ${relativeFile} - 当前根目录到当前打开文件的相对路径(包括文件名)
- ${relativeFileDirname} - 当前根目录到当前打开文件的相对路径(不包括文件名)
- ${fileBasename} - 当前打开的文件名(包括扩展名)
- ${fileBasenameNoExtension} - 当前打开的文件名(不包括扩展名)
- ${fileDirname} - 当前打开文件的目录
- ${fileExtname} - 当前打开文件的扩展名
- ${cwd} - 启动时 task 工作的目录
- ${lineNumber} - 当前激活文件所选行
- ${selectedText} - 当前激活文件中所选择的文本
- ${execPath} - vscode 执行文件所在的目录
Lab0
实践部分跳过,从写代码的阶段开始,请遵守代码规范:
请使用 Vscode 中的
cmake/cmake tools
插件,可以省去很多麻烦,当然你想输命令来做也是可以的还有
clangd
来进行文件的索引(虽然感觉有些时候clangd
挺笨的)
怎么大括号要换行啊,忍不了了,直接改 .clang-format
:
Read Code
Please read over the public interfaces (the part that comes after “public:” in the files util/socket.hh and util/file_descriptor.hh. (Please note that a Socket is a type of FileDescriptor, and a TCPSocket is a type of Socket.)
这里我们需要阅读 libsponge/uitl/socket.hh
libsponge/uitl/address.hh
中对于 TCPSocket
,Socket
与 Address
的定义及用法。
实际上的要求并不复杂,我们需要建立一个 TCP
连接,然后发送一段预设好的消息,等到回复后,将其打印出来。
仔细阅读 文档即可完成,代码如下:
注意这里的 read
函数和之前不一样了,需要传入一个引用,而不是接收一个返回值。
如果你使用的编译器为
clang
,注意在src/byte_stream.hh
中需要添加头文件#include <cstdint>
否则会导致uint64_t
报错。
随后,进行检查,我们可以使用两种方法
如果你是命令行偏爱者,请输入:
进行第一步测试,如果通过了,那么可以直接跑写好的单元测试:
然后可以看见如下:
如果你不想敲命令行,那么可以使用之前说的 cmake
插件,如下图所示:
如果嫌弃 vsc 的日志输出没有高亮的话,可以下载一个插件
Build Output Colorizer
虽然这个好像也没什么太大的用处……
这个插件的测试不如命令行稳定,可能第一次测失败了,第二次就成功了(如果失败了可以转用命令行试试看)
An in-memory reliable byte stream
这里需要理解文档中所提到的 best effort
数据流,通俗来说,就是它只尽力传输,但不保证传输的正确性,你给他多少他就传多少。
通过文档中的介绍,我们很轻易就能发现 ByteStream
实际上就是一个循环队列,写的一方往队列里加东西,读的一方从队列里取东西,写者方到达容器极限但仍存在未写入的流时,写者会选择抛弃超出的部分。
我们需要完成的接口在文档中已列出,请 仔细阅读 每个函数后的注释,保证已经明确了这个函数的功能。
注意,这里 Writer
与 Reader
都继承自 ByteStream
,所以我们只需要向 ByteStream
添加成员即可。
我们可以为类添加新的 protected
成员以实现接口。
显然,我们需要添加容器,我们可以选择 STL
中提供的 deque
,但我选择自己从头构建一个循环队列(就当复习数据结构了),我们通过一个数组和两个指针来模拟循环队列:
注意,
CS144
的标准不能使用C
中的数组,只能使用STL
封装的vector
Init Version
我们从成员变量开始,由于我们考虑使用 vector
和双指针来模拟循环队列,并且考虑到接口中存在:
set_error() / has_error()
is_closed() / is_finished()
bytes_pushed() / bytes_poped()
所以我们可以设计一些变量,在传输的时候就进行维护,这样就可以直接返回而不需要重新计算了
我们从上面提到的简单接口开始实现,首先是构造函数:
这里使用了
resize
而非reserve
,是因为reserve
只会设置capacity
,当你第一次放元素进去的时候还是需要push_back / emplace_back
,并不适合我们这里的双指针。至于用
vector
的原因,就是我们并不知道std::array<typename Tp, size_t Nm>
中的Nm
值,其为RunTime
的。
简单的接口如下:
在这里,请注意 is_finished
的实现,如何保证已经传输完成?显然,我们需要保证两点:
- 输入端保证不会有输入了
- 缓冲区已经被清空了
而对于 byte_buffered
,我们注意到还有一个类似的函数 available_capacity
,但请注意,这两个函数不能调用(因为属于不同的类)
但其实我们可以发现 available_capacity() = capacity_ - byte_buffered()
所以我们先来实现 byte_buffered()
,实际上就是在求循环队列的长度:(队首 - 队尾 + 容量)% 容量
然而在这里我们需要特判一种情况:
由于队首和队尾相同的情况不一定是队列为空,也有可能是队列长度恰好等于总容量。
于是,若队列非空(也就是 buffer
非空)而计算的 result
却为 0,那么显然这时的队列并不是初始状态(完全空),相反的,整个缓冲区应该都是满的。
这样,我们显然也能实现 available_capacity
:
当 helper
函数都实现完后,接着就可以实现主要函数了,我们从较为简单的 Writer.push()
看起,由于是尽力传输,所以 push
会按照下面步骤来完成:
- 判断是否已经关闭输入端,如果是,那么不传输,否则继续
- 计算出当前缓冲区的剩余容量,如果需要传输的数据长度大于这个容量,那么我们只传输长度为剩余容量的子串,否则我们传输整个字符串
- 维护
write_count_
的值
代码就十分显然了:
当然这里会有一个
bug
,在后面会修改
对于 Reader
而言,我们有两个函数, peek
与 pop
,其中 peek
只是读出内容但并不会清理缓冲区,也不会对其他变量做任何操作,可以想象成 消费者只是先看一看,但并没有真的付钱把商品拿走 这种情况。
但不同的是,你会发现 peek
居然没有输入参数,这说明每次 peek
,Reader
都会把缓冲区内所有可读的东西扔出去(最好情况),那么你就会写出如下代码:
乍一看好像没什么问题,但注意
string_view
是无内存的,他只是引用了一个已经存在的内存地址而已,你可以把它当作一个切片。 而在上面那个函数中,result
引用了tmp
的内存地址,但tmp
是个局部变量,当函数返回时就会被销毁,那么返回回去的result
就成为了一个dangling reference
,我们永远无法取得存在于buffer_
的数据。
明白了这一点,实际上就好做了很多,我们可以让 string_view
直接引用 buffer_
,但请注意,这样的话我们就损失了一些吞吐率(没办法一次全取出来了,因为队列是循环的,会导致尾在头前面的情况),代码如下:
至于 pop
,就很显然了,并不需要太多的思考:
测试如下:
其错误为:
Final Version
出现这个错误的原因是:
我们上面的 push
和 peek
都没有处理传输的字符串其实是个空串的情况(其实主要是 push
,peek
的 substr
应该都已经算是处理了)
于是我们修改 push
如下:
对于 Reader 而言:
这样,就能通过所有测试点了
如果你更喜欢命令行,那么可以测试如下: