前言
本篇文章仅供学习交流,请不要直接照搬copy,后果自负
一、实验目的
- 学习如何获取一个网页的内容
- 学习如何实现简单的客户端-服务端的同步与监听
- 学习编写简单的网络程序
- 学习实现内存中的简单可靠字节流传输
二、实验说明
- 我们在此处开发了一个简洁的程序,利用VMware Workstation Pro的功能,通过互联网获取网页。我们成功地利用这个功能建立了一个可靠的、双向的无序字节流通信机制,其中一个程序在我们的计算机上运行,而另一个程序则在互联网上的另一台计算机上(例如Web服务器,如Apache或nginx,或者netcat程序)。这种功能被称为流套接字,它提供了高效的传输服务。
- 尽管互联网本身只提供“不可靠的”数据报服务,但我们可以观察到在互联网上实现了可靠的无序字节流通信的抽象。接下来,我们将在计算机中实现一个具备此功能的程序:字节可以按顺序从输入端写入,并可以以相同的顺序从输出端读取。字节流是无限的,即写入者可以结束输入,之后不能再写入更多的字节。当读取者到达流的末尾时,它将达到"EOF"(结束)标志,表示没有更多的字节可供读取。这种传输方式具备可靠性。
三、实验内容
- 获取一个网页的内容
- 当我们在浏览器中输入网址http://cs144.keithw.org/hello时,会显示如图1-1所示的页面。现在,我们需要通过命令行在虚拟机上获取该页面的内容。
图1-1 在浏览器查看的内容
- 在命令行中输入以下命令:telnet cs144.keithw.org http。这个命令的作用是告诉telnet程序在你的计算机和名为cs144.keithw.org的服务器之间建立一个可靠的字节流连接,并运行名为“http”的特定服务,即用于万维网的超文本传输协议(HTTP)。如图1-2所示。
图1-2 在命令行中输入telnet cs144.keithw.org http
- 按照以下步骤在命令行中操作:
- 输入"GET /hello HTTP/1.1"。这个命令告诉服务器我们要获取的页面的路径部分是"/hello",并且使用GET方法进行获取,采用的协议版本是HTTP/1.1。
- 输入"Host: cs144.keithw.org"。这会告诉服务器URL的主机部分是"cs144.keithw.org"。
- 输入"Connection: close"。这会告诉服务器在传送完HTML页面后关闭连接。
- 再按一次回车键 ,这告诉服务器我们已经完成了 HTTP 请求。结果如图 1-3 所示。可以看到,我们得到了该页面的内容为”Hello,cs144”。
图1-3 命令行结果
- 监听和连接
在之前的实验中,我们发现Telnet可以被看作是一个客户端程序,它可以连接到其他计算机上运行的程序。现在我们可以尝试创建一个简单的服务器程序,用于等待客户端连接。
以下是操作步骤:
- 输入命令"netcat -v -l -p 9090"。Netcat可以在两台设备之间进行交互,即侦听模式/传输模式。选项"-v"表示显示所有接收到的监听信息,“-l"表示在命令行窗口中监听入站信息,充当服务器角色,”-p 9090"表示使用9090端口。
- 打开另一个命令行窗口,并输入命令"telnet localhost 9090"。此时该窗口充当主机角色,与服务器共享相同的9090端口,从而实现服务器与主机之间的简单通信。
- 现在我们在任意一个窗口输入信息。比如此处我们在服务器端输入hello network,如图 1-4 所示。
图1-4 服务器端的输入和主机端的显示一致
- 使用套接字编写网络程序
- 输入命令"git clone https://github.com/cs144/minnow"以获取一个名为"minnow"的启动代码库的源代码。
- 如果成功获取了页面,将看到一个名为"minnow"的目录。可以使用"ls"命令查看目录内容。接下来,使用"cd"命令进入"minnow"目录。然后,输入"cmake -S . -B build"命令来创建一个名为"build"的目录。最后,输入"cmake --build build"命令来编译代码。最后显示如图1-5表示成功。
图1-5 测试成功
- 需要注意的是,可能会碰到这样的报错,如图1-6所示。
图1-6 报错显示
此时需要在头文件中添加"#include “和"using std::uint64_t;”
- 打开 webget.cc 文件,修改代码,如图1-7所示。代码见附录。
图1-7 代码细节
- 在build目录下,输入 make 进行编译,编译结果如图 1-8 所示。
图1-8 编译结果
- 输入"./apps/webget cs144.keithw.org /hello"进行测试,测试结果如图 1-9 所示。
图1-9 测试结果
- 输入"make check_webget "测试样例,测试结果如图 1-10 所示。可以看到, 所有的测试样例都通过.
图1-10 测试结果
- 实现内存中的可靠字节流
- 将"byte_stream.hh"和"byte_stream.cc"文件打开修改如图1-11、1-12
图1-11 byte_stream.hh文件代码细节
图1-12 byte_stream.cc 文件代码细节
- 在build目录下输入"make cheak0"对文件进行编译检查,可以看到所有测试点都通过,结果如图 1-13 所示。
图1-13 check结果
四、实验体会
在这次实验中,我们学习了如何获取网页的内容,并实现了简单的客户端-服务端的同步与监听。同时,我们还学习了编写简单的网络程序和实现内存中的可靠字节流传输。以下是我对实验的一些体会和总结:
- 通过实验,我深入了解了互联网的基本原理和工作方式。了解了通过HTTP协议进行网页内容获取的过程,以及Telnet和Netcat等工具的使用方法。
- 实验中,我们使用Telnet建立了一个可靠的字节流连接,并成功地获取了指定网页的内容。这让我更加熟悉了Telnet的功能和用法。
- 我们还学习了如何创建一个简单的服务器程序,用于等待客户端的连接。通过使用Netcat工具,我们成功地建立了服务器和客户端之间的简单通信。
- 实验中的编写网络程序部分让我更深入地了解了套接字编程和网络通信的基本概念。通过使用启动代码库和进行代码编译,我成功地实现了一个简单的网络程序。
- 最后,我们还实现了内存中的可靠字节流传输,通过对"byte_stream.hh"和"byte_stream.cc"文件的修改和编译检查,成功地完成了内存中字节流的传输。
通过这次实验,我对网络编程和字节流传输有了更深入的理解,并且掌握了一些实际的工具和技能,这对我今后在网络领域的学习和工作将有很大的帮助。
五、代码附录
- 附录一:webget.cc
void get_URL( const string& host, const string& path ) { TCPSocket socktest; socktest.connect( Address( host, "http" ) ); socktest.write("GET " + path + " HTTP/1.1\r\n" // 请求行 "Host: " + host + "\r\n" // 告知服务器主机名 "Connection: close\r\n" // 通知服务器关闭连接 "\r\n"); // 空行 socktest.shutdown( SHUT_WR ); // 关闭写端 while ( !socktest.eof() ) { // 读取所有数据 std::string tmp; socktest.read( tmp ); cout << tmp; } socktest.close(); }
- 附录二:byte_stream.hh
#pragma once #include <deque> #include <stdexcept> #include <string> #include <string_view> #include <cstdint> using namespace std; using std::uint64_t; class Reader; class Writer; class ByteStream { protected: enum State { CLOSED, ERROR };//CLOSED 0 , ERROR 1 uint64_t capacity_; uint64_t bytes_pushed_ {}; // 已写入的字节数 uint64_t bytes_popped_ {}; //已弹出的字节数 {}表示初始化为0 也可以这样初始化 /*unsigned char flag = {};:使用花括号进行空初始化。 unsigned char flag = 0;:直接指定初始值为 0。 unsigned char flag{};:使用 C++11 中的花括号初始化形式。 unsigned char flag(0);:使用传统的构造函数语法来初始化。*/ unsigned char flag {}; // 0: normal, 1: closed, 2: error std::deque<std::string> buffer_data {}; std::string_view buffer_view {};//介绍 string_view 的博客:https://blog.csdn.net/hepangda/article/details/80821567?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169875684216800184134794%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169875684216800184134794&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-80821567-null-null.142^v96^pc_search_result_base8&utm_term=string_view%20&spm=1018.2226.3001.4187 //string_view 其实就是一个只读的相当于“取别名”的string public: explicit ByteStream( uint64_t capacity ); // 提供ByteStream的 reader 和 writer 接口的辅助函数 Reader& reader(); const Reader& reader() const; Writer& writer(); const Writer& writer() const; }; class Writer : public ByteStream { public: void push( std::string data ) noexcept; // 在可用容量允许的范围内向流中写入数据 void close() noexcept; // 关闭流,不允许再向流中写入数据 void set_error() noexcept; // 流中出现错误,置位错误标志 bool is_closed() const noexcept; // 判断流是否已关闭 uint64_t available_capacity() const noexcept; // 计算流中剩余可用容量 uint64_t bytes_pushed() const noexcept; // 计算流中已写入的字节数 }; class Reader : public ByteStream { public: std::string_view peek() const noexcept; // 返回流中下一个数据块的只读视图 void pop( uint64_t len ) noexcept; // 从流中弹出指定长度的数据块 bool is_finished() const noexcept; // 判断流是否已关闭且所有数据块都已弹出 bool has_error() const noexcept; // 判断流是否出现错误 uint64_t bytes_buffered() const noexcept; // 计算当前流中剩余的字节数 uint64_t bytes_popped() const noexcept; // 计算流中已弹出的字节数 }; /* * read: A (provided) helper function thats peeks and pops up to `len` bytes * from a ByteStream Reader into a string; */ void read( Reader& reader, uint64_t len, std::string& out );
- 附录三:byte_stream.cc
#include <stdexcept> #include "byte_stream.hh" using namespace std; using std::uint64_t; ByteStream::ByteStream( uint64_t capacity ) : capacity_( capacity ) {} void Writer::push( string data ) noexcept { if(writer().is_closed())return;//如果已经关闭就无法写入 auto len = min( data.size(), available_capacity() ); // 确定可写入的数据长度 if ( len == 0 ) { // 如果可写入的数据长度为0,说明已经写满了,返回 return; } else if ( len < data.size() ) { // 如果可写入的数据长度小于 data 的长度,说明只能写入部分数据 data.resize( len ); // 将 data 的长度截断为可写入的长度 } //使用deque<string> // 将 data 写入到 buffer 中 buffer_data.push_back( move( data ) ); //在写入前,buffer_data 只包含了刚刚推入的这个元素,即 buffer_data 是空的,除此之外没有其他元素。所以是判断是否为1 if ( buffer_data.size() == 1) // 写入前为空时需要更新 buffer_view buffer_view = buffer_data.front(); /*使用deque<char> // 将 data 写入到 buffer 中 for(size_t i = 0;i<data.size();++i){ buffer_data.push_back( data[i] ); //在写入前,buffer_data 只包含了刚刚推入的这个元素,即 buffer_data 是空的,除此之外没有其他元素。所以是判断data的长度 } if ( buffer_data.size() == data.size()) // 写入前为空时需要更新 buffer_view { //方法一string_view 有一个构造函数如下,一个参数是 const char* 和一个长度:constexpr basic_string_view( const CharT* s, size_type count ); buffer_view=std::basic_string_view( &buffer_data.front(), 1 ); //方法二 std::string tmp=""; tmp.push_back(buffer_data.front()); buffer_view = tmp; } */ // 更新已写入的数据长度 bytes_pushed_ += len; } void Writer::close() noexcept { flag |= ( 1 << CLOSED ); } void Writer::set_error() noexcept { flag |= ( 1 << ERROR ); } bool Writer::is_closed() const noexcept { return flag & ( 1 << CLOSED ); } uint64_t Writer::available_capacity() const noexcept { return capacity_ - reader().bytes_buffered(); //剩余可写入容量 = 总容量 - 未读 } uint64_t Writer::bytes_pushed() const noexcept { return bytes_pushed_; } string_view Reader::peek() const noexcept { return buffer_view; //deque<char> return {&buffer_data.front(),1}; } bool Reader::is_finished() const noexcept { return writer().is_closed() && ( bytes_buffered() == 0 ); } bool Reader::has_error() const noexcept { return flag & ( 1 << ERROR ); } void Reader::pop( uint64_t len ) noexcept { if ( len > bytes_buffered() ) { return; } // 更新已弹出的数据长度 bytes_popped_ += len; /*deque<char> // 将 buffer 中的数据弹出 while(len--){ buffer_data.pop_front(); }*/ //deque<string> while ( len > 0 ) { if ( len >= buffer_view.size() ) { //len: 想要删除的长度 bytes_buffered:当前可删除长度 len -= buffer_view.size(); buffer_data.pop_front(); buffer_view = buffer_data.front(); // 最开始就保证了 buffer_data 不为空 } else { buffer_view.remove_prefix( len ); len = 0; } } } uint64_t Reader::bytes_buffered() const noexcept { return writer().bytes_pushed() - bytes_popped(); } uint64_t Reader::bytes_popped() const noexcept { return bytes_popped_; }