CS144-Lab0

 网络  Linux  socket  C++  TCP/IP 󰈭 2923字

据说会使用C++一步步搭建自己的Linux TCP/IP协议栈, 应该是非常哦莫西路的!

选用的是CS144 2021 Fall.

基础网络工具

获取网页

使用telnet获取网页 https://cs144.github.io/的内容.

1$ telnet cs144.keithw.org http	# 打开与指定计算机的可靠字节流
2
3GET /hello HTTP/1.1		# 指定URL的path
4Host: cs144.keithw.org		# 指定URL的host
5Connection: close		# 通告request发起完成, 一旦server完成后即可关闭连接

结果如下:

 1$ telnet cs144.keithw.org http
 2Trying 104.196.238.229...
 3Connected to cs144.keithw.org.
 4Escape character is '^]'.
 5GET /hello HTTP/1.1
 6Host: cs144.keithw.org
 7Connection: close
 8
 9HTTP/1.1 200 OK
10Date: Sat, 31 Dec 2022 13:05:30 GMT
11Server: Apache
12Last-Modified: Thu, 13 Dec 2018 15:45:29 GMT
13ETag: "e-57ce93446cb64"
14Accept-Ranges: bytes
15Content-Length: 14
16Connection: close
17Content-Type: text/plain
18
19Hello, CS144!
20Connection closed by foreign host.

对应的assignment也比较简单, 修改一下path即可.

1$ telnet cs144.keithw.org http	# 打开与指定计算机的可靠字节流
2
3GET /lab0/rqdmap HTTP/1.1
4Host: cs144.keithw.org
5Connection: close

结果为:

 1$ telnet cs144.keithw.org http
 2Trying 104.196.238.229...
 3Connected to cs144.keithw.org.
 4Escape character is '^]'.
 5GET /lab0/rqdmap HTTP/1.1
 6Host: cs144.keithw.org
 7Connection: close
 8
 9HTTP/1.1 200 OK
10Date: Sat, 31 Dec 2022 13:12:00 GMT
11Server: Apache
12X-You-Said-Your-SunetID-Was: rqdmap
13X-Your-Code-Is: 245945
14Content-length: 110
15Vary: Accept-Encoding
16Connection: close
17Content-Type: text/plain
18
19Hello! You told us that your SUNet ID was "rqdmap". Please see the HTTP headers (above) for your secret code.
20Connection closed by foreign host.

邮件发送

邮件发送也是通过telnet完成, 不过需要连接上stanford的smtp后使用规定的邮箱好像, 不然会提示550 5.7.1 Relaying denied(非stanford收件人)或是550 5.1.1 User Unknown(rqdmap@stanford.edu, 非学生邮箱), 因而skip了

1telnet 148.163.153.234 smtp
2
3HELO rqdmapArchLinux.stanford.edu
4MAIL FROM: rqdmap@stanford.edu
5RCPT TO: rqdmap@gmail.com
6...

监听与连接

本地通过netcat启动对端口的监听, 再通过telnet进行连接.

TCP socket 编程

stream socket是Linux Kernel提供的功能, 可以保证字节流的顺序传递, socket看上去就是个普通的文件描述符, 其本质是使用TCP来实现的.

首先fork了一份github仓库代码rqdmap/sponge并编译源码:

1cd sponge
2mkdir build
3cd build
4cmake ..
5make

最后一步make报错:

 1[ 21:43:38 ]   make
 2[  3%] Building CXX object libsponge/CMakeFiles/sponge.dir/util/address.cc.o
 3/home/rqdmap/Codes/cs144/sponge/libsponge/util/address.cc: In member function ‘std::pair<std::__cxx11::basic_string<char>, short unsigned int> Address::ip_port() const’:
 4/home/rqdmap/Codes/cs144/sponge/libsponge/util/address.cc:91:29: error: aggregate ‘std::array<char, 1025> ip’ has incomplete type and cannot be defined
 5   91 |     array<char, NI_MAXHOST> ip;
 6      |                             ^~
 7/home/rqdmap/Codes/cs144/sponge/libsponge/util/address.cc:92:29: error: aggregate ‘std::array<char, 32> port’ has incomplete type and cannot be defined
 8   92 |     array<char, NI_MAXSERV> port;
 9      |                             ^~~~
10/home/rqdmap/Codes/cs144/sponge/libsponge/util/address.cc:100:41: error: could not convert ‘{<expression error>, <expression error>}’ from ‘<brace-enclosed initializer list>’ to ‘std::pair<std::__cxx11::basic_string<char>, short unsigned int>11  100 |     return {ip.data(), stoi(port.data())};
12      |                                         ^
13      |                                         |
14      |                                         <brace-enclosed initializer list>
15make[2]: *** [libsponge/CMakeFiles/sponge.dir/build.make:90: libsponge/CMakeFiles/sponge.dir/util/address.cc.o] Error 1
16make[1]: *** [CMakeFiles/Makefile2:1944: libsponge/CMakeFiles/sponge.dir/all] Error 2
17make: *** [Makefile:101: all] Error 2

好像是因为没有包含array头文件, 进入到util.hh中include该头文件即可正确编译.

C++编程

lab要求使用现代的C++编程方式, 可以参考C++ Core Guidelines.

其主要思想是保证每个对象都拥有最小的公共接口、拥有内部的安全检查以及会自动垃圾回收? 采用RAII(Resource Acquisition Is Initialization)机制.

lab提出了这样的几个要求:

  • 参考文档 C++ reference!

  • 不要使用malloc(), free(), new, delete.

  • 绝对不要使用原生指针(*), 而是使用智能指针(unique_ptr以及shared_prt).

  • 不需要使用模板、线程、锁和虚拟函数. (高级, 也不太会)

  • 不要使用C风格字符串, 使用std::string

  • 不要使用C风格的类型转换, 使用C++的static_cast

  • 如果要传递函数指针, 尽量传递const引用.

  • 对于不是mut的变量、方法, 尽可能添加const.

  • 避免全局变量, 使得每个变量拥有最小的作用域.

webget 应用

编写一个应用, 能够从获取cs144.keithw.org/hello的响应信息.

主要需要阅读一下address.hh, socket.hh以及file_descriptor.hh这三份头文件给出的接口. 把request发出去后就能收到相应的请求了.

 1void get_URL(const string &host, const string &path) {
 2    // Your code here.
 3
 4    // You will need to connect to the "http" service on
 5    // the computer whose name is in the "host" string,
 6    // then request the URL path given in the "path" string.
 7
 8    // Then you'll need to print out everything the server sends back,
 9    // (not just one call to read() -- everything) until you reach
10    // the "eof" (end of file).
11
12    TCPSocket ts;
13    Address addr(host, "http");
14    ts.connect(addr);
15
16    string msg = "GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n";
17    ts.write(msg);
18
19    string res;
20    while (!ts.eof()) {
21        res = ts.read();
22        cout << res;
23    }
24}

内存下的可靠字节流传输

感觉是个正式进军socket传输之前的练手. 要求实现指定的接口函数:

 1  public:
 2    //! Construct a stream with room for `capacity` bytes.
 3    ByteStream(const size_t capacity);
 4
 5    //! \name "Input" interface for the writer
 6    //!@{
 7
 8    //! Write a string of bytes into the stream. Write as many
 9    //! as will fit, and return how many were written.
10    //! \returns the number of bytes accepted into the stream
11    size_t write(const std::string &data);
12
13    //! \returns the number of additional bytes that the stream has space for
14    size_t remaining_capacity() const;
15
16    //! Signal that the byte stream has reached its ending
17    void end_input();
18
19    //! Indicate that the stream suffered an error.
20    void set_error() { _error = true; }
21    //!@}
22
23    //! \name "Output" interface for the reader
24    //!@{
25
26    //! Peek at next "len" bytes of the stream
27    //! \returns a string
28    std::string peek_output(const size_t len) const;
29
30    //! Remove bytes from the buffer
31    void pop_output(const size_t len);
32
33    //! Read (i.e., copy and then pop) the next "len" bytes of the stream
34    //! \returns a string
35    std::string read(const size_t len);
36
37    //! \returns `true` if the stream input has ended
38    bool input_ended() const;
39
40    //! \returns `true` if the stream has suffered an error
41    bool error() const { return _error; }
42
43    //! \returns the maximum amount that can currently be read from the stream
44    size_t buffer_size() const;
45
46    //! \returns `true` if the buffer is empty
47    bool buffer_empty() const;
48
49    //! \returns `true` if the output has reached the ending
50    bool eof() const;
51    //!@}
52
53    //! \name General accounting
54    //!@{
55
56    //! Total number of bytes written
57    size_t bytes_written() const;
58
59    //! Total number of bytes popped
60    size_t bytes_read() const;
61    //!@}

最开始写的时候头脑不清楚, 其实是区分发送方和接收方的, 而且双方共用一个队列, 因而要仔细处理一下.

除了构造函数, 这里发送方对应的函数为write, remaining_capacity, end_input以及set_error, 其余的都是接收方的.

实现的话做一个循环队列就行了应该.

添加private数据段:

1  private:
2    std::string buf;
3    size_t length;
4    size_t head, tail;
5    bool end;
6    size_t rx, tx;

实现的代码为:

 1ByteStream::ByteStream(const size_t capacity) : buf(""), length(2), head(0), tail(1), end(false), rx(0), tx(0) {
 2    length = capacity + 1;
 3    buf.resize(length);
 4    tail = length - 1;
 5}
 6
 7size_t ByteStream::write(const string &data) {
 8    end = false;
 9
10    size_t cnt = 0;
11    for (auto x : data) {
12        if (remaining_capacity()) {
13            buf[head] = x;
14            head = (head + 1) % length;
15            cnt++;
16            tx++;
17        }
18    }
19
20    return cnt;
21}
22
23//! \param[in] len bytes will be copied from the output side of the buffer
24string ByteStream::peek_output(const size_t len) const {
25    size_t __len = min(len, buffer_size());
26    size_t now = (tail + 1) % length;
27    string res;
28    while (__len--) {
29        res.push_back(buf[now]);
30        now = (now + 1) % length;
31    }
32    return res;
33}
34
35//! \param[in] len bytes will be removed from the output side of the buffer
36void ByteStream::pop_output(const size_t len) {
37    size_t __len = min(len, buffer_size());
38    tail = (tail + __len) % length;
39    rx += __len;
40}
41
42//! Read (i.e., copy and then pop) the next "len" bytes of the stream
43//! \param[in] len bytes will be popped and returned
44//! \returns a string
45std::string ByteStream::read(const size_t len) {
46    string __tmp = peek_output(len);
47    pop_output(len);
48    return __tmp;
49}
50
51void ByteStream::end_input() { end = true; }
52
53bool ByteStream::input_ended() const { return end; }
54
55size_t ByteStream::buffer_size() const { return (head - tail - 1 + length) % length; }
56
57bool ByteStream::buffer_empty() const { return buffer_size() == 0; }
58
59bool ByteStream::eof() const { return input_ended() && buffer_empty(); }
60
61size_t ByteStream::bytes_written() const { return tx; }
62
63size_t ByteStream::bytes_read() const { return rx; }
64
65size_t ByteStream::remaining_capacity() const { return (tail - head + length) % length; }

尽量是遵循规范了, 没有new和delete什么的… 老老实实用string和resize实现了动态的长度.

需要注意, 在这里L1的初始化中必须要使用初始化列表, 不然会报错(因为开了编译选项), 大概长这样:

1error: ‘ByteStream::buf’ should be initialized in the member initialization list [-Werror=effc++]

事实上在函数体中初始化也是可行的, 但是由于开了选项, 询问chatgpt后说是因为使用初始化列表性能更高, 因而被认为是更好的实践..

此外, 初始化列表的顺序要和头文件中声明的顺序保持一致, 不然也会报错, 大概长这样:

1error: ‘ByteStream::length’ will be initialized after [-Werror=reorder]

对于那些变长的字段, 如length, tail等也必须要在列表中赋值, 再在函数体中更新为新的值.

其余的地方没有太多坑, 不过writeread写的感觉好丑(能跑就行 bushi) 而且test中rx字段应该在pop处进行统计, 之前放在read处进行统计, 结果G了一些测试点.

顺利通过make check_lab0.

总结

经过aw的统计, 在neovim中总耗时: 1h… (?), 实际总体耗时(包括学习背景知识、配置环境等杂项)大约: 4h


修改记录:
CS144-Lab0