【Linux网络编程】第十二弹---构建与优化HTTP请求处理:从HttpRequest到HttpServer的实战
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
1、HttpRequest类
1.1、基本结构
1.2、构造析构函数
1.3、反序列化函数
1.4、GetLine()
1.5、打印函数
2、HttpServer类
2.1、HandlerHttpRequest()
3、测试
3.1、测试结果(基本版本)
3.2、代码优化一
3.3、测试结果(优化一)
3.4、代码优化二
3.5、测试结果(优化二)
4、增加路径字段
4.1、HttpRequest类
4.2、解析请求行
4.3、获取url 和 path
4.4、HandlerHttpRequest()
1、HttpRequest类
1.1、基本结构
HttpRequest类基本结构包括请求行,请求报头,正文!
class HttpRequest
{
private:
// \r\n
// \r\ndata
// 获取一行信息
std::string GetLine(std::string &reqstr);
// 解析请求行
void ParseReqLine();
// 解析请求报头,以冒号加空格分隔
void ParseReqHeader();
public:
HttpRequest();
// 反序列化
void Deserialize(std::string &reqstr) ;
// 打印请求信息
void Print();
~HttpRequest();
private:
// 基本的httprequest的格式
std::string _req_line; // 请求行
std::vector<std::string> _req_headers; // 请求报头
std::string _blank_line; // 空行
std::string _body_text; // 正文
};
1.2、构造析构函数
构造函数初始化空行即可,因为空行是固定的,析构函数无需处理!
const static std::string base_sep = "\r\n";
HttpRequest() : _blank_line(base_sep)
{}
~HttpRequest()
{}
1.3、反序列化函数
反序列化即将字符串转化成结构化字段!
// 反序列化
void Deserialize(std::string &reqstr)
{
// 基本的反序列化
_req_line = GetLine(reqstr); // 读取一行,请求行
// 请求报头
std::string header;
do
{
header = GetLine(reqstr);
if (header.empty())
break;
else if (header == base_sep)
break;
_req_headers.push_back(header);
} while (true);
// 正文
if (!reqstr.empty())
{
_body_text = reqstr;
}
}
1.4、GetLine()
获取一行有效信息,没找到分隔符返回空串,找到分隔符但是没有有效信息则返回分隔符!
// \r\n
// \r\ndata
// 获取一行信息
std::string GetLine(std::string &reqstr)
{
auto pos = reqstr.find(base_sep);
if (pos == std::string::npos) // 没找到分隔符返回空
return std::string();
std::string line = reqstr.substr(0, pos); // 截取一行有效信息
reqstr.erase(0, line.size() + base_sep.size()); // 删除有效信息和分隔符
return line.empty() ? base_sep : line; // 有效信息为空则返回分隔符,不为空返回有效信息
}
1.5、打印函数
打印反序列化出来的字符串!
// 打印请求信息
void Print()
{
std::cout << "---------------------------" << std::endl;
std::cout << "###" << _req_line << std::endl;
for (auto &header : _req_headers)
{
std::cout << "@@@" << header << std::endl;
}
std::cout << "***" << _blank_line;
std::cout << ">>>" << _body_text << std::endl;
}
2、HttpServer类
2.1、HandlerHttpRequest()
std::string HandlerHttpRequest(std::string &reqstr)
{
#ifdef TEST
std::cout << "---------------------------------------------" << std::endl;
std::cout << reqstr;
// return std::string();
std::string responsestr = "HTTP/1.1 200 OK\r\n";
responsestr += "Content-Type: text/html\r\n";
responsestr += "\r\n";
responsestr += "<html><h1>hello linux,hello net!<h2></html>";
return responsestr;
#else
HttpRequest req; // 构建请求对象
req.Deserialize(reqstr); // 反序列化字符串
req.Print(); // 打印反序列的字符串
return std::string(); // 保证编译通过
#endif
}
3、测试
主函数
// ./httpserver 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << "local-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
HttpServer hserver;
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
std::bind(&HttpServer::HandlerHttpRequest, &hserver, std::placeholders::_1),
port);
tsvr->Loop();
return 0;
}
3.1、测试结果(基本版本)
注意:执行的方法都是HandlerHttpRequest(),且代码执行的是#else后的代码!
运行结果
上面是基本的序列化,我们还可以进一步序列化,将请求行的成员都反序列化,因此请求类需要增加成员变量!
3.2、代码优化一
HttpRequest类成员变量
HttpRequest类需要增加请求方法,统一资源定位符,版本三个成员变量!
class HttpRequest
{
private:
// 基本的httprequest的格式
std::string _req_line; // 请求行
std::vector<std::string> _req_headers; // 请求报头
std::string _blank_line; // 空行
std::string _body_text; // 正文
// 更具体的属性字段,需要进一步反序列化
std::string _method; // 请求方法
std::string _url; // 统一资源定位符
std::string _version; // 版本
};
反序列化函数
反序列化函数需要增加解析请求行函数!
// 反序列化
void Deserialize(std::string &reqstr)
{
// 基本的反序列化
_req_line = GetLine(reqstr); // 读取一行,请求行
// 请求报头
std::string header;
do
{
header = GetLine(reqstr);
if (header.empty())
break;
else if (header == base_sep)
break;
_req_headers.push_back(header);
} while (true);
// 正文
if (!reqstr.empty())
{
_body_text = reqstr;
}
// 再进一步反序列化
ParseReqLine(); // 解析请求行
}
解析请求行函数
请求行的成员之间使用空格分隔,因此可以使用字符串流对象,直接进行流提取!
// 解析请求行
void ParseReqLine()
{
std::stringstream ss(_req_line); // 以空格为分隔符 cin >>
ss >> _method >> _url >> _version; // 以空格为分隔符依次将请求行的内容赋值给成员变量
}
打印请求信息
// 打印请求信息
void Print()
{
std::cout << "---------------------------" << std::endl;
std::cout << "###" << _req_line << std::endl;
for (auto &header : _req_headers)
{
std::cout << "@@@" << header << std::endl;
}
std::cout << "***" << _blank_line;
std::cout << ">>>" << _body_text << std::endl;
std::cout << "Method: " << _method << std::endl;
std::cout << "Url: " << _url << std::endl;
std::cout << "Version: " << _version << std::endl;
}
3.3、测试结果(优化一)
浏览器输入 42.193.244.117:8888
注意:IP是自己服务器的公网IP,并且需要启动可执行程序!
运行结果
浏览器输入 42.193.244.117:8888/a/b/c/d/e/f/html
运行结果
上面将请求行的成员都反序列化,我们还可以进一步序列化,将请求报头进行反序列化,照样需要增加成员变量,此处我们使用哈希表存储,因为每行报文都是以冒号+空格分隔的值!
3.4、代码优化二
HttpRequest类成员变量
HttpRequest类增加一个KV形式的哈希表即可!
class HttpRequest
{
private:
// 基本的httprequest的格式
std::string _req_line; // 请求行
std::vector<std::string> _req_headers; // 请求报头
std::string _blank_line; // 空行
std::string _body_text; // 正文
// 更具体的属性字段,需要进一步反序列化
std::string _method; // 请求方法
std::string _url; // 统一资源定位符
std::string _version; // 版本
std::unordered_map<std::string, std::string> _headers_kv; // 存储每行报文的哈希表
};
反序列化函数
该反序列函数需要加前面的基础上继续解析请求报头!
// 反序列化
void Deserialize(std::string &reqstr)
{
// 基本的反序列化
_req_line = GetLine(reqstr); // 读取一行,请求行
// 请求报头
std::string header;
do
{
header = GetLine(reqstr);
if (header.empty())
break;
else if (header == base_sep)
break;
_req_headers.push_back(header);
} while (true);
// 正文
if (!reqstr.empty())
{
_body_text = reqstr;
}
// 再进一步反序列化
ParseReqLine(); // 解析请求行
ParseReqHeader(); // 解析请求报头
}
解析请求报头函数
遍历请求报头成员变量,以行分隔符查找有效信息,找到且分隔符前的有效信息不为空且分隔符后的有效信息不为空,则将KV值插入到哈希表中!
const static std::string line_sep = ": "; // 行分隔符
// 解析请求报头,以冒号加空格分隔
void ParseReqHeader()
{
for (auto &header : _req_headers)
{
auto pos = header.find(line_sep);
if (pos == std::string::npos)
continue;
std::string k = header.substr(0, pos); // 截取key值
std::string v = header.substr(pos + line_sep.size()); // 截取value值
if (k.empty() || v.empty())
continue;
_headers_kv.insert(std::make_pair(k, v)); // 将对应的kv值插入到哈希表中
}
}
打印请求信息函数
// 打印请求信息
void Print()
{
std::cout << "---------------------------" << std::endl;
std::cout << "###" << _req_line << std::endl;
for (auto &header : _req_headers)
{
std::cout << "@@@" << header << std::endl;
}
std::cout << "***" << _blank_line;
std::cout << ">>>" << _body_text << std::endl;
std::cout << "Method: " << _method << std::endl;
std::cout << "Url: " << _url << std::endl;
std::cout << "Version: " << _version << std::endl;
for (auto head_kv : _headers_kv)
{
std::cout << ")))" << head_kv.first << "->"
<< head_kv.second << std::endl;
}
}
3.5、测试结果(优化二)
注意:执行的方法都是HandlerHttpRequest(),且代码执行的是#else后的代码!
运行结果
4、增加路径字段
我们向服务器请求的时候,需要知道资源的路径,因此我们可以增加路径字段(根目录为wwwroot/),当url为 / 时,默认访问wwwroot/index.html!
4.1、HttpRequest类
HttpRequest类增加路径成员,并将路径初始化为web根目录!
const static std::string prefixpath = "wwwroot"; // web根目录
class HttpRequest
{
public:
HttpRequest() : _blank_line(base_sep), _path(prefixpath)
{}
private:
// 基本的httprequest的格式
std::string _req_line; // 请求行
std::vector<std::string> _req_headers; // 请求报头
std::string _blank_line; // 空行
std::string _body_text; // 正文
// 更具体的属性字段,需要进一步反序列化
std::string _method; // 请求方法
std::string _url; // 统一资源定位符
std::string _path; // 资源路径
std::string _version; // 版本
std::unordered_map<std::string, std::string> _headers_kv; // 存储每行报文的哈希表
};
4.2、解析请求行
解析请求行除了解析url之外还需要解析path,path 默认直接使用path + url即可,但是当url 为 / 时,path 还需要加上默认访问文件 index.html !
// 解析请求行
void ParseReqLine()
{
std::stringstream ss(_req_line); // 以空格为分隔符 cin >>
ss >> _method >> _url >> _version; // 以空格为分隔符依次将请求行的内容赋值给成员变量
_path += _url;
// 只有web根目录返回index.html
if (_path[_path.size() - 1] == '/')
{
_path += homepage;
}
}
4.3、获取url 和 path
获取url 和 path 直接返回成员变量即可!
std::string Url()
{
LOG(DEBUG, "Client Want url %s\n", _url.c_str());
return _url;
}
std::string Path()
{
LOG(DEBUG, "Client Want path %s\n", _path.c_str());
return _path;
}
4.4、HandlerHttpRequest()
std::string HandlerHttpRequest(std::string &reqstr)
{
HttpRequest req; // 构建请求对象
req.Deserialize(reqstr); // 反序列化字符串
std::string url = req.Url();
std::string path = req.Path();
return std::string(); // 保证编译通过
}
运行结果