CS144 Lab 1

by on under CS144
4 minute read

上一个实验:Lab 0

这篇文章会讲解 CS144 在 vscode 上的 GDB 配置以及 Lab 1 的思路和代码。

GDB 配置

在 Lab 0 里我们不需要使用 GDB debug,但是在 Lab 1 里我们需要,这里是我配置 GDB 的办法,来源wine99 的博客,结合自己的系统情况修改了一下。首先将 Lab 0 的分支代码 merge 到 Lab 1 的对应分支里。然后当前 sponge 文件夹内,新建一个名为 .vscode 的文件夹,其中里面有四个 json 文件,分别是:

c_cpp_properties:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "${workspaceFolder}/libsponge/**"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/g++-8",
            "cStandard": "c11",
            "intelliSenseMode": "linux-clang-x64",
            "compileCommands": "${workspaceFolder}/build/compile_commands.json"
        }
    ],
    "version": 4
}

launch.json:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "debug lab test",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build/tests/${fileBasenameNoExtension}",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "miDebuggerPath": "/usr/bin/gdb"
        },
        {
            "name": "debug webget",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build/apps/webget",
            "args": ["cs144.keithw.org", "/hello"],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            // "preLaunchTask": "C/C++: g++ build active file",
            "preLaunchTask": "build project",
            "miDebuggerPath": "/usr/bin/gdb"
        },
        {
            "name": "debug current file",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}/${fileBasenameNoExtension}",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "C/C++: g++ build active file",
            "miDebuggerPath": "/usr/bin/gdb"
        }
    ]
}

settings.json:

{
    "debug.allowBreakpointsEverywhere": true,
    "files.associations": {
        "chrono": "cpp",
        "random": "cpp",
        "limits": "cpp",
        "algorithm": "cpp"
    }
}

tasks.json:

{
    "tasks": [
        {
            "type": "shell",
            "label": "C/C++: g++ build active file",
            "command": "/usr/bin/g++-8",
            "args": [
                "-g",
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        },
        {
            "type": "shell",
            "label": "build project",
            "command": "cd build && make -j8",
            "args": [],
        },
    ],
    "version": "2.0.0"
}

在配置好这些文件之后,我们就可以愉快地(并不)使用 GDB debug 了。

Lab 1

这个 Lab 的目标是写一个 StreamReassembler,它接受一些字符串和对应的 index,这里的 index 是字符串的第一个字符在 TCP 字节流的位置(起始值为0),按顺序拼好并传递给 Lab 0 实现的 ByteStream

Getting started

在开始写代码前,我们需要定义清楚一些问题,可以避免一些踩坑(我一开始没这么做,导致第一次快写完的时候整个重写了)。常见的需要定义清楚的问题有这么几个:

Q1:unassembled bytes 是什么?

A:是还未传递但是要传递给 ByteStream 的储存在 StreamReassembler 的字符数,其中如果不同的字符串有重叠部分的话,重叠部分只算一次;

举个例子,假设我们有两个字符串,一个是 Happy,它的 index 是 0,另一个是 py anniversary,它的 index 是 3,那么可想而知,整个算下来应该是 Happy anniversary,此时 unassembled bytes 值为17。

再举个例子,假设我们还是有两个字符串,一个是 Ha,它的 index 是 0,另一个是 y anniversary, 它的 index 是 3,此时的 unassembled bytes 值为 15,因为整个算下来是 Ha??y anniversary,我们不知道也没有存 ?? 位置的字符,所以这两个位置是不计数的。

Q2:capacity 是对什么容量的限制?

A:文档里的图解释的挺清楚的,我这里再复述一遍,是 StreamReassembler 里储存的 index 最大的字符的 index 减去在 ByteStream 里还没有被读取的 index 最小的字符的 index 的上限加一,说起来有点弯弯绕,结合下面这个例子会比较好。

假设 ByteStream 里还未读取的字符是 HappyStreamReassembler 里只有一个字符串,ver,它的index 是 10,那么此时它占据的容量是 13,因为组合到一起,是 Happy?????ver,尽管我们不知道 ????? 的内容,但是我们要为之预留空间。推广一下,如果读取的字符串有一部分超过了我们能接受的最大容量,即使我们有足够的内存,也要舍弃它,因为超出了 capacity 的限制。

Design the core data structures

就这个 Lab 来讲,我们需要一个数据结构来维护没有读取的字符串,并且这些字符串是有序的(index),其实我一开始的想法是用堆,完成并 debug 后过了绝大部分的测试例,只有关于 unassembled bytes 的测试有问题,仔细研究定义(如上)后,决定推倒重新写。

我们需要一个容器,可以容纳来自同一个字节流的字符串,并且我们需要跟踪字符数目,权衡之后我选择用 std::deque<char> _unassembled_bytes 来维护所有未传输给 ByteStream 的字符。如果这个队列空间不够存下下一个字符串的话,我们就扩大它,在整体不超过 capacity 的限制下存下尽量多的字符。这里问题来了,我们还需要思考,这个队列里存放的字符是不是已经读取的字符,比如我们读了 Happy?????ver,这些 ? 要如何表示,一开始我的想法是用 '\0' 表示,后来觉得如果字节流里有 '\0' 就不行,因此选用了另一个 std::deque<bool> _pos_checker 来记录 _unassembled_bytes 对应位置的字节是否是已经读取的字符,他们被同步更新,并且 _pos_checkertrue 的数量就是 unassembled bytes 的数值。

以下是一个更新字符串的实现:

void StreamReassembler::update_unassembled_bytes(const string &data, const size_t index) {
    size_t start_data_idx = max(0, static_cast<int>(_idx) - static_cast<int>(index));
    _max_idx = max(_max_idx, data.size() ? index + data.size() : 0); // fixed from 12-17
    size_t target_right_bound = min(index + data.size(), _capacity - _output.buffer_size() + _idx);
    if (target_right_bound > _right_bound) {
        // no space, need to expand it
        for (size_t i = 0; i < target_right_bound - _right_bound; i++) {
            _unassembled_bytes.push_back('\0');
            _pos_checker.push_back(false);
        }
        _right_bound = target_right_bound;
    }
    size_t start_deque_idx = start_data_idx + index - _idx;
    for (size_t i = 0; i + start_data_idx < data.size() && i + start_deque_idx < _pos_checker.size(); i++) {
        _unassembled_bytes[i + start_deque_idx] = data[i + start_data_idx];
        if (!_pos_checker[i + start_deque_idx])
            _unassembled_bytes_cnt++;
        _pos_checker[i + start_deque_idx] = true;
    }
}

这里有一些变量需要解释:

_idx:deque 里第一个位置对应的 index

_unassembled_bytes_cntunassembled bytes 的数值

_right_bound:deque 能储存的字符的最大 index 加一(恰好不能储存的序数)

_max_idx:已经得知存在的字符串最后一个字符的最大 index 加一,用来确定是否已经结束输入。假设我们的 StreamReassembler 要读取 Happy anniversary,且 index 为 0,可是由于容量限制,我们只能读取 Happy anniversar,由于最后一个 y 是存在的,我们只是暂时存不下,_max_idx 就是这个 y 对应的的 index 值,这个可以用来判断是否要给 ByteStream 传递输入结束的信号,不太好理解的话可以先跳过,看后续代码就清楚了。

Implement the whole class

由于我们的逻辑是尽可能地将字符传递给 ByteStream _output,所以每次更新后都要尽量 write

参考实现如下:

void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
    update_unassembled_bytes(data, index);
    // read
    while (_output.remaining_capacity() && !_unassembled_bytes.empty() && _pos_checker.front()) {
        char curr = _unassembled_bytes.front();
        string to_write(1, curr);
        _unassembled_bytes.pop_front();
        _pos_checker.pop_front();
        _output.write(to_write);
        _idx++;
        _unassembled_bytes_cnt --;
    }
    if (eof) {
        _eof = true;
    }
    if (_eof && _idx == _max_idx) {
        _output.end_input();
    }
}

其他函数代码:

size_t StreamReassembler::unassembled_bytes() const { return _unassembled_bytes_cnt; }

bool StreamReassembler::empty() const { return unassembled_bytes() == 0; }

完成后我们在 build 目录下编译并测试,和之前类似:

$ CXX=g++-8 cmake ..
$ make format
$ make -j4
$ make check_lab1

通过的话会得到类似字样:

100% tests passed, 0 tests failed out of 16

Total Test time (real) =   0.66 sec

下一个实验:Lab 2

CS144, computer networking
comments powered by Disqus