2. 手动获取一个网页
输入命令:telnet cs144.keithw.org http
。
这条命令的含义是,让telnet程序在主机和服务器(cs144.keithw.org)之间打开一个可靠的字节流连接,连接的应用进程是http。
连接成功之后,输入下面几行语句:
- 第一行表示请求的URL路径
- 第二行表示请求的主机名
- 第三行告诉服务器在响应结束之后,关闭连接
GET /hello HTTP/1.1
Host: cs144.keithw.org
Connection: close
下面是运行结果:
xishufan@xishufan-virtual-machine:~/Downloads$ telnet cs144.keithw.org http
Trying 104.196.238.229...
Connected to cs144.keithw.org.
Escape character is '^]'.
GET /hello HTTP/1.1
Host: cs144.keithw.org
Connection: close
HTTP/1.1 200 OK
Date: Thu, 27 Apr 2023 14:13:19 GMT
Server: Apache
Last-Modified: Thu, 13 Dec 2018 15:45:29 GMT
ETag: "e-57ce93446cb64"
Accept-Ranges: bytes
Content-Length: 14
Connection: close
Content-Type: text/plain
Hello, CS144!
Connection closed by foreign host.
之前编写socket服务器,然后用telnet进行测试的时候,是指定了IP地址和连接的端口的,而这个实验中没有指定。在telnet命令中没有指定主机IP地址和端口号时,telnet客户端会尝试将第一个参数解释成主机名或IP地址。如果成功解析出主机名或IP地址,则telnet客户端会尝试连接到默认的telnet端口23。这里我们连接HTTP服务的熟知端口80。
3. 使用OS stream socket写一个获取网页程序
Linux内核可以在本地和网络中的主机之间建立一个可靠的双向字节流,这个特性也称作 stream socket。对于我们的程序和网络服务器来说,socket就像是一个普通的文件描述符(类似于硬盘上的文件,或者是标准输入输出)。两个stream socket连接之后,主机之间就可以建立通信。
但是网络并不提供可靠的传输,IP层只“尽最大努力交付”。可能会出现以下情况:
- 数据包丢失
- 数据包乱序
- 数据包内容被篡改
- 数据包重复
两台通信的计算机必须合作以确保流中的每个字节最终都在其正确的位置上被传递到另一端。他们还必须告诉对方他们准备从另一台计算机接收多少数据(窗口),并确保发送的数据不会超过对方愿意接受的范围。所有这些都是通过1981年制定的一个商定的方案来完成的,这个方案被称为传输控制协议(transmission control protocol),即TCP。
3.2 C++编写规范
最基础的规范是确保每个对象对外提供的可调用接口尽量少,并且要设计合理方式释放自己。避免使用成对的操作比如malloc/free或者new/delete,因为如果后半段代码发生异常,将无法释放资源。实例化放在构造函数中,而释放资源放在析构函数中,这是RAII思想(resource acquisition is initialization)。
具体来说,要做到如下几点:
- 不要使用malloc、free、new、delete
- 不要使用裸指针,尽量使用智能指针
- 避免使用模板、线程、锁、虚函数
- 避免使用C风格的字符串,比如 char *,或者字符串函数 strlen()、strcpy()。尽量使用string
- 不要使用C风格的类型转换,而是使用C++ 的 static_cast
- 向函数中传参时,尽量使用const定义
- 每个变量尽量使用const定义
- 每个方法尽量使用const定义
- 避免全局变量,每个变量的作用域尽量小
3.3 实现webget
这里需要我们实现get_URL
这个函数,这个函数的作用是使用TCP socket连接服务器并且请求页面数据。
- Socket类作为基类不能被实例化,我们使用TCPSocket类
- Address类的构造函数可以直接传入服务器URL和请求的服务类型,这里会自动解析服务器地址(DNS),以及自动解析HTTP服务的熟知端口
- 调用connet连接服务器,发送GET请求报文
- 循环读取缓冲直到数据全部接收完毕
void get_URL( const string& host, const string& path )
{
// 首先实例化一个通信TCP socket
TCPSocket sck;
// 然后设置要连接的服务器地址和端口
const Address addr(host, "http");
// 尝试连接
sck.connect(addr);
// 准备发送数据,需要注意的是最后还要加上换行才能让服务器知道报文结束了
string msg;
msg += "GET " + path + " HTTP/1.1\r\n";
msg += "Host: " + host + "\r\n";
msg += "Connection: close\r\n\r\n";
// 发送数据
sck.write(msg);
// 循环读取数据,直到结束
while (!sck.eof())
{
string content;
sck.read(content);
cout << content;
}
// 关闭连接
sck.close();
}
3.4 实现字节流读取
这个有一点像环形缓冲区设计,但是题目没有要求环形缓冲区的实现,只是限制了缓冲区的大小。
所以在实现中不需要模拟环形缓冲,直接用一个字符串代表缓冲区即可。
需要定义的额外变量如下:
// 公共缓存区
string buffer_ = "";
// 记录写操作是否完成
bool closed_ = false;
// 记录写操作是否错误
bool error_ = false;
// 记录写操作一共写入的字符数
uint64_t write_num_ = 0;
// 记录读操作一共读取的字符数
uint64_t read_num_ = 0;
// 记录缓存中使用的空间大小
uint64_t used_ = 0;
实现的函数如下:
#include <stdexcept>
#include "byte_stream.hh"
using namespace std;
ByteStream::ByteStream( uint64_t capacity ) : capacity_( capacity ) {}
// 将数据写入缓存中
void Writer::push( string data )
{
// Your code here.
// 判断当前还可以写入多少字节的数据
uint64_t write_size = capacity_ - used_ >= data.size() ? data.size() : capacity_ - used_;
buffer_ += data.substr( 0, write_size );
write_num_ += write_size;
used_ += write_size;
}
// 发出信号,写操作已经结束
void Writer::close()
{
closed_ = true;
// Your code here.
}
// 发出信号,写操作错误
void Writer::set_error()
{
error_ = true;
// Your code here.
}
bool Writer::is_closed() const
{
// Your code here.
return { closed_ };
}
// 当前可以写的空闲缓冲区大小
uint64_t Writer::available_capacity() const
{
// Your code here.
return { capacity_ - used_ };
}
uint64_t Writer::bytes_pushed() const
{
// Your code here.
return { write_num_ };
}
// 查看buffer中之后要读取的字符串
string_view Reader::peek() const
{
// Your code here.
return { buffer_ };
}
// 是否全部读取完毕
bool Reader::is_finished() const
{
// Your code here.
return { read_num_ == write_num_ && closed_ };
}
// 是否发生错误
bool Reader::has_error() const
{
// Your code here.
return { error_ };
}
// 读取len个字节
void Reader::pop( uint64_t len )
{
// Your code here.
buffer_ = buffer_.erase( 0, len );
read_num_ += len;
used_ -= len;
}
// 当前还未读取的字符数
uint64_t Reader::bytes_buffered() const
{
// Your code here.
return { used_ };
}
// 当前读取完毕的字符数
uint64_t Reader::bytes_popped() const
{
// Your code here.
return { read_num_ };
}