实验指导书下载:cs144.github.io/assignments…
2 Networking by hand
2.1 Fetch a Web page
非常简单的热身活动,通过手工设置http请求头并发送请求的方式,帮助我们快速熟悉Linux环境中telnet工具的基本使用。
唯一需要注意的是,敲命令的时候尽量要快点儿,不然可能在敲完命令之前tcp连接就断开了。
顺次执行如下命令即可:
telnet cs144.keithw.org http
GET /hello HTTP/1.1
Host: cs144.keithw.org
Connection: close
对比实际浏览器发送的请求头,可以看到它会复杂得多:
在telnet中http请求的响应结果如下:
对比在浏览器中直接访问目标URL的结果,可以看到除了
Connection字段外http请求的响应头和刚才用telnet手工测试的结果是基本一样的:
在HTTP/1.1协议中,若发送请求时请求头不携带Connection字段,则服务端程序取默认值
keep-alive。这就是我们在浏览器调试工具和telnet中看到响应头中Connection字段内容不同的原因。
2.2 Send yourself an email
这里我采用我自己的163邮箱账号进行演示。首先为了确保telnet能够透过163邮箱的服务器发送邮件,我们需要先在其网站上开启SMTP服务并申请一个由16个大写英文字母组成的授权密码。
接下来在shell中执行命令,连接163的smtp服务器,并与服务器打个招呼:
telnet smtp.163.com smtp
HELO wu-sunflower
HELO后面的内容我们可以随便填,一般服务器不会进行检查。
然后我们要配合服务端进行身份校验。我们将自己要准备发送邮件的邮箱地址(xxx@163.com),以及163网站提供的长度为16的授权密码,分别转换成base64编码,然后提交给smtp服务器:
AUTH LOGIN
填你自己的邮箱地址的base64编码
填你自己的授权密码的base64编码
现在我们填写发送者和收件人邮箱地址,以及邮件的正文部分(本实验中只包括From、To和Subject这三个字段)。完成后我们关闭连接:
MAIL FROM: <xxx@163.com>
RCPT TO: <xxx@163.com>
DATA
From: xxx@163.com
To: xxx@163.com
Subject: Hello from CS144 Lab 0!
.
QUIT
似乎有些邮件服务商要求
MAIL FROM和RCPT TO后面跟的邮箱地址要用尖括号包起来,有些则不用。
截图如下:
如果能够在邮箱中收到刚才发送的邮件,那么就说明实验成功了:
2.3 Listening and connecting
体验tcp连接中客户端和服务端之间的双工通信,没什么好讲的。
3 Writing a network program using an OS stream socket
编写代码,模拟手工使用telnet发送http请求的过程。只要搞清楚了相关类及其方法的用法,本题没有任何难度。唯一需要注意的是,当http请求头全部写进套接字之后,还需要再多写入一个\r\n,以告诉web服务器http请求已经发送完毕!
// apps/webget.cc
void get_URL( const string& host, const string& path ) {
Address my_address(host, "http");
TCPSocket my_socket;
my_socket.connect(my_address);
string str_command = "GET " + path + " HTTP/1.1\r\n";
string str_host = "Host: " + host + "\r\n";
string str_connection = "Connection: close\r\n\r\n";
my_socket.write(str_command);
my_socket.write(str_host);
my_socket.write(str_connection);
while (!my_socket.eof()) {
string my_buffer = "";
my_socket.read(my_buffer);
cout << my_buffer;
}
my_socket.close();
}
测试效果如下:
4 An in-memory reliable byte stream
这里我采用最简单的FIFO队列(c++ stl库已提供实现)来模拟字节流的行为。
src/byte_stream.hh中修改如下部分的代码,添加支撑字节流实现的成员变量:
class ByteStream
{
// ...
protected:
// Please add any additional state to the ByteStream here, and not to the Writer and Reader interfaces.
uint64_t capacity_;
uint64_t bytes_pushed_ {};
uint64_t bytes_buffered_ {};
uint64_t bytes_popped_ {};
uint64_t bytes_remove_prefix_ {};
std::queue<std::string> stream_ {};
bool error_ {};
bool is_closed_ {};
};
src/byte_stream.cc中的代码如下:
#include "byte_stream.hh"
using namespace std;
ByteStream::ByteStream( uint64_t capacity ) : capacity_( capacity ) {}
bool Writer::is_closed() const
{
return is_closed_;
}
void Writer::push( string data )
{
uint64_t cur_capacity = available_capacity();
if (is_closed_ || cur_capacity == 0 || data.empty()) {
return;
}
if (cur_capacity < data.size()) {
data.resize(cur_capacity);
}
bytes_buffered_ += data.size();
bytes_pushed_ += data.size();
stream_.emplace(std::move(data));
}
void Writer::close()
{
is_closed_ = true;
}
uint64_t Writer::available_capacity() const
{
return capacity_ - bytes_buffered_;
}
uint64_t Writer::bytes_pushed() const
{
return bytes_pushed_;
}
bool Reader::is_finished() const
{
return is_closed_ && bytes_buffered_ == 0;
}
uint64_t Reader::bytes_popped() const
{
return bytes_popped_;
}
string_view Reader::peek() const
{
return stream_.empty() ?
string_view {}
: string_view {stream_.front()}.substr(bytes_remove_prefix_);
}
void Reader::pop( uint64_t len )
{
while (len != 0U && bytes_buffered_ != 0U && !stream_.empty()) {
uint64_t front_size = stream_.front().size() - bytes_remove_prefix_;
if (len < front_size) {
bytes_remove_prefix_ += len;
bytes_buffered_ -= len;
bytes_popped_ += len;
break;
}
stream_.pop();
bytes_remove_prefix_ = 0;
bytes_buffered_ -= front_size;
bytes_popped_ += front_size;
len -= front_size;
}
}
uint64_t Reader::bytes_buffered() const
{
return bytes_buffered_;
}