CS144-Lab0-实验笔记

Lab 0


0x00 Networking by hand

Fetch a Web page

telnet 被定义为一个客户端程序,用于在你的计算机与另一台计算机运行的程序之间建立一个可靠的双向字节流 (Reliable bidirectional byte stream),输入命令:

1
telnet cs144.keithw.org http

该命令会连接名为 cs144.keithw.org 的计算机上运行的 http 服务,接下来手动构建一个 HTTP 请求:

1
2
3
4
5
6
7
8
# 想通过 HTTP 1.1 版本获取 (GET) 路径为 /hello 的资源
GET /hello HTTP/1.1

# 请求的具体域名
Host: cs144.keithw.org

# 发完这次回复后就立即关闭连接,不要等待后续请求
Connection: close

Send yourself an email

首先通过 ssh 登录到 sunetid@cardinal.stanford.edu,然后运行命令:

1
telnet 67.231.149.169 smtp

该命令会连接名为 cs144.keithw.org 的计算机上运行的 smtp 服务,接下来手动构建一个 SMTP 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 向邮件服务器标识你所在的计算机
HELO mycomputer.stanford.edu

# 指定发件人
MAIL FROM: sunetid@stanford.edu

# 指定收件人
RCPT TO: sunetid@stanford.edu

# 告知服务器你已准备好上传邮件的实际内容
DATA

# 邮件头,输入完头部之后留一个空行
From: sunetid@stanford.edu
To: sunetid@stanford.edu
Subject: Hello from CS144 Lab 0!

# 输入正文,用单独一行的 . 结束
xxx
.

# 退出
QUIT

Listening and connecting

在终端窗口运行如下命令,启动监听程序 (服务器端):

1
2
# 进入监听模式 (l),显示详细输出 (v),指定在 9090 端口开启服务 (-p 9090)
netcat -v -l -p 9090

新开一个终端,输入命令,localhost 是回环地址,代表自己的计算机 。用 telnet 作为客户端去连接刚才启动的服务器。

1
telnet localhost 9090

在任意一个窗口中输入文字并按回车,在 netcat (服务器) 输入的字符会出现在 telnet (客户端) 窗口,反之亦然。这证明了流式套接字 (Stream Socket) 是双向的 (Bidirectional)。你必须按下 Enter 键,缓冲区中的字节才会被发送出去。

0x01 Writing a network program using an OS stream socket

操作系统提供了一种称为 “流式套接字” (Stream Socket) 的功能。它在两个程序 (你的程序和远程服务器) 之间建立了一条可靠的双向字节流。对程序而言,它就像一个普通的文件描述符 (File Descriptor),类似于读写硬盘上的文件。从一端写入的字节,最终会以完全相同的顺序从另一端出来。

然而,互联网只提供尽力而为 (Best-effort) 的服务,传输的是一个个短小的数据报 (Datagrams)。每个数据报包含元数据 (源地址、目的地址) 和载荷数据 (最多约 1500 字节),数据报在传输过程中可能会发生:丢失 (Lost)、乱序到达 (Delivered out of order)、内容被篡改 (Contents altered)、重复 (Duplicated) 等异常情况。

为了让应用程序能用上可靠的字节流,OS 在两端必须进行复杂的协作,将不可靠的“数据报”转化为可靠的“字节流”。通过 TCP (Transmission Control Protocol) 协议,两台计算机相互配合。可以确保每个字节最终都能按顺序到达;并且告诉对方自己能接收多少数据,防止被大量数据淹没。

本小节需要编写一个名为 webget 的程序。在这个阶段,只需要使用操作系统现成的 TCP 支持来创建一个流式套接字并获取网页 (就像之前手动做的那样)。

Reading the Minnow support code

阅读文件 util/address.hh,里面定义了 Address 类,里面封装了 ipv4 地址和 dns 服务。可以通过 hostname 和 service 来初始化一个 Address 对象。

1
2
3
4
5
6
class Address
{
// ...
// (Constructer) Construct by resolving a hostname and servicename.
Address(const std::string &hostname, const std::string &service);
}

阅读文件 util/socket.hh,里面定义了 Socket 基类和 TCPSocket 类,提供套接字的服务。Socket 类本身继承于 FileDescriptor 类,可以和文件描述符一样来操作 Socket 对象。

1
2
3
4
5
6
7
8
9
10
11
class Socket : public FileDescriptor
{
public:
// Bind a socket to a specified address with [bind(2)](\ref man2::bind), usually for listen/accept
void bind(const Address &address);

// Connect a socket to a specified peer address with [connect(2)](\ref man2::connect)
void connect(const Address &address);

// ...
}

bind 函数主要用于绑定本机上的一个特定的地址,用于监听客户端请求或者接受客户端请求建立一个新的套接字。connect 函数主要用于连接特定地址的目标套接字,一般用于客户端访问服务端监听地址。

1
2
3
4
5
bind() → listen() → accept() → 通信
(绑定) (监听) (接受) (读写)

connect() → 通信 → shutdown()
(连接) (读写) (关闭)

Writing webget

在 apps/webget.cc 文件中有一个 get_URL 函数,我们需要完善该函数实现用 HTTP 格式向 Web 发送一个请求,并持续输出服务端返回的内容。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void get_URL(const string &host, const string &path)
{
debug("Function called: get_URL( \"{}\", \"{}\" )", host, path);
debug("get_URL() function not yet implemented");

Address add(host, "http");
TCPSocket sock;

sock.connect(add);
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");

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

An in-memory reliable byte stream

本节需要实现一个对象,充当数据传输的管道。字节从“输入”端写入,并以完全相同的顺序从“输出”端被读取。这个流不是无限的。写入者可以发出信号表示“输入结束”。当读取者读完所有剩余字节后,会达到 EOF (End of File),之后无法再读取数据。

为了防止内存耗尽,字节流必须进行流量控制。如果缓冲区已满 (达到容量),写入者将被暂时阻止写入更多数据,直到有空间腾出;随着读取者从流中“排空”(读取)字节,缓冲区会腾出空间,此时允许写入者继续写入。

实验只需要实现 Writer 和 Reader 类的函数,具体如下:

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
// Push data to stream, but only as much as available capacity allows.
void Writer::push(string data)
{
// If stream is closed, set error
if (is_closed_ && !data.empty())
{
set_error();
return;
}

// Trim data to available capacity
uint64_t cur_capacity = available_capacity();
if (data.size() > cur_capacity)
data = data.substr(0, cur_capacity);

// Append data to buffer
for (char c : data)
buffer_.push_back(c);
bytes_pushed_ += data.size();

debug("Writer::push({})", data);
}

// Signal that the stream has reached its ending. Nothing more will be written.
void Writer::close()
{
is_closed_ = true;
debug("Writer::close()");
}

// Has the stream been closed?
bool Writer::is_closed() const
{
debug("Writer::is_closed() -> {}", is_closed_);
return is_closed_;
}

// How many bytes can be pushed to the stream right now?
uint64_t Writer::available_capacity() const
{
uint64_t available_capacity = capacity_ - (bytes_pushed_ - bytes_popped_);
debug("Writer::available_capacity() -> {}", available_capacity);
return available_capacity;
}

// Total number of bytes cumulatively pushed to the stream
uint64_t Writer::bytes_pushed() const
{
debug("Writer::bytes_pushed() -> {}", bytes_pushed_);
return bytes_pushed_;
}
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
// Peek at the next bytes in the buffer -- ideally as many as possible.
// It's not required to return a string_view of the *whole* buffer, but
// if the peeked string_view is only one byte at a time, it will probably force
// the caller to do a lot of extra work.
string_view Reader::peek() const
{
debug("Reader::peek() called");
if (buffer_.empty())
{
return {};
}

return string_view(buffer_.data(), buffer_.size());
}

// Remove `len` bytes from the buffer.
void Reader::pop(uint64_t len)
{
if (is_finished() || len > bytes_buffered())
{
set_error();
return;
}

bytes_popped_ += len;
buffer_.erase(buffer_.begin(), buffer_.begin() + len);

debug("Reader::pop({})", len);
}

// Is the stream finished (closed and fully popped)?
bool Reader::is_finished() const
{
bool is_empty = bytes_buffered() == 0;
bool is_finished = is_closed_;

debug("Reader::is_finished() -> {}", is_empty && is_finished);

return is_empty && is_finished;
}

// Number of bytes currently buffered (pushed and not popped)
uint64_t Reader::bytes_buffered() const
{
debug("Reader::bytes_buffered() -> {}", bytes_pushed_ - bytes_popped_);
return bytes_pushed_ - bytes_popped_;
}

// Total number of bytes cumulatively popped from stream
uint64_t Reader::bytes_popped() const
{
debug("Reader::bytes_popped() -> {}", bytes_popped_);
return bytes_popped_;
}