CS144-lab0
简介
cs144 的目的是带我们实现一个属于自己的 tcp/ip 协议栈。
完整项目地址:https://github.com/cry0404/minnow (显然是后面来补的😁)
对应的实验指导在这里:https://cs144.github.io/
由于我采用的是 minnow 版(据说似乎要比 sponge 更简单一点?),所以环境配置就下载了一个 ubuntu24.04 的裸机,在本地的 vmware 中运行。只简单的配置了点命令补充(最后换成了 starship + fish) 。由于现在 ai 太好用了,根本没用到 doxygen 这种工具,直接让 ai 总结就行。
lab 0 就是带你熟悉环境配置,分别带我们体验了 telnet 以及 netcat 、 traceroute 这些基础的工具。
获取网页:
在网页浏览器中,访问 http://cs144.keithw.org/hello 。然后在 telnet 中实现同样的过程。
可以看到成功访问了。但需要注意的是,在这里telnet 中的手速一定要快,不然连接会断掉。(早在 thm 的房间里就被此深深折磨过,以及这里的 quit 是错误用法,但正确的我也忘了,文档上似乎有写,反正无关紧要)。
使用 netcat 与 telnet :
非常简单的操作,按照流程来即可以了。这里的 netcat 就相当于我们访问的 web 服务器,而 telnet 则是充当了浏览器的作用,帮助我们去请求。
并且在这里连接就不会中断,因为你是在本地监听,所以手速稍微放慢一点也没关系啦。
使用操作系统流套接字编写网络程序:
前面的 git 推送没什么好说的。如果你对 git 操作不熟悉的话,那么我推荐你阅读这本gitbook,或者多问问 ai 也是一个不错的选择。
cs144 对实验流程做了一些建议:
-
使用 C++ 参考文档 作为资源。(我们建议避免使用
cplusplus.com
,因为它可能已经过时。) -
永远不要 使用
malloc()
或free()
。 -
永远不要 使用
new
或delete
。 -
基本上不要使用原始指针 (
*
),只在必要时使用“智能”指针(unique_ptr
或shared_ptr
)。(你在 CS144 课程中不会需要使用这些。) -
避免使用模板、线程、锁和虚函数。(你在 CS144 课程中不会需要使用这些。)
-
避免使用 C 风格字符串(
char *str
)或字符串函数(strlen()
、strcpy()
)。这些容易出错。请使用std::string
代替。 -
永远不要 使用 C 风格的类型转换(例如
(FILE *)x
)。如果必须转换,请使用 C++static_cast
。(通常你在 CS144 课程中不会需要这个。) -
优先 通过
const
引用传递函数参数(例如:const Address & address
)。 -
尽量 将所有变量声明为
const
,除非它们需要被修改。 -
尽量 将所有方法声明为
const
,除非它们需要修改对象。 -
避免全局变量,并尽可能缩小变量的作用域。
-
在提交作业之前,运行以下命令:
-
cmake --build build --target tidy
以获取改进 C++ 代码的建议。 -
cmake --build build --target format
以保持代码格式一致。
-
实现 webget:
在做 cs144 之前我的套接字编程基础约等于 0 ,所以在做之前恶补了一下,我主要参考了以下的基础教程。
其实网络编程看完一些基础就差不多可以了,主要是 cpp 中的一些特性和语法看起来还有些麻烦。
在实现之前,我们需要阅读 util 中封装好的 Socket 类。
Socket (基类)
├── DatagramSocket
│ ├── UDPSocket
│ ├── PacketSocket
│ └── LocalDatagramSocket
└── TCPSocket
└── LocalStreamSocket
封装的函数:
- bind(): 绑定地址
- connect(): 连接到对端
- shutdown(): 关闭连接
- local_address(): 获取本地地址
- peer_address(): 获取对端地址
同时查看对应封装的接口:
class Socket : public FileDescriptor {
void bind(const Address& address);
void connect(const Address& address);
void listen(int backlog);
// ...
};
主要是继承了 Socket 类的 TcpSocket 和 UdpSocket 需要仔细查看。
接下来开始编写。只要掌握了基本的读写应该就没问题。(就算有问题多问问 ai 就没问题了:)
void get_URL( const string& host, const string& path )
{
cerr << "Function called: get_URL(" << host << ", " << path << ")\n";
cerr << "Warning: get_URL() has not been implemented yet.\n";
TCPSocket socket;
Address address( host, "http");
socket.connect( address );
socket.write( "GET " + path + " HTTP/1.1\r\n");
socket.write( "Host: " + host + "\r\n");
socket.write( "Connection: close\r\n");
socket.write( "\r\n");
while(true){
string buffer;
socket.read( buffer );
std::cout<<buffer;
if(buffer.empty()){
break;
}
}
socket.shutdown(SHUT_RDWR);
socket.close();
}
可以看到成功运行了。需要注意的是,不要忘了 GET 和 Host 后面都有空格(因为这个第一次还错了...)
实现内存可靠的字节流:
这里本质上就是实现一个队列而已,但是这个队列应该尽量做到效率高。
要明确:字节流是有限的,但在写入者结束输入并完成流之前,它可以几乎是任意长的。您的实现必须能够处理比容量大得多的流。容量限制了在特定时刻内存中保留的字节数(已写入但尚未读取的字节数),但并不限制流的长度。即使一个只有一字节容量的对象,也能携带长达数以千计的流,只要写入者每次只写入一个字节,并且读者在写入者被允许写入下一个字节之前读取每个字节。
在类中添加一些方便统计的变量:
protected:
// Please add any additional state to the ByteStream here, and not to the Writer and Reader interfaces.
uint64_t capacity_;
bool error_ {};
std:: string buffer_; //存储
bool closed_ { false };//检查状态,相当于多封装一层
uint64_t pushed_count_ {0};//统计已写的
uint64_t popped_count_ {0};//统计弹出的
bool is_stream_closed() const { return closed_; }//封装 close 的检查,便于 writer 和 reader 调用
};
具体的实现:
#include "byte_stream.hh"
using namespace std;
ByteStream::ByteStream( uint64_t capacity )
: capacity_( capacity )
, error_( false )
, buffer_()
, closed_( false )
, pushed_count_( 0 )
, popped_count_( 0 )
{}
void Writer::push( string data )
{
if ( is_closed() ) {
return;
}
size_t available = available_capacity();
if ( data.size() > available ) {
data = data.substr( 0, available );
}
buffer_ += data;
pushed_count_ += data.size();
//(void)data; // Your code here.
}
void Writer::close()
{
// Your code here.
closed_ = true; // 关闭直接将刚才设计的成员变量关掉即可
return;
}
bool Writer::is_closed() const
{
return closed_; // Your code here.
}
uint64_t Writer::available_capacity() const
{
return capacity_ - buffer_.size(); // Your code here.
}
uint64_t Writer::bytes_pushed() const
{
return pushed_count_; // Your code here.
}
string_view Reader::peek() const
{
string_view view( buffer_ );
return view; // Your code here.
}
void Reader::pop( uint64_t len )
{
len = min( len, buffer_.size() ); // 确保不超过当前缓冲区的大小
buffer_ = buffer_.substr( len );
popped_count_ += len;
//(void)len; // Your code here.
return;
}
bool Reader::is_finished() const
{
return is_stream_closed() && buffer_.empty(); // Your code here.
}
uint64_t Reader::bytes_buffered() const
{
return buffer_.size(); // Your code here.
}
uint64_t Reader::bytes_popped() const
{
return popped_count_; // Your code here.
}
由于本身封装已经很好了,基本上只是作一些简单的调用,主要是理解类中的成员变量(其实对于没怎么正经写过 oop 的人来说还挺新奇)。
尝试运行,可以看到运行的很成功,t_webget 很慢可能是因为我没有设置网络(?。
(终端设置代理后就降到了 1.6 s 左右,基本正常了,唉,网络...)
总结
写 cs144 对于我来说属于一个新奇的挑战,因为其实从来没有认认真真写过 oop,上一个还是图书管理系统......并且在套接字基础约等于 0 的情况下一开始看起来是很劝退的。支持我做下去的其实是 ai。最近一直有在思考,ai 时代下,该怎么去写代码。目前得出来的答案是:让自己懂大致的框架即可。当中低端的代码 ai 都可以轻松实现时,大体的架构更为重要,编程更接近搭积木了,我们更应该在乎的是如何去搭,而不是积木本身。
或者更简单一点的理由——做的过程中总能慢慢学到一些东西,更何况还有一个无比耐心的老师陪着你,所以只需要告诉自己:just do it。