Lab0 Networking Warmup

Lab0实验手册: https://vixbob.github.io/cs144-web-page/assignments/lab0.pdf(2021 Fall)

部分实验目的和内容摘抄自手册。

Networking by hand

这一小节只是简单的网络实验,不涉及代码,围绕telnetnetcat的一些操作展开,无兴趣的可以跳过。

Fetch a Web page

打开浏览器,访问http://cs144.keithw.org/hello,你会看到Hello, CS144!

现在虚拟机可以做同样的事情,Linux下运行:

1
telnet cs144.keithw.org http 
这个命令建立你和另一台计算机(cs144.keithw.org)的可靠字节流,并且运行万维网使用的http(Hyper-Text Transfer Protocol,超文本传输协议)服务。telnet和ssh类似,都是用于远程登录或管理设备的网络协议,但telnet的数据以明文发送,仅支持基本用户名-命名验证,没有文件传输能力,基本被ssh取代;

继续输入

1
GET /hello HTTP/1.1  #GET为请求操作,HTTP/1.1是客户端http协议版本
这部分告诉服务器访问URL的路径,即服务器的hello文件;继续输入
1
Host: cs144.keithw.org #指定目标服务器
这部分告诉服务器URL的主机部分。

此时按下回车,告诉服务器你以及发送完http请求;

此时会出现Hello, CS144!

最后输入close关闭字节流连接。

Send yourself an email

此处原实验手册是面向斯坦福大学的学生邮箱描述的,可惜本人和众多人才网友一样没有被斯坦福大学录取,无法获得可用的邮箱以达成深切的实践体验,只能卑微地采取QQ邮箱验证的方法,对telnet兴趣不大的完全可以跳过: 前提:

  • 可用的QQ邮箱、另一个邮箱(用于接收)

  • QQ邮箱授权码(百度,开启SMTP服务即可)

  • base64编码网站(百度)

步骤性的东西,跟随以下命令敲即可

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
telnet smtp.qq.com smtp  #开启smtp服务
helo qq.com
auth login #登录
# 此时会出现VXNlcm5hbWU6(base64),即让你输入用户名
MjQzNjQ0NDgxNUBxcS5jb20= #QQ邮箱,xxx@qq.com转换的base64编码
#此时出现UGFzc3dvcmQ6,即输入授权码
bWJak2oincV6anZyZWNmYQ== #QQ邮箱授权码,转换成base64编码
#出现验证成功,继续
# 输入:
MAIL FROM: <2436444815@qq.com>
RCPT TO: <EdenMoxe@gmail.com> #接收邮箱
# 返回OK,输入
data
#返回354 End data with <CR><LF>.<CR><LF>.就可以编辑你的邮件内容了,如下
From: EdenMo <2436444815@qq.com>
To: EdenMoxe <EdenMoxe@gmail.com>
Subject: Test SMTP

Dear CS144,
How are you? Taiwan is ours,I love my country,thank you!
Yours,
Li Hua.

. #输入完成,输入句点.;
#检查邮箱即会收到邮件,最后输入QUIT退出客户端
效果: 邮箱

Listening and connecting

telnet是客户端-服务器模型,刚刚的场景我们充当客户端获取网站文字、给另一个邮箱发邮件,现在我们需要充当一个简单的服务器,监听客户端的连接;

终端输入:

1
netcat -v -l -p 9090 #v日志信息,l监听模式,p指定端口
新开终端作为客户端连接:
1
telnet localhost 9090
然后这两个终端就可以进行字符传输了,就和我们在socket实现的那样。

Writing a network program using an OS stream socket实验

然后就进入代码环节了,CS144提供了TCP的相关函数,按类、文件组织起来(尤其重要的类是FileDescriptor, Socket, TCPSocket, and Address classes,函数接口定义在file descriptor.hh, socket.hh, and address.hh等),编程语言为C++11,具体TCP库函数可以参考CS144函数文档

实验向我们提出了使用C++11的相关规范,希望我们:

  • Never use malloc() or free()

  • Never use new or delete.

  • 原则上不使用raw指针,仅必要时使用智能指针(unique ptr or shared ptr);

  • 避免使用函数模板、线程函数、锁、虚函数;

  • 避免C风格的字符串(char*类型)和相关函数(strlen、strcpy等),使用C++的string代替;

  • 避免使用C风格的强制类型转换,使用C++的静态类型转换(static cast);

  • 传递参数使用const引用更佳;

  • 不变的变量应该定义成const,不会改变变量值的函数也应该定义为const

  • 避免使用全局变量,每个变量的赋值最好在最小范围内;

  • 提交前使用make format命令规范化代码风格。

可见CS144对C++ 11是十分青睐的,也具有一定的语言门槛,也是很多人放弃这个项目的原因。大部分语法在我的C++的三篇基础文章应该提到,除了一些极具特色功能在我过往没有提到,一般被用于专门的C++项目,我本身的C++项目实际上并不算多,只能在后面用到逐渐完善了,例如智能指针,后面我也会整理出一篇文章用于记录。

实验目的

前面铺垫的很多,实际上都是说让我们使用TCP协议来实现一个可靠的流式套接字传输,所以参考的类是TCPSocketAddress;我们需要补全apps/webget.cc中的get_URL函数,实现获取cs144.keithw.org的网页字符内容,在上节实验通过telnet来做,现在换成了TCP而言,作为客户端,TCP的步骤就是socket——connect——send——recv即可。TCP这里没有封装发送和接收成员函数,相信这个是后面的Lab目标,因此这里只能使用继承而来的文件描述符的writeread写法;

具体实现

回忆网络编程内容,socket就是调用socket函数,传入流式套接字、IPV4、0(自动匹配协议)建立套接字,这些在这里被封装到TCP类中,直接使用即可;connect是将IPv4的配置结构体(协议、IP、端口)强转到通用结构体绑定在套接字,这里Address类完成了这个工作,只需要传入主机名称和服务即可,因此apps/webget.cc代码如下,函数调用位置基本已经标注,http请求必须以"\r\n"结束,调用Tcp_socket.shutdown(SHUT_WR)意味着写端关闭,while循环才能得到eof(读完并且写端关闭)正常退出而非继续阻塞等待;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void get_URL(const string &host, const string &path) {	
TCPSocket Tcp_socket;//默认无参构造
//address.hh,Address(const std::string &hostname, const std::string &service);
Address serveraddr(host,"http");
////void connect(const Address &address);
Tcp_socket.connect(serveraddr);
//file_des---.hh,size_t write(const char *str, const bool write_all = true)
Tcp_socket.write("GET "+path+" HTTP/1.1\r\n"+"Host: "+host+"\r\n");
Tcp_socket.write("\r\n"); //空行,http请求结束
//file_des--.hh,bool eof() const { return _internal_fd->_eof; }
Tcp_socket.shutdown(SHUT_WR); //关闭写端,读完可退出
while(!Tcp_socket.eof()){
//file_des--.hh,std::string read(const size_t limit = std::numeric_limits<size_t>::max());
std::cout<<Tcp_socket.read(); //打印
}
Tcp_socket.close();
exit(0);
//cerr << "Function called: get_URL(" << host << ", " << path << ").\n";
//cerr << "Warning: get_URL() has not been implemented yet.\n";
}

验证:在build文件夹执行make编译,运行apps/webget中可执行程序,并且评测check_webget即可,总结如下:

1
cd ./build&&make&&./apps/webget cs144.keithw.org /hello&&make check_webget

如果提交该节实验,请先执行make format规范格式,否则后面在同分支下make format会导致变更重复;

完整Commit参考:Lab0 Webget Done

An in-memory reliable byte stream实验

实验目的

字节流实验,实际上希望我们实现一个读写缓冲区结构,而且在写入读出时分别计数,具体参考libsponge/byte_stream.hh定义的函数接口如下:

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
class ByteStream {
private:
// Your code here -- add private members as necessary.

// Hint: This doesn't need to be a sophisticated data structure at
// all, but if any of your tests are taking longer than a second,
// that's a sign that you probably want to keep exploring
// different approaches.

bool _error{}; //!< Flag indicating that the stream suffered an error.

public:
ByteStream(const size_t capacity); //构造函数,创建缓冲区,参数为容量
size_t write(const std::string &data); //写入data字符串
size_t remaining_capacity() const; //返回剩余容量
std::string peek_output(const size_t len) const; //从缓冲区返回前len个字符
void pop_output(const size_t len); //从缓冲区弹走前len个字符
std::string read(const size_t len); //读出前len=弹走+返回
void end_input(); //标志输入完成
bool input_ended() const; //检查输入是否完成
size_t buffer_size() const; //返回缓冲区已用大小
bool buffer_empty() const; //判断缓冲区是否空
bool eof() const; //判断是否eof,eof= 输入完成+数据读完
size_t bytes_written() const; //返回已写入字符数
size_t bytes_read() const; //返回已读走字符数

//检错操作已完成
void set_error() { _error = true; }
bool error() const { return _error; }
};

现在由我们决定成员变量,来完成这些函数接口的定义。

具体实现

首先确定缓冲区应该是一种FIFO结构,能作为FIFO的容器,例如vectordequelistqueue,首先vector的头部操作需要移动其他元素,复杂度为O(n),非首选;list的底层是一个双向循环链表,结点需要额外的指针域记录前驱和后继指针,queue是标准的FIFO结构,但是它和list一样都不支持随机存储访问,因此这里我选择的是dequedeque的原理是中控器+缓冲区,因此在头部插入和尾部插入都是一样方便,虽然访问中控器带来了一定的时间开销,但是可以接受的。定义成员函数如下:

1
2
3
4
5
6
7
8
#include <deque>
//Inside class
std::deque<char> _buffer; //缓冲区
size_t _capacity; //容量
size_t _writecnt; //写入计数
size_t _readcnt; //读走计数
bool _endinput; //写端完毕
bool _error{}; //!< Flag indicating that the stream suffered error.
然后在libsponge/byte_stream.cc完成接口,总体而言不难理解,不赘述了,一些问题见Q&A;
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
//构造
ByteStream::ByteStream(const size_t capacity):_buffer({}),_capacity(capacity),_writecnt(0),_readcnt(0),_endinput(false),_error(false){}

//写函数
size_t ByteStream::write(const string &data) {
size_t size=data.size()>(_capacity-_buffer.size())?(_capacity-_buffer.size()):data.size();
for(size_t i = 0; i < size ; i++){
_buffer.push_back(data[i]);
_writecnt++;
}
#if 0
if(_writecnt!=size){ //不要乱返回
_buffer.clear();
_writecnt = 0;
return 0;
}
#endif
return size;
}

//从缓冲区返回前len个字符
string ByteStream::peek_output(const size_t len) const {
string peek_res;
size_t size=len < _buffer.size()?len:_buffer.size();
peek_res.assign(_buffer.begin(),_buffer.begin()+size); //左闭右开
if(peek_res.size()==size)
return peek_res;
return {};
}

//从缓冲区弹走前len个字符
void ByteStream::pop_output(const size_t len) {
size_t size=len < _buffer.size()?len:_buffer.size();
for(size_t i =0 ; i < size;i++){
_buffer.pop_front();
_readcnt++;
}
}

//
std::string ByteStream::read(const size_t len) {
string read_res;
size_t size=len < _buffer.size()?len:_buffer.size();
read_res.assign(_buffer.begin(),_buffer.begin()+size); //返回
_readcnt+=size;
_buffer.erase(_buffer.begin(),_buffer.begin()+size); //弹走
if(read_res.size()==size) //返回检查
return read_res;
_buffer.clear();
_readcnt=0;
return {};
}

//后面的接口就很简单了,基本都是返回字段即可
void ByteStream::end_input() {_endinput = true;}
bool ByteStream::input_ended() const {return _endinput;}
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 _writecnt;}
size_t ByteStream::bytes_read() const { return _readcnt;}
size_t ByteStream::remaining_capacity() const { return _capacity - _buffer.size();}

验证:

1
2
# make format #if need
cd build&&make check_lab0
Lab0结果

完整Commit参考:Lab0 ByteStream Done

Q&A

  1. 缓冲区空标志不对、写入数量、溢出写入等情况不对:
  • 检查写函数逻辑再检查其他逻辑,我在write函数定义了错误检查,检查错误返回-1是不对的,因为size_t代表大于等于0的数,return -1会导致类型错误,导致write返回一个很大的值;return 0也是无效(仅针对测试),只能取消掉了;
  1. test 7 t_address_dt测试失败:
  • 非强迫证患者建议忽略这个报错。t_address_dt使用了境外网站测试,因此首先要保证虚拟机(不是主机)能够正常访问google等,如果还不行尝试修改VMnet8地址、本地IP地址等;我使用的是NAT模式,一般将虚拟机网络固定成VMnet8+监听端口号即可共享主机虚拟网络(注意这个端口号不是节点的网络端口,是某软件的监听端口号,V2某在左下角看,Cla某在设置页);我的VMnet8是固定的,配置静态IP的方法见之前的文章(有点东西就像回旋镖,随便写的也不知道什么时候会有用