CS144 Lab0: networking warmup

实验环境搭建

该课程首页为 https://cs144.github.io,视频可以在 youtube 或者 bilibili 上找到。

我使用 Debian Bullseye 进行本课程相关的实验,具体的依赖项安装可以参考 BYO Linux installation

Networking by hand

这部分主要是一些命令行的操作。

Fetch a Web page

  1. 在浏览器中打开 http://cs144.keithw.org/hello,会看到 Hello, CS144!

  2. 命令行中运行 telnet cs144.keithw.org http,会看到以下内容:

    1
    2
    3
    4
    $ telnet cs144.keithw.org http
    Trying 104.196.238.229...
    Connected to cs144.keithw.org.
    Escape character is '^]'.

    可以按 ctrl+],再输入 close 和回车退出。

    接着根据描述输入请求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    $ telnet cs144.keithw.org http
    Trying 104.196.238.229...
    Connected to cs144.keithw.org.
    Escape character is '^]'.
    # type a request:
    GET /hello HTTP/1.1
    Host: cs144.keithw.org
    Connection: close

    # There is a blank line above!

    # response:
    HTTP/1.1 200 OK
    Date: Wed, 12 Oct 2022 03:15:51 GMT
    Server: Apache
    Last-Modified: Thu, 13 Dec 2018 15:45:29 GMT
    ETag: "e-57ce93446cb64"
    Accept-Ranges: bytes
    Content-Length: 14
    Connection: close
    Content-Type: text/plain

    Hello, CS144!
    Connection closed by foreign host.

    留意这里的 ETag

    Etag 是 Entity tag 的缩写,可以理解为“被请求变量的实体值”,Etag 是服务端的一个资源的标识,在 HTTP 响应头中将其传送到客户端。服务器单独负责判断记号是什么及其含义,并在 HTTP 响应头中将其传送到客户端。比如,浏览器第一次请求一个资源的时候,服务端给予返回,并且返回了 ETag: "50b1c1d4f775c61:df3" 这样的字样给浏览器,当浏览器再次请求这个资源的时候,浏览器会将 If-None-Match: W/"50b1c1d4f775c61:df3" 传输给服务端,服务端拿到该 ETAG,对比资源是否发生变化,如果资源未发生改变,则返回 304 状态码,不返回具体的资源。

  3. 根据要求发送 SUNet ID 来获取 secret code。因为并没有真实的 ID,所以这里用随机的数字替代。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    $ telnet cs144.keithw.org http
    Trying 104.196.238.229...
    Connected to cs144.keithw.org.
    Escape character is '^]'.
    # type a request:
    GET /lab0/1234 HTTP/1.1
    Host: cs144.keithw.org
    Connetction: close

    # There is a blank line above!

    # response:
    HTTP/1.1 200 OK
    Date: Wed, 12 Oct 2022 03:20:59 GMT
    Server: Apache
    X-You-Said-Your-SunetID-Was: 1234
    X-Your-Code-Is: 147679
    Content-length: 108
    Vary: Accept-Encoding
    Content-Type: text/plain

    Hello! You told us that your SUNet ID was "1234". Please see the HTTP headers (above) for your secret code.
    Connection closed by foreign host.

    关于这里的 X-You...X-Your... 都是非标准用法,具体可以看 stackoverflow 上的回答。

Send yourself an email

这部分需要真实的 SUNet ID,所以跳过。

Listening and connecting

1
2
3
4
5
6
# terminal 1
$ netcat -v -l -p 9090
Listening on 0.0.0.0 9090
Connection received on localhost 52578
hello, cs144!
^C
1
2
3
4
5
6
7
8
# terminal 2
$ telnet localhost 9090
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello, cs144!
Connection closed by foreign host.

Writing a network program using an OS stream socket

先把 sponge 这个项目克隆到本地。

Writing webget

这部分就是把之前在终端中执行过的那些命令用程序给实现出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// sponge/apps/webget.cc

// ...

void get_URL(const string &host, const string &path) {
// You will need to connect to the "http" service on
// the computer whose name is in the "host" string,
// then request the URL path given in the "path" string.

Address addr = Address(host, "http");
TCPSocket sock;
sock.connect(addr);

sock.write("GET " + path + " HTTP/1.1\r\n");
sock.write("Host: " + host + "\r\n");
sock.write("Connection: close\r\n");
sock.write("\r\n");

// If you don’t shut down your outgoing byte stream,
// the server will wait around for a while for you to send additional requests
// and won’t end its outgoing byte stream either.
sock.shutdown(SHUT_WR);

// Then you'll need to print out everything the server sends back,
// (not just one call to read() -- everything) until you reach
// the "eof" (end of file).

while (!sock.eof()) {
cout << sock.read();
}

sock.close();

// cerr << "Function called: get_URL(" << host << ", " << path << ").\n";
// cerr << "Warning: get_URL() has not been implemented yet.\n";
}

// ...

一些注意事项:

  1. HTTP 请求中换行必须使用 \r\n 而不能只使用 \n
  2. 同时这里需要包含 Connection: close,表示远程服务器处理完当前请求后立即关闭。
  3. 注意代码片段中的 shutdown(SHUT_WR) 的用法,虽然没有这行代码也能通过测试。想要了解 shutdownclose 的区别可以查看《Linux 高性能服务器编程》5.7 节的内容:简单来讲 close 只能同时关闭 socket 的读写,但是 shutdown 可以分别关闭。
  4. 要求把所有的输出都打印出来,所以需要利用循环 while(!sock.eof()) 来多次调用 read()

An in-memory reliable byte stream

我们需要在内存中实现一个可读、可写,支持流量控制的有序字节流。

头文件中声明一些需要用到的私有变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// sponge/libsponge/byte_stream.hh

// ...
class ByteStream {
private:
// ...

std::deque<char> _buffer;
size_t _capacity;
size_t _written_size;
size_t _read_size;
bool _is_end_input;

// ...
}

具体的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// sponge/libsponge/byte_stream.cc

// ...

ByteStream::ByteStream(const size_t capacity)
: _buffer(), _capacity(capacity), _written_size(0), _read_size(0), _is_end_input(false) {}

size_t ByteStream::write(const string &data) {
if (input_ended()) {
return 0;
}

size_t write_size = std::min(data.length(), remaining_capacity());
_written_size += write_size;

for (size_t i = 0; i < write_size; ++i) {
_buffer.push_back(data[i]);
}

return write_size;
}

//! \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 peek_size = std::min(len, buffer_size());
return std::string(_buffer.begin(), _buffer.begin() + peek_size);
}

//! \param[in] len bytes will be removed from the output side of the buffer
void ByteStream::pop_output(const size_t len) {
size_t pop_size = std::min(len, buffer_size());
_read_size += pop_size;
while (pop_size--) {
_buffer.pop_front();
}
}

//! Read (i.e., copy and then pop) the next "len" bytes of the stream
//! \param[in] len bytes will be popped and returned
//! \returns a string
std::string ByteStream::read(const size_t len) {
std::string data = peek_output(len);
pop_output(len);
return data;
}

void ByteStream::end_input() { _is_end_input = true; }

bool ByteStream::input_ended() const { return _is_end_input; }

size_t ByteStream::buffer_size() const { return _buffer.size(); }

bool ByteStream::buffer_empty() const { return _buffer.empty(); }

bool ByteStream::eof() const { return input_ended() && buffer_empty(); }

size_t ByteStream::bytes_written() const { return _written_size; }

size_t ByteStream::bytes_read() const { return _read_size; }

size_t ByteStream::remaining_capacity() const { return _capacity - buffer_size(); }

注意事项:

  1. write() 中传入的 data 的长度是可以大于缓冲区的容量,但是超过剩余空间大小的部分会被丢弃。
  2. 留意 eof() 成立的条件:需要 writer 停止写入,同时 reader 读取完缓冲区中的全部内容。

调试代码

在 VSCode 中使用 clangdCodeLLDB 插件,在 .vscode 文件夹下添加 launch.json 文件并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug sponge",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/build/tests/${fileBasenameNoExtension}",
"args": [],
"cwd": "${workspaceFolder}",
"internalConsoleOptions": "neverOpen",
"console": "integratedTerminal",
}
]
}

测试文件位于 sponge/tests/ 文件夹下,对应的 CMake 命令位于 sponge/etc/test.cmake 中。

参考

CS144 系列实验的一些参考:

Labs Code

CS144 的代码都保存在我的 GitHub 仓库中。