这个模块是最终给组件使用者提供的,用于以简单的接口实现 HTTP 服务器的搭建的模块。
该模块内部包括如下元素:
-
一个
TcpServer对象:- 负责连接的
IO处理。
- 负责连接的
-
静态资源根目录:
- 其实就是一个字符串,表示当前服务器中想被外部访问的目标资源的根目录!
-
几张哈希表存储请求方法与其处理函数的映射表:
-
组件使用者向
HttpServer模块设置哪些请求应该使用哪些函数进行处理,比如说GET、POST、DELETE等方法要如何处理。这样子等TcpServer模块收到对应的请求之后就会使用对应的函数进行处理。 -
不同的方法,所对应的处理函数是不同的。对于
GET、HEAD来说,它们的处理函数就是要获取静态资源,则需要用到上面的静态资源根目录成员;而对于POST、DELETE、PUT等方法来说,它们的处理函数更多的是功能性函数,也就是需要进行具体的业务处理! -
不过实际上我们用的是数组来存储映射表,因为我们在操作的时候其实需要遍历去比对正则表达式,不用到快速索引,所以用数组来代替!

-
先来梳理一下 HTTP 服务器的一个处理流程,方便我们了解后面需要实现什么接口:
- 首先要从
socket中接收数据,放到接收缓冲区中。 - 调用
message_handle()函数也就是消息的回调函数,进行业务处理。 - 对请求进行解析,得到一个
HttpRequest对象,其内部就包含了该请求的所有要素。 - 然后根据将该
HttpRequest对象进行请求的路由查找,即从上面提到的映射表中找到对应的处理函数。此时根据请求方法,分为两种情况:- 静态资源请求:
- 比如说
GET和HEAD方法,就是单纯为了请求文件的资源,那么直接将静态资源文件的数据读取出来即可,填充到HttpResponse模块中!
- 比如说
- 功能性请求:
- 比如说
PUT和POST方法,对于这类方法我们要在请求映射表中查找对应的处理函数,进行业务处理,然后再进行填充HttpResponse模块!
- 比如说
- 静态资源请求:
- 将上一步得到的
HttpResponse模块组织成HTTP格式响应,进行发送即可!
所以根据上面的流程可以知道,我们需要实现以下给使用者使用的核心接口:
- 添加请求-处理函数的映射信息接口:
- 这里要特别注意的是,因为资源路径是有多种的,但是对应的处理函数可能只有一种,因为存在多对一的关系,所以对于传入的资源路径则要求必须是一个正则表达式,这样子我们在解析资源路径的时候,可以把这个对应的处理函数与的多个资源路径进行统一,就不用说搞很多重复功能但不同名的处理函数了。
- 举个例子,如果资源路径是
/hello的话,此时我们需要调用一个Hello()执行函数,而有种情况就是资源路径为/hello/123,此时我们还是需要调用Hello(),但是可能该路径下还有别的文件比如资源路径/hello/456,此时我们不可能说再搞个Hello456()专门给它执行,所以我们要求传入的资源路径是一个正则表达式,则上面两个资源路径就变成/hello/\d+,则统一调用的都是Hello()函数,并且只需要在映射表种记录该正则表达式与该执行函数的关系即可,而不会出现多对一关系的情况!
- 设置静态资源根目录接口
- 设置是否启动非活跃连接超时关闭接口
- 设置线程池中线程数量接口
- 启动服务器接口
然后还需要一些内部私有成员函数,来完成 http 服务器的处理流程:
- 两个提供给
TcpServer对象的接口:- 连接建立完成后的回调处理:
- 该回调处理很简单,就是给
TcpServer设置协议的上下文即可。(即设置通用类型Any的成员变量,用于记录请求数据的处理过程,才能在线程被切走然后又重新切回来之后知道之前处理到了哪里)
- 该回调处理很简单,就是给
- 收到消息后的回调处理:
- 收到消息之后就要从缓冲区中获取上下文,然后进行解析处理,判断请求方法是静态资源请求还是功能性请求类型的,根据两种类型的不同进行不同的处理(这个过程我们叫做请求的路由查找,下面也会专门搞个函数来负责处理),最后就是组织
http响应格式进行返回!
- 收到消息之后就要从缓冲区中获取上下文,然后进行解析处理,判断请求方法是静态资源请求还是功能性请求类型的,根据两种类型的不同进行不同的处理(这个过程我们叫做请求的路由查找,下面也会专门搞个函数来负责处理),最后就是组织
- 连接建立完成后的回调处理:
- 辅助消息回调处理的函数:
-
路由查找函数
- 该函数其实就是为下面两个函数进行选择,根据请求方法的不同选择下面不同的处理函数!
-
静态资源请求处理函数
-
功能性请求处理函数
-
组织协议格式进行返回的函数
-
using handle_t = std::function<void(const HttpRequest&, HttpResponse*)>;
class HttpServer
{
private:
TcpServer _server; // 高性能服务器对象
std::string _static_directory; // 静态资源根目录
std::vector<std::pair<std::regex, handle_t>> get_route; // get方法的执行函数路由表
std::vector<std::pair<std::regex, handle_t>> post_route; // post方法的执行函数路由表
std::vector<std::pair<std::regex, handle_t>> put_route; // put方法的执行函数路由表
std::vector<std::pair<std::regex, handle_t>> delete_route; // delete方法的执行函数路由表
public:
/* 对外提供的核心接口 */
HttpServer();
// 添加请求-处理函数的映射信息接口(注意这里key不是字符串,而是一个正则表达式)
void add_get(const std::string& pattern, const handle_t& handler);
void add_post(const std::string& pattern, const handle_t& handler);
void add_put(const std::string& pattern, const handle_t& handler);
void add_delete(const std::string& pattern, const handle_t& handler);
// 设置静态资源根目录接口
void set_static_directory(const std::string& path);
// 设置是否启动非活跃连接超时关闭接口
void enable_inactive_release(int timeout);
// 设置线程池中线程数量接口
void set_nums_of_thread(int count);
// 启动服务器接口
void start_httpserver();
private:
/* 服务器内部的两个回调处理函数,以及其所需的一些辅助函数 */
// 连接建立完成后的回调处理
void connected_handle(const ConnectionPtr& cptr);
// 收到消息后的回调处理
void message_handle(const ConnectionPtr& cptr, Buffer* buffer);
// 路由查找函数
void route(HttpRequest& request, HttpResponse* response);
// 判断是否为正确的静态资源请求
bool is_static_resource_request(const HttpRequest& request);
// 静态资源请求处理函数
void static_resource_request(const HttpRequest& request, HttpResponse* response);
// 功能性请求函数的分类处理
void functional_request(HttpRequest& request, HttpResponse* response,
std::vector<std::pair<std::regex, handle_t>>& route);
// 组织协议格式进行返回的函数
void organize_and_response(const ConnectionPtr& cptr, const HttpRequest& request, HttpResponse* response);
// 响应错误信息
void error_response(const ConnectionPtr& cptr, const HttpRequest& request, HttpResponse* response);
};Ⅱ. 接口实现
1、核心接口实现
using handle_t = std::function<void(const HttpRequest&, HttpResponse*)>;
class HttpServer
{
private:
TcpServer _server; // 高性能服务器对象
std::string _static_directory; // 静态资源根目录
std::vector<std::pair<std::regex, handle_t>> get_route; // get方法的执行函数路由表
std::vector<std::pair<std::regex, handle_t>> post_route; // post方法的执行函数路由表
std::vector<std::pair<std::regex, handle_t>> put_route; // put方法的执行函数路由表
std::vector<std::pair<std::regex, handle_t>> delete_route; // delete方法的执行函数路由表
public:
HttpServer(uint16_t port, int timeout = DEFAULT_TIMEOUT)
: _server(port)
{
// 这里只设置两个回调处理
_server.set_connected_callback(std::bind(&HttpServer::connected_handle, this, std::placeholders::_1));
_server.set_message_callback(std::bind(&HttpServer::message_handle, this, std::placeholders::_1,
std::placeholders::_2));
_server.enable_inactive_release(timeout);
}
// 添加请求-处理函数的映射信息接口(注意这里key不是字符串,而是一个正则表达式)
void add_get(const std::string& pattern, const handle_t& handler)
{
get_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void add_post(const std::string& pattern, const handle_t& handler)
{
post_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void add_put(const std::string& pattern, const handle_t& handler)
{
put_route.push_back(std::make_pair(std::regex(pattern), handler));
}
void add_delete(const std::string& pattern, const handle_t& handler)
{
delete_route.push_back(std::make_pair(std::regex(pattern), handler));
}
// 设置静态资源根目录接口
void set_static_directory(const std::string& path)
{
assert(Util::is_directory(path) == true);
_static_directory = path;
}
// 设置是否启动非活跃连接超时关闭接口
void enable_inactive_release(int timeout) { _server.enable_inactive_release(timeout); }
// 设置线程池中线程数量接口
void set_nums_of_thread(int count) { _server.set_nums_of_subthread(count); }
// 启动服务器接口
void start_httpserver() { _server.start_server(); }
};2、连接建立完成后的回调处理
这个我们简单处理,就是设置一下该连接的上下文,然后进行日志打印即可!
// 连接建立完成后的回调处理
void connected_handle(const ConnectionPtr& cptr)
{
cptr->set_context(HttpContext());
DLOG("new connection: %p", cptr.get());
}3、收到消息后的回调处理
这个函数是进行业务处理的核心函数,因为当该连接对应的文件描述符有可读事件触发了,或者当连接要关闭的时候,我们先进行业务处理!
因为这个函数是在 socket 接收数据之后放到缓冲区后调用的,所以我们要先读取缓冲区中的数据,通过我们前面封装的上下文 HttpContext 进行读取,这样子能保证处理时候上下文一致!
接收完之后就将接收到的上下文放到请求中,即放到 HttpRequest 对象中去,然后再创建一个空的响应对象即 HttpResponse 一起进行路由查找,路由查找函数就是帮我们根据传入的请求结构,与用户设置的执行函数映射表进行匹配,如果找到对应请求的执行函数,则进行处理,找不到的话其内部会进行错误码的设置!
路由查找结束之后就是根据 HttpResponse 对象的结构进行组织成响应报文,返回给客户端!
发送结束之后还需要进行上下文的重置,因为我们这个回调函数是一个循环体,如果缓冲区有数据的话就会进行持续的处理,为了防止下一条请求被影响(比如说状态码不正确的情况),我们就得重置上下文!
最后判断是否为短连接,是的话直接关闭连接即可,如果不是的话则不需要关心!
// 收到消息后的回调处理
void message_handle(const ConnectionPtr& cptr, Buffer* buffer)
{
// 如果缓冲区有数据的话就进行持续的处理
while(buffer->get_sizeof_read() > 0)
{
// 1. 获取上下文
HttpContext* context = cptr->get_context()->get<HttpContext>();
// 2. 通过上下文对缓冲区数据进行解析,得到HttpRequest对象(如果缓冲区数据解析成功,且请求已经获取完毕了,才开始去路由查找和处理)
context->recv_and_handle_request(buffer);
// 2.1 如果缓冲区数据解析失败,则直接响应错误信息然后关闭连接即可
HttpRequest& request = context->get_request();
HttpResponse response(context->get_response_status());
if(context->get_response_status() >= 400)
{
// 即填充错误信息页面数据到响应中,然后返回该错误页面响应
error_response(cptr, request, &response);
// 出错了就把当前连接的缓冲区数据清空,不然会和下面的shutdown函数形成死循环。最后顺便把状态也清空一下
request.reset();
buffer->clear_buffer();
cptr->shutdown();
return;
}
// 2.2 如果缓冲区数据解析成功,但是请求还没获取完整,则退出该函数,等待新数据的到来后再继续处理
if(context->get_recv_status() != RECV_DONED)
return;
// 3. 进行路由查找(在其内部进行对应请求的处理)
route(request, &response);
// 4. 组织HttpResponse进行返回
organize_and_response(cptr, request, &response);
// 5. 重置上下文,防止下一条请求被影响(比如状态码什么的)
context->reset();
// 6. 判断是否为短连接,是的话直接关闭连接
if(request.is_short_connection())
cptr->shutdown();
}
}4、辅助函数
① 路由查找函数
这个函数不难,就是判断请求的是什么资源,如果是静态资源请求的话,则进行静态资源请求处理;如果是功能性请求的话,则根据请求方法来将不同功能性请求派发到不同作用的函数中去!
如果既不是静态资源请求,也不是功能性请求的话,则设置状态码为 405 表示请求方法未找到即可!
// 路由查找函数
void route(HttpRequest& request, HttpResponse* response)
{
// 1. 如果是静态资源请求的话,则进行静态资源请求处理
if(is_static_resource_request(request) == true)
return static_resource_request(request, response);
// 2. 如果是功能性请求的话,则根据请求方法来将不同功能性请求派发到不同作用的函数中去
if(request._method == "GET" || request._method == "HEAD")
return functional_request(request, response, get_route);
else if(request._method == "POST")
return functional_request(request, response, post_route);
else if(request._method == "PUT")
return functional_request(request, response, put_route);
else if(request._method == "DELETE")
return functional_request(request, response, delete_route);
// 3. 如果既不是静态资源请求,也不是功能性请求的话,则设置状态码为405表示请求方法未找到
response->_status = 405;
}② 静态资源处理函数
首先得先判断当前请求的是否为静态资源,如果是的话再进行静态资源请求处理!具体判断步骤和处理过程在下面代码中有注释:
// 判断是否为正确的静态资源请求
bool is_static_resource_request(const HttpRequest& request)
{
// 1. 要求必须设置了静态资源根目录
if(_static_directory.empty())
return false;
// 2. 要求请求方法GET或者HEAD
if(request._method != "GET" && request._method != "HEAD")
return false;
// 3. 判断请求路径是否合法
if(Util::is_path_valid(request._path) == false)
return false;
// 4. 请求的资源必须存在,并且是一个普通文件
// 如果是请求资源是目录的话,那么就在其后面加上默认页面文件index.html即可
std::string path = _static_directory + request._path; // 为了避免直接修改请求的资源路径,因此定义一个临时对象
if(path.back() == '/')
path += "index.html";
if(Util::is_regular_file(path) == false)
return false;
return true;
}
// 静态资源请求处理函数
void static_resource_request(const HttpRequest& request, HttpResponse* response)
{
// 就是将要请求的静态资源读取出来后,放到response的正文中,然后设置资源的类型Content-Type即可
std::string path = _static_directory + request._path;
if(path.back() == '/')
path += "index.html";
bool ret = Util::read_file(path, &response->_body);
if(ret == false)
return;
response->set_header("Content-Type", Util::get_mime_from_suffix(path));
}③ 功能性请求函数处理
这个函数也不难,就是在对应请求方法的路由表中,查找是否存在该请求的处理方法,如果有的话则调用,没有的话则设置 404 状态码返回即可!不过要注意的就是因为 std::regex_match 中要求正则表达式是一个 const 对象,所以我们要单独搞一个变量出来进行传递,不然的话会报错!
// 功能性请求函数的分类处理
void functional_request(HttpRequest& request, HttpResponse* response, std::vector<std::pair<std::regex, handle_t>>& route)
{
// 在对应请求方法的路由表中,查找是否存在该请求的处理方法,如果有的话则调用,没有的话则设置404状态码
// 思路:使用路由表中的每个正则表达式与请求路径进行匹配,匹配成功则使用对应函数进行处理
// 所以路由表中的key最好存放的是正则表达式,如果是字符串的话则需要去编译成正则表达式,比较费时间
for(auto& handler : route)
{
const std::regex& regex = handler.first;
bool ret = std::regex_match(request._path, request._matches, regex);
if(ret == false)
continue;
return handler.second(request, response); // 传入请求信息和空的response,执行对应的处理函数
}
// 如果走到这里的话,说明上面的功能性请求没找到对应的处理方法,则设置404状态码即可
response->_status = 404;
}④ 组织和响应处理函数
在组织响应内容之前,要先完善一些响应的头部字段,比如长短连接、正文长度、请求资源类型、重定向等等,如下所示:
// 组织协议格式进行返回的函数
void organize_and_response(const ConnectionPtr& cptr, const HttpRequest& request, HttpResponse* response)
{
// 1. 完善一些头部字段(比如长短连接、正文长度、请求资源类型、重定向等等)
if(request.is_short_connection() == true)
response->set_header("Connection", "close");
else
response->set_header("Connection", "keep-alive");
if(!response->_body.empty() && response->has_header("Content-Length") == false)
response->set_header("Content-Length", std::to_string(response->_body.size()));
if(!response->_body.empty() && response->has_header("Content-Type") == false)
response->set_header("Content-Type", "application/octet-stream");
if(response->_is_redirect == true)
response->set_header("Location", response->_redirect_path);
// 2. 根据response组织响应内容(状态行、响应报头、空行、响应正文)
std::stringstream sstr;
sstr << request._version << " " << std::to_string(response->_status)
<< " " << Util::get_information_from_status(response->_status) << "\r\n";
for(auto& e : response->_header)
sstr << e.first << ": " << e.second << "\r\n";
sstr << "\r\n" << response->_body;
// 3. 发送数据
cptr->send_data(sstr.str().c_str(), sstr.str().size());
}⑤ 错误信息处理
因为有可能接收到的内容是错误的,或者服务器处理错误,此时返回一个错误页面即可!
// 响应错误信息
void error_response(const ConnectionPtr& cptr, const HttpRequest& request, HttpResponse* response)
{
// 1. 读取错误页面文件
std::string buffer;
bool ret = Util::read_file("./wwwroot/error.html", &buffer);
if(ret == false)
{
ELOG("响应错误信息操作失败!");
return;
}
// 2. 将数据设置为响应正文,然后进行组织发送
response->set_content(buffer, "text/html");
organize_and_response(cptr, request, response);
}5、简单首页登录页面
<!DOCTYPE html>
<html>
<head>
<title>登录页面</title>
<style>
body {
background: #f1f1f1;
font-family: Arial, sans-serif;
}
.container {
width: 300px;
margin: 0 auto;
padding: 20px;
background: #fff;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
color: #333;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border-radius: 3px;
border: 1px solid #ccc;
}
input[type="submit"] {
width: 100%;
padding: 10px;
background: #4CAF50;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
input[type="submit"]:hover {
background: #45a049;
}
.error {
color: red;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<h2>用户登录</h2>
<form action="/login" method="post">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<div class="error">错误消息显示在这里(如果有)</div>
<input type="submit" value="登录">
</form>
</div>
</body>
</html>6、简单的错误页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>出错了</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f1f1f1;
}
.error-container {
margin: 10% auto;
text-align: center;
}
h1 {
font-size: 3rem;
color: #333;
}
p {
font-size: 1.2rem;
color: #777;
margin-bottom: 2rem;
}
a {
display: inline-block;
padding: 0.5rem 1rem;
margin-top: 2rem;
background-color: #007bff;
color: #fff;
border-radius: 4px;
text-decoration: none;
transition: background-color 0.2s ease;
}
a:hover {
background-color: #0062cc;
}
</style>
</head>
<body>
<div class="error-container">
<h1>出错了</h1>
<p>抱歉,您访问的页面暂时无法显示。</p>
<a href="./index.html">返回首页</a>
</div>
</body>
</html>