【计算机网络】Stanford CS144 学习笔记

本文为我的斯坦福计算机网络课的编程实验(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++-8clang-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。此外多读读讲义提示,注意下EOFshutdown()的参数即可。

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_singletest.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
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值