前言
上一篇部落格中我們完成了 Lab0,使用雙端佇列實現了一個位元組流類 ByteStream
,可以向位元組流中寫入資料並按寫入順序讀出資料。由於網路環境的變化,傳送端滑動視窗內的資料包到達接收端時可能失序,所以接收端收到資料之後不能直接寫入 ByteStream
中,而是應該快取下來並按照序號重組成正確的資料。這篇部落格所介紹的 Lab1 將實現一個位元組流重組器 StreamReassambler
來完成上述任務。
實驗要求
接收方的資料情況如下圖所示,藍色部分表示已消費的資料,綠色表示已正確重組但是還沒消費的資料,紅色則是失序到達且還沒重組的資料:
由於接收端緩衝區大小 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
,最小值為 0eof
:是不是最後一個資料包
三個引數中,最耐人尋味的就是 index
引數,如果只是單純的失序到達,資料之間沒有發生重疊,Lab1 就比較好做了,但是實驗指導書中明確指出
May substrings overlap? Yes
這就比較難搞了,因為重疊分成兩種:
-
前面一部分與已重組的資料發生重疊
-
前面不與已重組的資料發生重疊
實際上由於 data
的末尾可能超出 first unacceptable
,需要對超出部分進行截斷,這可能導致 eof
標誌失效,但是問題不大,傳送方之後會重新傳送這個資料包。
程式碼實現
為了處理上述重疊情況,需要一個 _next_index
成員代表 first unassembled
索引,一個 _unassembles
雙端佇列代表 first unassembled
到 first 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
標籤頁,在程式碼中打下斷點, 點選綠色按鈕就能開始除錯了:
除錯效果如下圖所示:
後記
透過這次實驗,可以加深對接收端資料重組和分組序號的瞭解,期待後面的幾個實驗,以上~~