本文为我的斯坦福计算机网络课的编程实验(Lab Assignments)的学习总结。课程全称:CS 144: Introduction to Computer Networking。
事情发生于我读了半本《计算机网络:自顶向下方法》后,想要找点练手的东西,碰巧在知乎上看到了这个推荐帖:CS144: 什么,你学不会TCP?那就来自己写一个吧!。这门课的作业要求实现一个简单的TCP协议,自带充足评测程序,同时又比较有挑战性,我便欣然做之。
LAB0
在我开始做实验的时候官方不知为何已经删掉了sponge的github代码仓库,不过庆幸的是有huangrt01老哥早早做完了实验把自己的代码发到了github上,我克隆到本地后利用git回退到最初状态就能做了。不过因为CS144官方不喜欢大伙儿把完成的代码公开到网上,防止后来的学生抄作业,所以本文发布时huangrt01已经把仓库给设为private了。咱这里给出自己做完后打包好的仓库,寄存在gitee上(老外应该不会上gitee吧)。
另一件尴尬的事儿是CS144的2020年开了新课,旧版网页被覆盖掉了,我在gitee上拉了仓库镜像,利用gitee的pages功能搭建了官网2019年的镜像(即我做实验对应的版本)。
CS144官方网页:https://cs144.github.io/
我的官网镜像:https://kangyupl.gitee.io/cs144.github.io/
我的实验备份:https://gitee.com/kangyupl/sponge
LAB0初始代码对应着master的分支,我的题解放在solution分支下,有需要可以自行切换。
开工准备
首先要安装g++-8或clang-6,请根据自己的linux发行版自行搜索对应方法。我用的是g++-8,如果安装后CMAKE的时候还是提示g++版本不够,那就百度一下怎么把用g++-8替代gcc,再不行的话那就为gcc-8创建一个名为cc的软连接,为g++-8创建一个名为c++的软链接。
另外,如果你CMAKE的时候报出了如下错误:
CMake Error: The following variables are used in this project, but they are set to NOTFOUND.
Please set them or make sure they are set and tested correctly in the CMake files:
LIBPCAP
linked by target "udp_tcpdump" in directory /home/kangyu/sponge/apps
linked by target "ipv4_parser" in directory /home/kangyu/sponge/tests
linked by target "ipv4_parser" in directory /home/kangyu/sponge/tests
linked by target "tcp_parser" in directory /home/kangyu/sponge/tests
linked by target "tcp_parser" in directory /home/kangyu/sponge/tests
此时安装libpcap-dev库来解决,大多数的Linux发行版的软件源中应该都有这玩意。
Writing webget
要求实现get_URL函数,功能为向指定IP地址发送HTTP GET请求,然后输出所有响应。可参考配套Doc中[TCPSocket的示例代码](https://cs144.github.io/doc/lab0/class_t_c_p_socket.html。此外多读读讲义提示,注意下EOF和shutdown()的参数即可。
webget.cc
void get_URL(const string &host, const string &path) {
TCPSocket sock{
};
sock.connect(Address(host,"http"));
sock.write("GET "+path+" HTTP/1.1\r\nHost: "+host+"\r\n\r\n");
sock.shutdown(SHUT_WR);
while(!sock.eof()){
cout<<sock.read();
}
sock.close();
return;
}
An in-memory reliable byte stream
要求实现一个有序字节流类(in-order byte stream),使之支持读写、容量控制。这个字节流类似于一个带容量的队列,从一头读,从另一头写。当流中的数据达到容量上限时,便无法再写入新的数据。特别的,写操作被分为了peek和pop两步。peek为从头部开始读取指定数量的字节,pop为弹出指定数量的字节。
第一反应是搞个循环队列,容器基于数组,长度等于容量,这样内存被充分利用,效率也不错。不过讲义要求我们用“Modern C++”,避免用普通指针,所以我退而求其次用std::deque代替。为什么不用std::queue?因为queue只能访问开头的节点,无法实现peek操作。
byte_stream.hh
class ByteStream {
private:
// Your code here -- add private members as necessary.
std::deque<char> _buffer = {};
size_t _capacity = 0;
size_t _read_count = 0;
size_t _write_count = 0;
bool _input_ended_flag = false;
bool _error = false; //!< Flag indicating that the stream suffered an error.
//......
byte_stream.cc
ByteStream::ByteStream(const size_t capacity) : _capacity(capacity) {
}
size_t ByteStream::write(const string &data) {
size_t len = data.length();
if (len > _capacity - _buffer.size()) {
len = _capacity - _buffer.size();
}
_write_count += len;
for (size_t i = 0; i < len; i++) {
_buffer.push_back(data[i]);
}
return len;
}
//! \param[in] len bytes will be copied from the output side of the buffer
string ByteStream::peek_output(const size_t len) const {
size_t length = len;
if (length > _buffer.size()) {
length = _buffer.size();
}
return string().assign(_buffer.begin(), _buffer.begin() + length);
}
//! \param[in] len bytes will be removed from the output side of the buffer
void ByteStream::pop_output(const size_t len) {
size_t length = len;
if (length > _buffer.size()) {
length = _buffer.size();
}
_read_count += length;
while (length--) {
_buffer.pop_front();
}
return;
}
void ByteStream::end_input() {
_input_ended_flag = true; }
bool ByteStream::input_ended() const {
return _input_ended_flag; }
size_t ByteStream::buffer_size() const {
return _buffer.size(); }
bool ByteStream::buffer_empty() const {
return _buffer.size() == 0; }
bool ByteStream::eof() const {
return buffer_empty() && input_ended(); }
size_t ByteStream::bytes_written() const {
return _write_count; }
size_t ByteStream::bytes_read() const {
return _read_count; }
size_t ByteStream::remaining_capacity() const {
return _capacity - _buffer.size(); }
调试方法论
因为我用的是vscode,所以讲一下使用vscode时debug的方法:
如下图,在本次check中,测试样例t_strm_reassem_single出错。

以t_strm_reassem_single为关键词全局搜索可发现,所有的测试样例对应的命令都在spong/etc/tests.cmake中定义,我们从中找到需要的部分
add_test(NAME t_strm_reassem_cap COMMAND fsm_stream_reassembler_cap)
add_test(NAME t_strm_reassem_single COMMAND fsm_stream_reassembler_single)
add_test(NAME t_strm_reassem_seq COMMAND fsm_stream_reassembler_seq)
add_test(NAME t_strm_reassem_dup COMMAND fsm_stream_reassembler_dup)
add_test(NAME t_strm_reassem_holes COMMAND fsm_stream_reassembler_holes)
add_test(NAME t_strm_reassem_many COMMAND fsm_stream_reassembler_many)
add_test(NAME t_strm_reassem_overlapping COMMAND fsm_stream_reassembler_overlapping)
add_test(NAME t_strm_reassem_win COMMAND fsm_stream_reassembler_win)
可以发现t_strm_reassem_single对应的测试命令为fsm_stream_reassembler_single,test.cmake中的COMMAND都是以sponge/build/tests/作为“当前路径”执行的,在这里就相当于运行sponge/build/tests/fsm_stream_reassembler_single程序,

而所有测试程序对应的源文件存放在sponge/tests/下

如果使用GDB的话,现在可以直接通过gdb 程序路径进行调试。不过我使用的是基于GDB插件的vscode,需要对launch.json做点小修改。我把修改的行加了注释。
{
"version": "0.2.0",
"configurations": [
{
"name": "sponge debug",//!挑个容易识别的名字
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/tests/${fileBasenameNoExtension}", //!设置为测试程序源码相对应的目标程序路径
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
//"preLaunchTask": "C/C++: g++-8 build active file", //!不需要前置任务
"miDebuggerPath": "/usr/bin/gdb"
}
]
}
此时在vscode中切回fsm_stream_reassembler_harness.cc,打完断点后即可正常调试

LAB1
要求实现一个流重组器(stream reassembler),可以将带索引的字节流碎片重组成有序的字节流。每个字节流碎片都通过索引、长度、内容三要素进行描述。重组完的字节流应当被送入指定的字节流(byte stream)对象_output中。
特别注意:
0.这节需要安装pcap库和pcap-dev库才能正常编译,如果没编译没报错那就没事了。
1.碎片可能交叉或重叠。
2.如果某次新碎片到达后字节流的开头部分被凑齐,那就应当立刻把凑齐的部分立刻写入到_output中。即对应讲义中的:
When should bytes be written to the stream?
As soon as possible. The only situation in which a byte should not be in the stream is that when there is a byte before it that has not been “pushed” yet.
3.碎片可能是一个只包含EOF标志的空串
4.LAB0的顺序字节流和LAB1的流重组器各有各的容量限制。流重组器把字节流写满后,只有当字节流腾出空后才能继续写,相当于字节流满时流重组器出口被“堵住”了。同样当流重组器容量满了后自身也无法被写入新数据,此时到来的新碎片只能被丢弃掉。
第一反应联想到了操作系统里的进程内存管理,用一个二叉排序树来记录每个碎片的索引、长度,排序规则为按索引值升序,每次插入新碎片时判断能不能和前后碎片进行合并。流的内容则可以用一个数组来做缓冲区,或者干脆一块存储在二叉树的节点里。不过还是因为“Modern C++”的缘故,我再次退而求其次用std::list代替之。等我哼哧哼哧花了好几个小时写完LAB1后,又哼哧哼哧得改了一众BUG后,才想起std::set底层就是用红黑树实现的,可以直接拿来用。
“哼哼–哼哼哼—哼哼哼哼----啊啊啊啊啊啊啊啊”阿宅大哭。
最终实现与上文愿景差不多,用一个block_node结构体来存放每个碎片的索引、长度、内容。又因为set排序实现基于对应节点类型的小于运算符规则,所以我把block_node结构体的小于运算符重载为按索引值升序。再简单说下我的push_substring处理流程:
- 容量判断:满了就立刻返回。
- 处理子串的冗余前缀:如果子串包含已经被写入字节流的部分,就把这部分剪掉。
- 合并子串:运用
set自带的lowerbound快速确定插入位置,前后重复比较,用个自己写的子函数判断重叠的字顺便合并之。 - 写入字节流:如果流重组器头部非空,就把头部写入字节流,并更新指示头部的游标。
- EOF判断
stream_reassembler.hh
class StreamReassembler {
private:
// Your code here -- add private members as necessary.
struct block_node {
size_t begin = 0;
size_t length = 0;
std::string data = "";
bool operator<(const block_node t) const {
return begin < t.begin; }
};
std::set<block_node> _blocks = {
};
std::vector<char> _buffer = {
};
size_t _unassembled_byte = 0;
size_t _head_index = 0;
bool _eof_flag = false;
ByteStream _output; //!< The reassembled in-order byte stream
size_t _capacity; //!< The maximum number of bytes
//! merge elm2 to elm1, return merged bytes
long merge_block(block_node &elm1, const block_node &elm2);
//......
stream_reassembler.cc
StreamReassembler::StreamReassembler(const size_t capacity) : _output(capacity), _capacity(capacity) {
_buffer.resize(capacity);
}
long StreamReassembler::merge_block(block_node &elm1, const block_node &elm2) {
block_node x, y;
if (elm1.begin > elm2.begin) {
x = elm2;
y = elm1;
} else {
x = elm1;
y = elm2;
}
if (x.begin + x.length < y.begin) {
return -1; // no intersection, couldn't merge
} else if (x.begin + x.length >= y.begin + y.length) {
elm1 = x;
return y.length;
} else {
elm1.begin = x.begin;
elm1.data = x.data + y.data.substr(x.begin + x.length - y.begin);
elm1.length = elm1.data.length();
return x.begin + x.length - y.begin;
}
}
//! \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 >= _head_index + _capacity) {
// capacity over
return;
}
// handle extra substring prefix
block_node elm;
if (index + data.length() <= _head_index) {
// couldn't equal, because there have emtpy substring
goto JUDGE_EOF;
} else if (index < _head_index) {
size_t offset = _head_index - index;
elm.data.assign(data.begin() + offset, data.end());
elm.begin = index + offset;
elm.length = elm.data.length();
} else {
elm.begin = index;
elm.length = data.length();
elm.da

1503

被折叠的 条评论
为什么被折叠?



