Featured image of post CS144(sp23) Lab0

CS144(sp23) Lab0

Stanford CS144 Spring 2023 实验环境与 Lab0

# 前言

关于为什么选修这门课,为什么突然弃坑又为什么突然醒悟重新写这部分内容,请看 关于上个版本的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

不错,现在我可以用之前用不了的方案二了 😂

由于 sp23 使用的是 C++20,所以保证你的 cmakeg++/clang++ 支持 C++20(建议直接和我用一个版本的)

拉取 github 上的代码,建议根据 How to make a fork of repo private 进行设置,然后拉取到本地:

git clone git@github.com:<username>/<repo-name>.git

# How to debug

如何使用 VS Code 进行调试?

答案当然是自己写 launch.json 了,但由于本人水平不够,所以为每个过不去的测试都会写一个简单的配置,例如 check_webget

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "cppdbg",
            "request": "launch",
            "name": "webget debug",
            "program": "${workspaceFolder}/build/apps/webget",
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}/build",
            "args": [
                "cs144.keithw.org",
                "/nph-hasher/xyzzy"
            ],
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "MIMode": "gdb",
        },
    ]
}

需要注意的点实际上就是 program 部分要写对,每个测试对应的可执行文件的名称叫什么能知道就行。如果需要多配置,在 configurations 中添加类似的配置即可。

你需要知道的一些预定义变量

  • ${workspaceFolder} - 当前工作目录(根目录)
  • ${workspaceFolderBasename} - 当前文件的父目录
  • ${file} - 当前打开的文件名(完整路径)
  • ${relativeFile} - 当前根目录到当前打开文件的相对路径(包括文件名)
  • ${relativeFileDirname} - 当前根目录到当前打开文件的相对路径(不包括文件名)
  • ${fileBasename} - 当前打开的文件名(包括扩展名)
  • ${fileBasenameNoExtension} - 当前打开的文件名(不包括扩展名)
  • ${fileDirname} - 当前打开文件的目录
  • ${fileExtname} - 当前打开文件的扩展名
  • ${cwd} - 启动时task工作的目录
  • ${lineNumber} - 当前激活文件所选行
  • ${selectedText} - 当前激活文件中所选择的文本
  • ${execPath} - vscode执行文件所在的目录

# Lab0

实践部分跳过,从写代码的阶段开始,请遵守代码规范:

image.png

请使用 Vscode 中的 cmake/cmake tools 插件,可以省去很多麻烦,当然你想输命令来做也是可以的

还有 clangd 来进行文件的索引(虽然感觉有些时候 clangd 挺笨的)

怎么大括号要换行啊,忍不了了,直接改 .clang-format

---
Language: Cpp
BasedOnStyle: Mozilla
IndentWidth: 4
AccessModifierOffset: -4
ColumnLimit: 116
SpacesInParentheses: true
AlwaysBreakAfterReturnType: None
AlwaysBreakAfterDefinitionReturnType: None
SpaceBeforeCpp11BracedList: true
BreakBeforeBinaryOperators: All
Cpp11BracedListStyle: true
AllowShortBlocksOnASingleLine: Always
BreakBeforeBraces: Custom
BraceWrapping:
  AfterClass: false
  AfterControlStatement: false
  AfterFunction: false
  AfterStruct: false
  AfterEnum: false
  SplitEmptyFunction: false
  SplitEmptyRecord: false
  SplitEmptyNamespace: false
PackConstructorInitializers: NextLine
...

# 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 中对于 TCPSocketSocketAddress 的定义及用法。

实际上的要求并不复杂,我们需要建立一个 TCP 连接,然后发送一段预设好的消息,等到回复后,将其打印出来。

==仔细阅读== 文档即可完成,代码如下:

void get_URL( const string& host, const string& path ) {
    TCPSocket socket = TCPSocket();
    socket.connect( Address( host, "http" ) );
    socket.write( "GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n" );
    socket.shutdown( SHUT_WR );
    string buffer;
    while ( !socket.eof() ) {
        socket.read( buffer );
        cout << buffer;
    }
}

注意这里的 read 函数和之前不一样了,需要传入一个引用,而不是接收一个返回值。

如果你使用的编译器为 clang,注意在 src/byte_stream.hh 中需要添加头文件 #include <cstdint> 否则会导致 uint64_t 报错。

随后,进行检查,我们可以使用两种方法

如果你是命令行偏爱者,请输入:

cd build 
make -j`nproc`
./apps/webget cs144.keithw.org /hello

进行第一步测试,如果通过了,那么可以直接跑写好的单元测试:

# if you are in the `build` directory
cd ..
cmake --build build --target check_webget

然后可以看见如下:

image.png

如果你不想敲命令行,那么可以使用之前说的 cmake 插件,如下图所示:

image.png

如果嫌弃 vsc 的日志输出没有高亮的话,可以下载一个插件 Build Output Colorizer

虽然这个好像也没什么太大的用处……

这个插件的测试不如命令行稳定,可能第一次测失败了,第二次就成功了(如果失败了可以转用命令行试试看)

# An in-memory reliable byte stream

这里需要理解文档中所提到的 best effort 数据流,通俗来说,就是它只尽力传输,但不保证传输的正确性,你给他多少他就传多少。

通过文档中的介绍,我们很轻易就能发现 ByteStream 实际上就是一个循环队列,写的一方往队列里加东西,读的一方从队列里取东西,写者方到达容器极限但仍存在未写入的流时,写者会选择抛弃超出的部分。

我们需要完成的接口在文档中已列出,请 ==仔细阅读== 每个函数后的注释,保证已经明确了这个函数的功能。

image.png

注意,这里 WriterReader 都继承自 ByteStream,所以我们只需要向 ByteStream 添加成员即可。

我们可以为类添加新的 protected 成员以实现接口。

显然,我们需要添加容器,我们可以选择 STL 中提供的 deque ,但我选择自己从头构建一个循环队列(就当复习数据结构了),我们通过一个数组和两个指针来模拟循环队列:

注意,CS144 的标准不能使用 C 中的数组,只能使用 STL 封装的 vector

# Init Version

我们从成员变量开始,由于我们考虑使用 vector 和双指针来模拟循环队列,并且考虑到接口中存在:

  1. set_error() / has_error()
  2. is_closed() / is_finished()
  3. bytes_pushed() / bytes_poped()

所以我们可以设计一些变量,在传输的时候就进行维护,这样就可以直接返回而不需要重新计算了

class ByteStream {
protected:
    uint64_t capacity_;
    uint64_t write_index_ { 0 };
    uint64_t read_index_ { 0 };
    uint64_t read_count_ { 0 };
    uint64_t write_count_ { 0 };
    bool closed_ { false };
    bool error_ { false };
    std::vector<char> buffer_;
// ... other code
}

我们从上面提到的简单接口开始实现,首先是构造函数:

ByteStream::ByteStream( uint64_t capacity ) : capacity_( capacity ){
    buffer_.resize( capacity );
}

这里使用了 resize 而非 reserve,是因为 reserve 只会设置 capacity,当你第一次放元素进去的时候还是需要 push_back / emplace_back,并不适合我们这里的双指针。

至于用 vector 的原因,就是我们并不知道 std::array<typename Tp, size_t Nm> 中的 Nm 值,其为RunTime的。

简单的接口如下:

void Writer::close() {
    // Your code here.
    closed_ = true;
}

void Writer::set_error() {
    // Your code here.
    error_ = true;
}

bool Writer::is_closed() const {
    // Your code here.
    return closed_;
}

uint64_t Writer::bytes_pushed() const {
    // Your code here.
    return write_count_;
}

bool Reader::is_finished() const {
    // Your code here.
    return closed_ && bytes_buffered() == 0;
}

bool Reader::has_error() const {
    // Your code here.
    return error_;
}

uint64_t Reader::bytes_popped() const {
    // Your code here.
    return read_count_;
}

在这里,请注意 is_finished 的实现,如何保证已经传输完成?显然,我们需要保证两点:

  1. 输入端保证不会有输入了
  2. 缓冲区已经被清空了

而对于 byte_buffered,我们注意到还有一个类似的函数 available_capacity,但请注意,这两个函数不能调用(因为属于不同的类)

但其实我们可以发现 available_capacity() = capacity_ - byte_buffered()

所以我们先来实现 byte_buffered(),实际上就是在求循环队列的长度:(队首 - 队尾 + 容量)% 容量

然而在这里我们需要特判一种情况:

由于队首和队尾相同的情况不一定是队列为空,也有可能是队列长度恰好等于总容量。

于是,若队列非空(也就是 buffer 非空)而计算的 result 却为 0,那么显然这时的队列并不是初始状态(完全空),相反的,整个缓冲区应该都是满的。

uint64_t Reader::bytes_buffered() const {
    // Your code here.
    auto result = ( write_index_ - read_index_ + capacity_ ) % capacity_;
    if ( !result && write_count_ > read_count_ ) {
        result = capacity_;
    }
    return result;
}

这样,我们显然也能实现 available_capacity

uint64_t Writer::available_capacity() const {
    // Your code here.
    uint64_t busy = ( write_index_ - read_index_ + capacity_ ) % capacity_;
    if ( write_count_ != read_count_ && !busy ) {
        busy = capacity_;
    }
    return capacity_ - busy;
}

helper 函数都实现完后,接着就可以实现主要函数了,我们从较为简单的 Writer.push() 看起,由于是尽力传输,所以 push 会按照下面步骤来完成:

  1. 判断是否已经关闭输入端,如果是,那么不传输,否则继续
  2. 计算出当前缓冲区的剩余容量,如果需要传输的数据长度大于这个容量,那么我们只传输长度为剩余容量的子串,否则我们传输整个字符串
  3. 维护 write_count_ 的值

代码就十分显然了:

void Writer::push( string data ) {
    // Your code here.
    if ( closed_ ) {
        return;
    }
    auto remaining = available_capacity();
    if ( remaining >= data.size() ) {
        for ( auto& i : data ) {
            buffer_[write_index_] = i;
            write_index_ = ( write_index_ + 1 ) % capacity_;
        }
        write_count_ += data.size();
    } else if ( remaining > 0 ) {
        data = data.substr( 0, remaining );
        for ( auto& i : data ) {
            buffer_[write_index_] = i;
            write_index_ = ( write_index_ + 1 ) % capacity_;
        }
        write_count_ += remaining;
    }
}

当然这里会有一个 bug,在后面会修改

对于 Reader 而言,我们有两个函数, peekpop,其中 peek 只是读出内容但并不会清理缓冲区,也不会对其他变量做任何操作,可以想象成 消费者只是先看一看,但并没有真的付钱把商品拿走 这种情况。

但不同的是,你会发现 peek 居然没有输入参数,这说明每次 peekReader 都会把缓冲区内所有可读的东西扔出去(最好情况),那么你就会写出如下代码:

string_view Reader::peek() const {
    // Your code here.
    auto reamining = byte_buffered();
    vector<char> tmp;
    uint64 count = 0, index = read_index;
    while(count < remaining){
	    tmp.emplace_back(buffer_[index]);
	    index = ( index + 1 ) % capacity_;
    }
    auto result = string_view(tmp.begin(), tmp.end());
    return result;
}

乍一看好像没什么问题,但注意

string_view 是无内存的,他只是引用了一个已经存在的内存地址而已,你可以把它当作一个切片。 而在上面那个函数中,result 引用了 tmp 的内存地址,但 tmp 是个局部变量,当函数返回时就会被销毁,那么返回回去的 result 就成为了一个 dangling reference,我们永远无法取得存在于 buffer_ 的数据。

明白了这一点,实际上就好做了很多,我们可以让 string_view 直接引用 buffer_,但请注意,这样的话我们就损失了一些吞吐率(没办法一次全取出来了,因为队列是循环的,会导致尾在头前面的情况),代码如下:

string_view Reader::peek() const {
    // Your code here.
    auto result = string_view( buffer_.begin(), buffer_.end() );
    if ( write_index_ > read_index_ ) {
        result = result.substr( read_index_, write_index_ - read_index_ );
    } else {
        result = result.substr( read_index_ );
    }
    return result;
}

至于 pop,就很显然了,并不需要太多的思考:

void Reader::pop( uint64_t len ) {
    // Your code here.
    read_index_ = ( read_index_ + len ) % capacity_;
    read_count_ += len;
}

测试如下:

image.png

其错误为:

image.png

# Final Version

出现这个错误的原因是:

image.png

我们上面的 pushpeek 都没有处理传输的字符串其实是个空串的情况(其实主要是 pushpeeksubstr 应该都已经算是处理了)

于是我们修改 push 如下:

void Writer::push( string data ) {
    // Your code here.
    if ( closed_ ) {
        return;
    }
    if ( data.empty() ) {
        return;
    }
    uint64_t remaining = available_capacity();
    if ( remaining >= data.size() ) {
        for ( auto& i : data ) {
            buffer_[write_index_] = i;
            write_index_ = ( write_index_ + 1 ) % capacity_;
        }
        write_count_ += data.size();
    } else if ( remaining > 0 ) {
        data = data.substr( 0, remaining );
        for ( auto& i : data ) {
            buffer_[write_index_] = i;
            write_index_ = ( write_index_ + 1 ) % capacity_;
        }
        write_count_ += remaining;
    }
}

对于 Reader 而言:

string_view Reader::peek() const {
    // Your code here.
    if ( bytes_buffered() == 0 ) {
        return {};
    }
    auto result = string_view( buffer_.begin(), buffer_.end() );
    if ( write_index_ > read_index_ ) {
        result = result.substr( read_index_, write_index_ - read_index_ );
    } else {
        result = result.substr( read_index_ );
    }
    return result;
}

这样,就能通过所有测试点了

image.png

如果你更喜欢命令行,那么可以测试如下:

image.png

使用 Hugo 构建