CS144 計算機網路 Lab1:Stream Reassembler

之一Yo發表於2023-04-20

前言

上一篇部落格中我們完成了 Lab0,使用雙端佇列實現了一個位元組流類 ByteStream,可以向位元組流中寫入資料並按寫入順序讀出資料。由於網路環境的變化,傳送端滑動視窗內的資料包到達接收端時可能失序,所以接收端收到資料之後不能直接寫入 ByteStream 中,而是應該快取下來並按照序號重組成正確的資料。這篇部落格所介紹的 Lab1 將實現一個位元組流重組器 StreamReassambler 來完成上述任務。

CS144 TCPSocket 架構

實驗要求

接收方的資料情況如下圖所示,藍色部分表示已消費的資料,綠色表示已正確重組但是還沒消費的資料,紅色則是失序到達且還沒重組的資料:

資料分類

由於接收端緩衝區大小 capacity 有限,超出容量的資料(first unacceptable 之後的資料)將被丟棄,這些被丟棄的資料包將起到流量控制的作用,可以限制傳送端滑動視窗的大小。

流重組器的介面如下所示:

StreamReassembler(const size_t capacity);

//! \brief Receives a substring and writes any newly contiguous bytes into the stream.
//!
//! If accepting all the data would overflow the `capacity` of this
//! `StreamReassembler`, then only the part of the data that fits will be
//! accepted. If the substring is only partially accepted, then the `eof`
//! will be disregarded.
//!
//! \param data the string being added
//! \param index the index of the first byte in `data`
//! \param eof whether or not this segment ends with the end of the stream
void push_substring(const std::string &data, const uint64_t index, const bool eof);

//! Access the reassembled byte stream
const ByteStream &stream_out() const { return _output; }
ByteStream &stream_out() { return _output; }

//! The number of bytes in the substrings stored but not yet reassembled
size_t unassembled_bytes() const;

//! Is the internal state empty (other than the output stream)?
bool empty() const;

其中最重要的函式就是 StreamReassambler::push_substring(),接收方收到資料之後就會呼叫此函式將資料儲存起來。此函式接受三個引數:

  • data: 接收到的資料
  • index: 資料的第一個位元組的索引,由於原始資料可能很大,超過了 TCPSegment 的容量,所以會將原始資料切分成多個片段,每個片段的第一個位元組的索引就是 index,最小值為 0
  • eof:是不是最後一個資料包

三個引數中,最耐人尋味的就是 index 引數,如果只是單純的失序到達,資料之間沒有發生重疊,Lab1 就比較好做了,但是實驗指導書中明確指出

May substrings overlap? Yes

這就比較難搞了,因為重疊分成兩種:

  1. 前面一部分與已重組的資料發生重疊
    重疊情況1

  2. 前面不與已重組的資料發生重疊

    重疊情況2

實際上由於 data 的末尾可能超出 first unacceptable,需要對超出部分進行截斷,這可能導致 eof 標誌失效,但是問題不大,傳送方之後會重新傳送這個資料包。

程式碼實現

為了處理上述重疊情況,需要一個 _next_index 成員代表 first unassembled 索引,一個 _unassembles 雙端佇列代表 first unassembledfirst unacceptable 之間的資料,由於裡面可能只有一部分資料是有效的,所以用一個遮罩 _unassembled_mask 指出哪些資料是有效但是還沒重組的。

class StreamReassembler {
  private:
    ByteStream _output;  //!< The reassembled in-order byte stream
    size_t _capacity;    //!< The maximum number of bytes
    std::deque<char> _unassembles{};
    std::deque<bool> _unassemble_mask{};
    size_t _unassambled_bytes{0};
    uint64_t _next_index{0};
    bool _is_eof{false};

    /** @brief 將資料寫入未重組佇列中
     * @param data 將被寫入的字串
     * @param dstart 字串開始寫入的位置
     * @param len 寫入的長度
     * @param astart 佇列中開始寫入的位置
     */
    void write_unassamble(const std::string &data, size_t dstart, size_t len, size_t astart);

    /** @brief 重組資料
     */
    void assemble();

  public:
    StreamReassembler(const size_t capacity);

    //! \brief Receives a substring and writes any newly contiguous bytes into the stream.
    void push_substring(const std::string &data, const uint64_t index, const bool eof);

    //! \name Access the reassembled byte stream
    const ByteStream &stream_out() const { return _output; }
    ByteStream &stream_out() { return _output; }

    //! The number of bytes in the substrings stored but not yet reassembled
    size_t unassembled_bytes() const;

    bool empty() const;
};

收到資料時,先將不重疊的資料寫入 _unassembles 佇列中,之後呼叫 StreamReassabler::assemble() 函式重組佇列中的連續資料,並更新 _next_index

StreamReassembler::StreamReassembler(const size_t capacity)
    : _output(capacity), _capacity(capacity), _unassembles(capacity, '\0'), _unassemble_mask(capacity, false) {}

//! \details This function accepts a substring (aka a segment) of bytes,
//! possibly out-of-order, from the logical stream, and assembles any newly
//! contiguous substrings and writes them into the output stream in order.
void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
    if (index > _next_index + _capacity)
        return;

    if (eof)
        _is_eof = true;

    if (eof && empty() && data.empty()) {
        _output.end_input();
        return;
    }

    auto end_index = data.size() + index;

    // 新資料在後面
    if (index >= _next_index) {
        auto astart = index - _next_index;
        auto len = min(_output.remaining_capacity() - astart, data.size());
        if (len < data.size())
            _is_eof = false;

        write_unassamble(data, 0, len, astart);
    }
    // 新資料與已重組的資料部分重疊
    else if (end_index > _next_index) {
        auto dstart = _next_index - index;
        auto len = min(_output.remaining_capacity(), data.size() - dstart);
        if (len < data.size() - dstart)
            _is_eof = false;

        write_unassamble(data, dstart, len, 0);
    }

    // 最後合併資料
    assemble();
    if (_is_eof && empty())
        _output.end_input();
}

void StreamReassembler::write_unassamble(const string &data, size_t dstart, size_t len, size_t astart) {
    for (size_t i = 0; i < len; ++i) {
        if (_unassemble_mask[i + astart])
            continue;

        _unassembles[i + astart] = data[dstart + i];
        _unassemble_mask[i + astart] = true;
        _unassambled_bytes++;
    }
}

void StreamReassembler::assemble() {
    string s;
    while (_unassemble_mask.front()) {
        s.push_back(_unassembles.front());
        _unassembles.pop_front();
        _unassemble_mask.pop_front();
        _unassembles.push_back('\0');
        _unassemble_mask.push_back(false);
    }

    if (s.empty())
        return;

    _output.write(s);
    _next_index += s.size();
    _unassambled_bytes -= s.size();
}

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

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

在命令列中輸入:

cd build
make -j8
make check_lab1

可以看到測試用例也全部透過了:

測試透過

除錯程式碼

由於使用程式碼編輯器的是 VSCode,所以這裡給出在 VSCode 中除錯專案程式碼的方式。

tasks.json

首先在專案目錄下建立 .vscode 資料夾,並新建一個 tasks.json 檔案,在裡面寫入下述內容:

{
    "tasks": [
        {
            "type": "shell",
            "label": "cmake",
            "command": "cd build && cmake .. -DCMAKE_BUILD_TYPE=Debug",
            "detail": "CMake 生成 Makefile",
            "args": [],
            "problemMatcher": "$gcc"
        },
        {
            "type": "shell",
            "label": "build",
            "command": "cd build && make -j8",
            "detail": "編譯專案",
            "args": [],
            "problemMatcher": "$gcc"
        },
    ],
    "version": "2.0.0"
}

這裡主要配置了兩個任務,一個呼叫 CMake 生成 Makefile,一個編譯 Makefile。在 VSCode 中按下 Alt + T + R,就能在任務列表中看到這兩個任務,點選之後就能執行。

任務列表

launch.json

.vscode 資料夾中新建 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 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"
        }
    ]
}

之後開啟一個測試用例,比如 tests/fsm_stream_reassembler_seq.cc,轉到 debug 標籤頁,在程式碼中打下斷點, 點選綠色按鈕就能開始除錯了:

除錯測試用例

除錯效果如下圖所示:

除錯效果

後記

透過這次實驗,可以加深對接收端資料重組和分組序號的瞭解,期待後面的幾個實驗,以上~~

相關文章