Stanford CS144 For Computer Network(二):Lab0 Networking Warmup
Lab0 Networking Warmup
Lab0实验手册: https://vixbob.github.io/cs144-web-page/assignments/lab0.pdf(2021 Fall)
部分实验目的和内容摘抄自手册。
Networking by hand
这一小节只是简单的网络实验,不涉及代码,围绕telnet
、netcat
的一些操作展开,无兴趣的可以跳过。
Fetch a Web page
打开浏览器,访问http://cs144.keithw.org/hello
,你会看到Hello, CS144!
;
现在虚拟机可以做同样的事情,Linux下运行: 1
telnet cs144.keithw.org http
继续输入 1
GET /hello HTTP/1.1 #GET为请求操作,HTTP/1.1是客户端http协议版本
1
Host: cs144.keithw.org #指定目标服务器
此时按下回车,告诉服务器你以及发送完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
25telnet 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
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协议来实现一个可靠的流式套接字传输,所以参考的类是TCPSocket
和Address
;我们需要补全apps/webget.cc
中的get_URL
函数,实现获取cs144.keithw.org
的网页字符内容,在上节实验通过telnet来做,现在换成了TCP而言,作为客户端,TCP的步骤就是socket——connect——send——recv
即可。TCP这里没有封装发送和接收成员函数,相信这个是后面的Lab目标,因此这里只能使用继承而来的文件描述符的write
和read
写法;
具体实现
回忆网络编程内容,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
20void 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
30class 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的容器,例如vector
、deque
、list
、queue
,首先vector
的头部操作需要移动其他元素,复杂度为O(n),非首选;list
的底层是一个双向循环链表,结点需要额外的指针域记录前驱和后继指针,queue
是标准的FIFO结构,但是它和list一样都不支持随机存储访问,因此这里我选择的是deque
;deque
的原理是中控器+缓冲区,因此在头部插入和尾部插入都是一样方便,虽然访问中控器带来了一定的时间开销,但是可以接受的。定义成员函数如下:
1
2
3
4
5
6
7
8
//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.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(_writecnt!=size){ //不要乱返回
_buffer.clear();
_writecnt = 0;
return 0;
}
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
完整Commit参考:Lab0 ByteStream Done
Q&A
- 缓冲区空标志不对、写入数量、溢出写入等情况不对:
- 检查写函数逻辑再检查其他逻辑,我在write函数定义了错误检查,检查错误返回-1是不对的,因为
size_t
代表大于等于0的数,return -1会导致类型错误,导致write返回一个很大的值;return 0也是无效(仅针对测试),只能取消掉了;
- test 7
t_address_dt
测试失败:
- 非强迫证患者建议忽略这个报错。
t_address_dt
使用了境外网站测试,因此首先要保证虚拟机(不是主机)能够正常访问google等,如果还不行尝试修改VMnet8地址、本地IP地址等;我使用的是NAT模式,一般将虚拟机网络固定成VMnet8+监听端口号即可共享主机虚拟网络(注意这个端口号不是节点的网络端口,是某软件的监听端口号,V2某在左下角看,Cla某在设置页);我的VMnet8是固定的,配置静态IP的方法见之前的文章(有点东西就像回旋镖,随便写的也不知道什么时候会有用;