【仿Muduo库项目】HTTP模块4——HttpServer子模块

目录

一.分模块讲解

1.1.方法路由表

1.2.判断是否是静态文件请求

1.3.静态资源的请求处理

1.4.功能性请求的分类处理

1.5.对请求进处理

1.6.组织Http响应并执行发送

1.7.错误相关响应

1.8.服务器相关消息处理回调函数

1.8.1.连接建立消息处理回调函数

1.8.2.消息到来处理回调函数

1.8.3.注册回调函数给服务器

二.完整代码


一.分模块讲解

1.1.方法路由表

HTTP的请求方法还是有很多种的:

  • GET:获取资源。
  • POST:提交数据。
  • PUT:更新资源。
  • DELETE:删除资源。
  • HEAD:获取资源的头部信息,不返回资源内容。
  • CONNECT:建立TCP连接。
  • Options:获取服务器支持的HTTP方法。
  • Trace:追踪请求的传输路径。
  • PATCH:部分更新资源。
  • ......

但是呢,请求方法太多了,我们只会去针对里面的几种请求方法来作出响应,因此,我们只会针对下面四种请求方法进行处理

  • GET:获取资源。
  • POST:提交数据。
  • PUT:更新资源。
  • DELETE:删除资源。

那么我们怎么去对这些进行处理呢?

我们会为GET、POST、PUT、DELETE 4种HTTP方法分别维护一张表。我们姑且叫它方法路由表

每张表的核心数据是**"请求路径"与"处理函数"的对应关系。**

|------|------|
| 请求路径 | 处理函数 |
| | |

例如,记录"/api/login 路径的POST请求"对应一个名为 handleLogin 的函数。

针对不同的请求路径,我们就去查询对应请求方法的方法路由表,然后去调用对应的处理函数即可。

但是有一个问题!!针对下面这类请求路径,我们难道说需要一个请求路径去存储一个处理函数吗?

  • /number/1
  • /number/2
  • /number/3
  • ......

显然不是的,我们不能太死板,所以我们存储这个请求路径的时候,是采用正则表达式去进行存储的,这样子同一类的请求路径就能去同一个处理函数里面进行处理了

由此,我们就能写出下面这几个成员变量了

cpp 复制代码
        // 定义请求处理函数类型
        using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;
        // 定义路由表类型,包含正则表达式和处理函数的映射
        using Handlers = std::vector<std::pair<std::regex, Handler>>;
        Handlers _get_route;    // GET请求路由表
        Handlers _post_route;   // POST请求路由表
        Handlers _put_route;    // PUT请求路由表
        Handlers _delete_route; // DELETE请求路由表

我们是需要往里面针对不同的事件,我们需要注册对应的事件处理函数的

那么问题来了?这个请求路径表里面的请求路径和处理函数哪里注册的?

当然,这个不是我们来注册,我们将注册的机会留给了上层的使用者,使用者通过下面这些函数来进行注册

cpp 复制代码
        /*设置/添加,请求(请求的正则表达)与处理函数的映射关系*/
        // 注册GET请求处理函数
        void Get(const std::string &pattern, const Handler &handler) {
            _get_route.push_back(std::make_pair(std::regex(pattern), handler));
        }
        // 注册POST请求处理函数
        void Post(const std::string &pattern, const Handler &handler) {
            _post_route.push_back(std::make_pair(std::regex(pattern), handler));
        }
        // 注册PUT请求处理函数
        void Put(const std::string &pattern, const Handler &handler) {
            _put_route.push_back(std::make_pair(std::regex(pattern), handler));
        }
        // 注册DELETE请求处理函数
        void Delete(const std::string &pattern, const Handler &handler) {
            _delete_route.push_back(std::make_pair(std::regex(pattern), handler));
        }

1.2.判断是否是静态文件请求

首先,我们的HTTP请求其实分为两大类的

  • 静态资源请求
  • 功能请求

那么我们需要针对两种不同的请求进行不同的处理。

但是需要进行处理的话,我们首先需要将他们分辨出来是吧。

系统依据解析出的"请求路径"进行判定:

  • 静态资源判定:如果路径指向静态资源目录(设置根目录 + 请求路径)下的一个实际存在的文件,则判定为静态资源请求。
  • 功能路由判定:**若不满足静态资源条件,则判定为功能请求。**系统将根据HttpRequest对象中的"请求方法"和"请求路径",到对应的方法路由表中进行精确查找,以获取预先关联的处理函数。

判断是否是静态文件请求

首先,能发起静态文件请求的只有两种HTTP请求方法:GET和HEAD, 如果不是这两种方法,其余的都是有问题的

再者,我们接着看

  • 当客户端发起一个HTTP请求,例如请求路径为 /root/home/index.html 时,我们是否应该直接将服务器文件系统中对应的 /root/home/index.html 文件返回给客户端呢?
  • 答案是否定的,这种做法存在严重的安全风险。
  • 如果允许用户直接访问服务器的任意文件路径,恶意用户可能会通过构造特殊路径(如 /etc/passwd、../config.py 等)尝试访问系统敏感文件或程序源代码,导致服务器机密信息泄露。

为了解决这一问题,安全的HTTP服务器必须采用"请求根目录隔离"机制。

  • 具体来说,我们会在服务器配置中预设一个网络资源根目录(例如 ./www),所有客户端请求都将被限制在此目录范围内进行解析。
  • 当收到一个请求时,服务器会将预设的根目录与请求路径进行安全合并,形成最终的文件系统访问路径。
  • 以前述请求为例,服务器会将根目录 ./www 与请求路径 /root/home/index.html 合并为 ./www/root/home/index.html,仅在此合成路径下查找资源。

因此,我们就引入了一个新的成员变量

cpp 复制代码
class HttpServer {
    private:
        std::string _basedir;   // 静态资源根目录
......
}

但是,还是有一种恶意的请求

它它的资源请求路径是./root/../../../../../index.html,这种更是逆天操作,我们必须进行阻止

那么怎么进行阻止呢?还记得我们之前写的这个函数吗

cpp 复制代码
class Util {
......
// 验证HTTP请求资源路径的有效性:防止目录遍历攻击(如/../)
        // /index.html  --- 前边的/叫做相对根目录  映射的是某个服务器上的子目录
        // 想表达的意思就是,客户端只能请求相对根目录中的资源,其他地方的资源都不予理会
        // /../login, 这个路径中的..会让路径的查找跑到相对根目录之外,这是不合理的,不安全的
        static bool ValidPath(const std::string &path) {
            std::vector<std::string> subdir;
            Split(path, "/", &subdir); // 按/分割路径
            int level = 0; // 当前目录深度(相对于根目录)
            for (auto &dir : subdir) 
            {
                if (dir == "..") { // 上级目录
                    level--;       // 深度减1
                    if (level < 0) 
                    {
                        return false; // 试图访问根目录之外
                    }
                   continue;
                }
                level++; // 正常目录,深度加1
            }
            return true;
        }
};

通过这个函数,我们就能阻止./root/../../../../../index.html等恶意访问行为

但是,还是不太够,我们的静态文件请求就必须是请求的就必须是一个已经存在的文件,那对于目录我们怎么进行处理呢?很简单,这种情况给后边默认追加一个index.html

比如资源请求路径是/image/,这种情况给后边默认追加一个index.html

现在准备工作就差不多了,最后,我们需要去判断这个文件是不是已经在我们的服务器上存在,如果存在,就判定为静态文件请求,否则,则判定不是静态文件请求

这个时候我们就借助了下面这个函数

cpp 复制代码
class Util{
......
// 判断路径是否指向一个普通文件
        static bool IsRegular(const std::string &filename) 
        {
            struct stat st;
            int ret = stat(filename.c_str(), &st);
            if (ret < 0) {
                return false;
            }
            return S_ISREG(st.st_mode); // 判断是否为普通文件
        }
};

这个函数不仅仅能判断这个文件是否是普通文件,还能判断这个文件是否已经存在了


至此,我们整个函数就写出来了

cpp 复制代码
// 判断是否为静态文件请求
        bool IsFileHandler(const HttpRequest &req) {
            // 1. 必须设置了静态资源根目录
            if (_basedir.empty()) 
            {
                return false;
            }
            // 2. 请求方法,必须是GET / HEAD请求方法
            if (req._method != "GET" && req._method != "HEAD") 
            {
                return false;
            }
            // 3. 请求的资源路径必须是一个合法路径
            if (Util::ValidPath(req._path) == false) 
            {
                return false;
            }
            // 4. 请求的资源必须存在,且是一个普通文件
            //    有一种请求比较特殊 -- 目录:/, /image/,这种情况给后边默认追加一个index.html
            // index.html    /image/a.png
            // 不要忘了前缀的相对根目录,也就是将请求路径转换为实际存在的路径  /image/a.png  ->   ./wwwroot/image/a.png
            std::string req_path = _basedir + req._path; // 为了避免直接修改请求的资源路径,因此定义一个临时对象
            if (req._path.back() == '/')  
            {
                req_path += "index.html";
            }
            if (Util::IsRegular(req_path) == false) //判断路径是否指向一个普通文件,前提是这个文件已经存在
            {
                return false;
            }
            return true;
        }

1.3.静态资源的请求处理

这个功能其实就是很简单,就是根据HTTP里面的资源请求路径,来读取服务器里面对应文件里面的内容,然后将内容放到我们的HTTP响应的正文里面,然后在HTTP响应报头里面设置一个字段Content-Type,就是正文的类型

cpp 复制代码
// 静态资源的请求处理 --- 将静态资源文件的数据读取出来,放到rsp的_body中,并设置mime
        void FileHandler(const HttpRequest &req, HttpResponse *rsp) //req是输入型参数,rep是输出型参数
        {
            std::string req_path = _basedir + req._path;//拼接实际路径------网络通信根目录+资源访问路径
            if (req._path.back() == '/')  
            {
                req_path += "index.html";
            }
            bool ret = Util::ReadFile(req_path, &rsp->_body);//将HTTP请求req_path文件里面的所有内容读取到HTTP响应rsp->_body里面
            if (ret == false) 
            {
                return;
            }
            std::string mime = Util::ExtMime(req_path);//根据文件拓展名来获取对应的MIME类型
            rsp->SetHeader("Content-Type", mime);//直接设置进这个HTTP响应中的响应报头去
            return;
        }

这个过程很简单,没什么好说的,注意这个HTTP请求是输入性参数,HTTP响应是输出型参数

1.4.功能性请求的分类处理

我们说了,我们针对不同的方法准备了不同的路由方法表,我们在处理功能性请求的时候。

系统将根据HttpRequest对象中的"请求方法"和"请求路径",到对应的方法路由表中进行精确查找,以获取预先关联的处理函数。

但是呢,根据不同方法去不同的路由表里面查找这个功能不是我们这个函数能实现的,我们这个函数实现的是你已经知道去哪个路由表里面查找的后续步骤了。

cpp 复制代码
// 功能性请求的分类处理
        void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers) 
        {
            // 在对应请求方法的路由表中,查找是否含有对应资源请求的处理函数,有则调用,没有则返回404
            // 思想:路由表存储的是键值对 -- 正则表达式 & 处理函数
            // 使用正则表达式,对请求的资源路径进行正则匹配,匹配成功就使用对应函数进行处理
            //  /numbers/(\d+)       /numbers/12345
            for (auto &handler : handlers) //遍历方法路由表
            {
                const std::regex &re = handler.first;//方法路由表的左边:正则表达式
                const Handler &functor = handler.second;//方法路由表的右边:请求处理函数
                bool ret = std::regex_match(req._path, req._matches, re);//根据资源请求路径来根方法路由表里面的正则表达式来进行匹配
                if (ret == false) //匹配失败,说明当前方法路由表没有对应的处理函数
                {
                    continue;
                }
                //匹配成功,说明方法路由表里面有对应的请求处理函数,调用对应的请求处理函数
                return functor(req, rsp); // 传入请求信息,和空的rsp,执行处理函数
            }
            rsp->_statu = 404; // 未找到对应的处理函数,返回404
        }

但是我们需要知道,这个方法路由表里面预先关联的处理函数其实上层使用者注册的,就可能存在没有注册对应的处理函数的情况,所以,我们需要对这种情况进行处理。

1.5.对请求进处理

我们一开头就说了

首先,我们的HTTP请求其实分为两大类的

  • 静态资源请求
  • 功能请求

那么我们需要针对两种不同的请求进行不同的处理。

但是需要进行处理的话,我们首先需要将他们分辨出来是吧。

系统依据解析出的"请求路径"进行判定:

  • 静态资源判定:如果路径指向静态资源目录(设置根目录 + 请求路径)下的一个实际存在的文件,则判定为静态资源请求。
  • 功能路由判定:**若不满足静态资源条件,则判定为功能请求。**系统将根据HttpRequest对象中的"请求方法"和"请求路径",到对应的方法路由表中进行精确查找,以获取预先关联的处理函数。

那么我们现在这个函数就是来完成这一步的

cpp 复制代码
// 路由分发函数:根据请求方法分发到不同的路由表
    void Route(HttpRequest &req, HttpResponse *rsp)
    {
        // 首先我们需要对请求进行分辨,是一个静态资源请求,还是一个功能性请求
        //    静态资源请求,则进行静态资源的处理
        //    功能性请求,则需要通过几个请求路由表来确定是否有处理函数
        //    既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405
        if (IsFileHandler(req) == true)
        {
            // 是一个静态资源请求,则进行静态资源请求的处理
            return FileHandler(req, rsp);
        }
        else//是一个功能性请求,则根据请求方法分发到不同的路由表
        {
            // 根据请求方法分发到不同的路由表
            if (req._method == "GET" || req._method == "HEAD")
            {
                return Dispatcher(req, rsp, _get_route);
            }
            else if (req._method == "POST")
            {
                return Dispatcher(req, rsp, _post_route);
            }
            else if (req._method == "PUT")
            {
                return Dispatcher(req, rsp, _put_route);
            }
            else if (req._method == "DELETE")
            {
                return Dispatcher(req, rsp, _delete_route);
            }
        }
        //既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405
        rsp->_statu = 405; // Method Not Allowed,不支持的请求方法
        return;
    }

这一步都是调用了上面的3个函数来进行处理的。没什么好说的,直接看注释即可

1.6.组织Http响应并执行发送

这一步是我们必不可少的一步,作为一个服务器,一定需要去自己组织好这个Http响应

其实这个思想很简单,大家只需要照应我们的HTTP响应报头的结构去看看即可

我们就是根据这个结构来进行组织我的Http响应报文的。

只不过呢,我们这里为了通信的完整流程,我们对几个头部字段做了针对性处理

Connection

  • 含义:控制当前连接是否在本次请求/响应完成后保持打开状态。
  • 常见值:
  • keep-alive:表示客户端希望保持连接打开,以便后续请求使用(HTTP/1.1默认是持久连接,即keep-alive)。
  • close:表示客户端或服务器希望关闭连接,本次传输后即断开。
  • 示例:
  • 请求头:Connection: keep-alive
  • 响应头:Connection: close

Content-Length

  • 含义:表示实体主体(即消息体,body)的大小,单位为字节(十进制数字)。
  • 用途:在HTTP响应中,告诉客户端本次响应的body长度,以便客户端知道何时接收完毕。在请求中(如POST),表示请求体的长度。
  • 注意:如果存在Transfer-Encoding: chunked,则Content-Length会被忽略。
  • 示例:
  • 响应头:Content-Length: 348

Content-Type

  • 含义:指示资源的媒体类型(MIME类型)以及字符编码(可选)。
  • 常见值:
  • text/html; charset=utf-8
  • application/json
  • image/png
  • 示例:
  • 响应头:Content-Type: text/html; charset=UTF-8
  • 请求头(如POST提交表单):Content-Type: application/x-www-form-urlencoded

Location

  • 含义:用于重定向,或者在创建新资源时指定新资源的URL。
  • 用途:
  • 在3xx重定向响应中,指示重定向的目标URL。
  • 在201 Created响应中,指向新创建的资源。
  • 示例:
  • 响应头:Location: http://www.example.com/newpage.html

剩余的操作就按照上面那个结构来即可

cpp 复制代码
// 将HttpResponse中的要素按照http协议格式进行组织,发送
    void WriteReponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp)
    {
        // 1. 先完善HTTP响应头部字段
        // 根据请求设置Connection头部
        if (req.Close() == true)
        {
            rsp.SetHeader("Connection", "close");
        }
        else
        {
            rsp.SetHeader("Connection", "keep-alive");
        }
        // 如果响应体不为空且未设置Content-Length头部,则自动设置Content-Length字段
        if (rsp._body.empty() == false && rsp.HasHeader("Content-Length") == false)
        {
            rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));
        }
        // 如果响应体不为空且未设置Content-Type头部,则自动设置Content-Type为二进制流类型
        if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == false)
        {
            rsp.SetHeader("Content-Type", "application/octet-stream");
        }
        // 如果设置了重定向标志,则设置Location头部
        if (rsp._redirect_flag == true)
        {
            rsp.SetHeader("Location", rsp._redirect_url);
        }

        // 2. 将rsp中的要素,按照http协议格式进行组织
        std::stringstream rsp_str;
        // 写状态行:协议版本 状态码 状态描述
        rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(rsp._statu) << "\r\n";
        // 写响应头部
        for (auto &head : rsp._headers)
        {
            rsp_str << head.first << ": " << head.second << "\r\n";
        }
        // 头部结束空行
        rsp_str << "\r\n";
        // 写响应正文
        rsp_str << rsp._body;

        // 3. 发送数据
        conn->Send(rsp_str.str().c_str(), rsp_str.str().size());
    }

1.7.错误相关响应

当我们的处理过程发生了错误,那么我们就必须返回一个HTTP响应,那么作为HTTP的响应,我们完全可以返回一个HTML界面

就比如说,我们拿状态码404来举一个例子

cpp 复制代码
<html>
<head>
    <meta http-equiv='Content-Type' content='text/html;charset=utf-8'>
</head>
<body>
    <h1>404 Not Found</h1>
</body>
</html>

它的效果就是下面这样子

cpp 复制代码
// 错误处理函数:生成错误页面并设置到响应中
    void ErrorHandler(const HttpRequest &req, HttpResponse *rsp)
    {
        // 1. 组织一个错误展示页面
        std::string body;
        body += "<html>";
        body += "<head>";
        body += "<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>";
        body += "</head>";
        body += "<body>";
        body += "<h1>";
        body += std::to_string(rsp->_statu);
        body += " ";
        body += Util::StatuDesc(rsp->_statu);
        body += "</h1>";
        body += "</body>";
        body += "</html>";
        // 2. 将页面数据,当作响应正文,放入rsp中
        rsp->SetContent(body, "text/html");
    }

由此,我们就写好了这个错误界面

1.8.服务器相关消息处理回调函数

1.8.1.连接建立消息处理回调函数

还记得我们在Connection类里面预留的那个成员变量------协议上下文吗?

cpp 复制代码
// 连接类,继承enable_shared_from_this以支持在类内部获取自身的shared_ptr
class Connection : public std::enable_shared_from_this<Connection>
{
private:
    // 协议上下文:可存储任意类型的协议处理相关状态和数据
    // 例如HTTP请求的解析状态、WebSocket的握手信息等
    std::any _context; // C++17的any对象,表示任意类型

......
}

// 协议切换函数(在EventLoop线程中执行)
// 用于动态修改连接的处理协议和回调函数
void UpgradeInLoop(const std::any &context,
                   const ConnectedCallback &conn,
                   const MessageCallback &msg,
                   const ClosedCallback &closed,
                   const AnyEventCallback &event)
{
        // 更新协议上下文和各回调函数
     _context = context;
     _connected_callback = conn;//更新用户设置的连接建立回调函数
     _message_callback = msg;//更新用户设置的消息到达回调函数
     _closed_callback = closed;//更新用户设置的连接关闭回调函数
     _event_callback = event;//更新用户设置的任意事件回调函数
}

// 设置协议上下文
void SetContext(const std::any &context)
{
    _context = context;
}

// 获取协议上下文指针
std::any *GetContext()
{
     return &_context;
}

在HTTP服务器中,每个连接(Connection)可能需要处理不同的协议(例如HTTP、WebSocket等)。协议上下文(Protocol Context)就是一个与特定协议相关的数据结构,它保存了在协议处理过程中需要保持的状态信息。

**在我们这个例子里面HttpContext 类就是用于处理HTTP协议的上下文。**它保存了HTTP请求的解析状态(如当前解析到哪一步、已经解析出的请求信息等)以及解析过程中产生的中间数据(如请求行、头部、正文等)。

当我们在一个连接中处理HTTP协议时,我们需要一个HttpContext对象来保存这个连接上HTTP请求的解析状态。这样,每次有数据到来时,我们就可以根据这个上下文继续解析,而不是从头开始。


好了,就不多说了,反正每一个Connection都需要一个协议上下文对象!!!!

只有这样子,对于一个连接来说,才知道自己处理一个HTTP请求处理到哪里了,这样子才不会导致一些错误。

所以当连接一建立,我们就立马给当前连接对应的Conection设置一个协议上下文对象

其实我们在这个函数里面完成的任务很简单,就是一件事,给这个连接对应的Connection对象设置协议处理上下文。

cpp 复制代码
// 新连接建立时的回调函数:设置上下文
    void OnConnected(const PtrConnection &conn)
    {
        conn->SetContext(HttpContext());          // 构造了一个HttpContext对象作为连接上下文
        DBG_LOG("NEW CONNECTION %p", conn.get()); // 调试日志
    }

后续,我们就根据这也协议处理上下文来对我们的数据进行处理解析!!

在协议处理上下文HttpContext类里面,我们完全实现了

  • 确保从输入缓冲区里面读取我们HTTP的一个完整的请求
  • 将读取到的数据解析存放到HttpContext类里面每个对应的成员变量里面

接下来我们只需要进行后续处理即可。

1.8.2.消息到来处理回调函数

消息一到来,我们需要对消息进行处理。我们是根据协议处理上下文来进行数据处理的,

首先,我们需要对协议处理上下文类里面的这3个成员变量特别属性

cpp 复制代码
// HTTP上下文类,负责解析HTTP请求
class HttpContext
{
private:
    int _resp_statu;           // 响应状态码(解析错误时使用)
    HttpRecvStatu _recv_statu; // 当前接收和解析的阶段状态
    HttpRequest _request;      // 已经解析得到的请求信息
......
}

在协议处理上下文HttpContext类里面,我们完全实现了

  • 确保从输入缓冲区里面读取我们HTTP的一个完整的请求
  • 将读取到的数据解析存放到HttpContext类里面的HttpRequest _request;成员变量里面
  • 后续操作我们完全只需要借助这个HttpContext类里面的HttpRequest _request;成员变量来对数据进行处理。

如果说里面发生了错误,那么就会设置另外一个成员变量int _resp_statu; ,也就是 响应状态码(解析错误时使用),这个响应状态码是作为HTTP响应报文里面的状态码,那么上层就可以根据这个状态码来判断,我们这个协议处理的过程中是不是发生了一些错误。

此外,还需要注意的就是HttpRecvStatu _recv_statu;(当前接收和解析的阶段状态)必须处于RECV_HTTP_OVER (请求接收完毕),才算是真正的处理完了一整个HTTP请求。


那么我们首先需要获取到这个HttpContext类里面的HttpRequest _request;成员变量

cpp 复制代码
// 消息到达时的回调函数:缓冲区数据解析+处理
    void OnMessage(const PtrConnection &conn, Buffer *buffer)
    {
        while (buffer->ReadAbleSize() > 0)//缓冲区可读大小>0
        {
            // 1. 获取协议上下文------掌握我们这个消息处理的怎么样了
            HttpContext* context = std::any_cast<HttpContext>(conn->GetContext());//获取这个连接的协议上下文
            // 2. 通过上下文对缓冲区数据进行解析,得到HttpRequest对象
            //    如果缓冲区的数据解析出错,就直接回复出错响应
            //    如果解析正常,且请求已经获取完毕,才开始去进行处理
            context->RecvHttpRequest(buffer);//从输入缓冲区里面读取HTTP请求报文,并组织成一个HTTP请求对象 _request; 
            HttpRequest &req = context->Request();//获取解析后的HTTP请求对象
    }
......
}

此外,我们还需要根据状态码和上下文处理状态来判断,

  • 状态码>=400(也就是发生了错误):那么我们直接返回一个HTTP响应(表示错误发生的),我们之前读取的部分HTTP报文全部要视为报废,我们整个的上下文也都需要进行重置,并且关闭这个连接
  • 协议上下文处理状态不是RECV_HTTP_OVER,说明我们还没有获取到一个完整的HTTP请求

那么我们很快就能

cpp 复制代码
//二.根据_request里面的数据来组织
            HttpResponse rsp(context->RespStatu()); // 创建HTTP响应对象,初始状态码从上下文获取
            if (context->RespStatu() >= 400)//如果说状态码>=400,就说明是出现错误了
            {
                // 进行错误响应,关闭连接
                ErrorHandler(req, &rsp);                        // 填充一个错误显示页面数据到rsp中
                WriteReponse(conn, req, rsp);                   // 组织响应发送给客户端
                context->ReSet();                               // 重置上下文
                buffer->MoveReadOffset(buffer->ReadAbleSize()); // 更新读位置,出错了就把缓冲区数据清空
                conn->Shutdown();                               // 关闭连接
                return;
            }
            if (context->RecvStatu() != RECV_HTTP_OVER)
            {
                // 当前请求还没有接收完整,则退出,等新数据到来再重新继续处理
                return;
            }

执行到这里,我们才能确定这个HTTP请求是完整的。现在我们就能对这个HTTP请求进行处理,然后我们就能组织出一个HTTP响应。

这个步骤就是调用上面我们写好的这些函数,其实很简单。


完整代码

cpp 复制代码
// 消息到达时的回调函数:缓冲区数据解析+处理
    void OnMessage(const PtrConnection &conn, Buffer *buffer)
    {
        while (buffer->ReadAbleSize() > 0)//缓冲区可读大小>0
        {
            //在协议处理上下文HttpContext类里面,我们完全实现了
            //确保从输入缓冲区里面读取我们HTTP的一个完整的请求
            //将读取到的数据解析存放到HttpContext类里面的HttpRequest _request;成员变量里面
            //后续操作我们完全只需要借助这个HttpContext类里面的HttpRequest _request;成员变量来对数据进行处理。
            
            //一.获取HttpContext类里面的HttpRequest _request;成员变量
            // 1. 获取协议上下文------掌握我们这个消息处理的怎么样了
            HttpContext* context = std::any_cast<HttpContext>(conn->GetContext());//获取这个连接的协议上下文
            // 2. 通过上下文对缓冲区数据进行解析,得到HttpRequest对象
            //    如果缓冲区的数据解析出错,就直接回复出错响应
            //    如果解析正常,且请求已经获取完毕,才开始去进行处理
            context->RecvHttpRequest(buffer);//从输入缓冲区里面读取HTTP请求报文,并组织成一个HTTP请求对象 _request; 
            HttpRequest &req = context->Request();//获取解析后的HTTP请求对象

            //二.根据_request里面的数据来组织
            HttpResponse rsp(context->RespStatu()); // 创建HTTP响应对象,初始状态码从上下文获取
            if (context->RespStatu() >= 400)//如果说状态码>=400,就说明是出现错误了
            {
                // 进行错误响应,关闭连接
                ErrorHandler(req, &rsp);                        // 填充一个错误显示页面数据到rsp中
                WriteReponse(conn, req, rsp);                   // 组织响应发送给客户端
                context->ReSet();                               // 重置上下文
                buffer->MoveReadOffset(buffer->ReadAbleSize()); // 更新读位置,出错了就把缓冲区数据清空
                conn->Shutdown();                               // 关闭连接
                return;
            }
            if (context->RecvStatu() != RECV_HTTP_OVER)
            {
                // 当前请求还没有接收完整,则退出,等新数据到来再重新继续处理
                return;
            }
            
            //执行到这里,说明我们的HTTP响应是完整的,我们就算是获取到了一个完整的HTTP响应

            // 3. 请求路由 + 业务处理
            Route(req, &rsp); // 路由分发函数,根据请求方法分发到对应的路由表,并且调用对应的处理函数
            // 4. 对HttpResponse进行组织发送
            WriteReponse(conn, req, rsp);
            // 5. 重置上下文
            context->ReSet();
            // 6. 根据长短连接判断是否关闭连接或者继续处理
            if (rsp.Close() == true)
                conn->Shutdown(); // 短链接则直接关闭
        }
        return;
    }

注意最后一个长连接的部分:

HTTP连接中的长连接和短连接指的是TCP连接的保持策略。

短连接(Short Connection):

  • 每次HTTP请求都会建立一个新的TCP连接,请求完成后立即关闭连接,这意味着每一次HTTP请求都会创建一个Connection对象,这就刚好和我们上面的代码对上了。

  • 优点:管理简单,不会占用服务器资源。

  • 缺点:每次请求都经历三次握手和四次挥手,增加了网络开销和延迟。

  • 适用于请求不频繁的场景。

长连接(Keep-Alive Connection):

  • 在同一个TCP连接上可以发送多个HTTP请求,并且这些请求可以重叠(流水线)或顺序发送。

  • 连接不会在请求完成后立即关闭,而是保持一段时间,等待后续请求。

  • 优点:减少了TCP连接的建立和关闭次数,降低了网络开销和延迟,提高了性能。

  • 缺点:服务器需要维护连接状态,占用资源。

  • 适用于需要多次请求(如网页加载多个资源)的场景。

在HTTP/1.0中,默认使用短连接,如果需要长连接,需要在请求头中加上"Connection: keep-alive"。

在HTTP/1.1中,默认使用长连接,如果需要关闭,需要在请求头中加上"Connection: close"。

在我们的HttpServer中,通过判断请求头中的"Connection"字段来确定当前请求完成后是否关闭连接。如果请求头中Connection为close,则为短连接,响应后关闭;

如果为keep-alive,则保持连接。同时,在响应头中也设置相应的Connection字段,以便客户端知道服务器的策略。

1.8.3.注册回调函数给服务器

其实我们在这个构造函数里面就将回调函数注册给了这个消息处理回调函数

cpp 复制代码
// 构造函数:初始化端口号和超时时间
    HttpServer(int port, int timeout = DEFALT_TIMEOUT) : _server(port)
    {
        _server.EnableInactiveRelease(timeout);                                                                            // 设置非活跃连接释放
        _server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1));                    // 设置连接建立回调
        _server.SetMessageCallback(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2)); // 设置消息到达回调
    }

二.完整代码

cpp 复制代码
#pragma once
#include <regex>
#include <unordered_map>
#include <string>
#include "util.hpp"
#include "httprequest.hpp"
#include "httpresponse.hpp"
#include "httpcontext.hpp"

#define DEFALT_TIMEOUT 10 // 设置默认连接超时释放时间为10秒

class HttpServer
{
private:
    // 定义请求处理函数类型
    using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;
    // 定义路由表类型,包含正则表达式和处理函数的映射
    using Handlers = std::vector<std::pair<std::regex, Handler>>;
    Handlers _get_route;    // GET请求路由表
    Handlers _post_route;   // POST请求路由表
    Handlers _put_route;    // PUT请求路由表
    Handlers _delete_route; // DELETE请求路由表
    std::string _basedir;   // 静态资源根目录
    TcpServer _server;      // TCP服务器对象

private:
    // 错误处理函数:生成错误页面并设置到响应中
    void ErrorHandler(const HttpRequest &req, HttpResponse *rsp)
    {
        // 1. 组织一个错误展示页面
        std::string body;
        body += "<html>";
        body += "<head>";
        body += "<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>";
        body += "</head>";
        body += "<body>";
        body += "<h1>";
        body += std::to_string(rsp->_statu);
        body += " ";
        body += Util::StatuDesc(rsp->_statu);
        body += "</h1>";
        body += "</body>";
        body += "</html>";
        // 2. 将页面数据,当作响应正文,放入rsp中
        rsp->SetContent(body, "text/html");
    }

    // 将HttpResponse中的要素按照http协议格式进行组织,发送
    void WriteReponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp)
    {
        // 1. 先完善HTTP响应头部字段
        // 根据请求设置Connection头部
        if (req.Close() == true)
        {
            rsp.SetHeader("Connection", "close");
        }
        else
        {
            rsp.SetHeader("Connection", "keep-alive");
        }
        // 如果响应体不为空且未设置Content-Length头部,则自动设置Content-Length字段
        if (rsp._body.empty() == false && rsp.HasHeader("Content-Length") == false)
        {
            rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));
        }
        // 如果响应体不为空且未设置Content-Type头部,则自动设置Content-Type为二进制流类型
        if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == false)
        {
            rsp.SetHeader("Content-Type", "application/octet-stream");
        }
        // 如果设置了重定向标志,则设置Location头部
        if (rsp._redirect_flag == true)
        {
            rsp.SetHeader("Location", rsp._redirect_url);
        }

        // 2. 将rsp中的要素,按照http协议格式进行组织
        std::stringstream rsp_str;
        // 写状态行:协议版本 状态码 状态描述
        rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(rsp._statu) << "\r\n";
        // 写响应头部
        for (auto &head : rsp._headers)
        {
            rsp_str << head.first << ": " << head.second << "\r\n";
        }
        // 头部结束空行
        rsp_str << "\r\n";
        // 写响应正文
        rsp_str << rsp._body;

        // 3. 发送数据
        conn->Send(rsp_str.str().c_str(), rsp_str.str().size());
    }

    // 判断是否为静态文件请求
    bool IsFileHandler(const HttpRequest &req)
    {
        // 1. 必须设置了静态资源根目录
        if (_basedir.empty())
        {
            return false;
        }
        // 2. 请求方法,必须是GET / HEAD请求方法
        if (req._method != "GET" && req._method != "HEAD")
        {
            return false;
        }
        // 3. 请求的资源路径必须是一个合法路径
        if (Util::ValidPath(req._path) == false)
        {
            return false;
        }
        // 4. 请求的资源必须存在,且是一个普通文件
        //    有一种请求比较特殊 -- 目录:/, /image/,这种情况给后边默认追加一个index.html
        // index.html    /image/a.png
        // 不要忘了前缀的相对根目录,也就是将请求路径转换为实际存在的路径  /image/a.png  ->   ./wwwroot/image/a.png
        std::string req_path = _basedir + req._path; // 为了避免直接修改请求的资源路径,因此定义一个临时对象
        if (req._path.back() == '/')
        {
            req_path += "index.html";
        }
        if (Util::IsRegular(req_path) == false) // 判断路径是否指向一个普通文件,前提是这个文件已经存在
        {
            return false;
        }
        return true;
    }

    // 静态资源的请求处理 --- 将静态资源文件的数据读取出来,放到rsp的_body中,并设置mime
    void FileHandler(const HttpRequest &req, HttpResponse *rsp) // req是输入型参数,rep是输出型参数
    {
        std::string req_path = _basedir + req._path; // 拼接实际路径------网络通信根目录+资源访问路径
        if (req._path.back() == '/')
        {
            req_path += "index.html";
        }
        bool ret = Util::ReadFile(req_path, &rsp->_body); // 将HTTP请求req_path文件里面的所有内容读取到HTTP响应rsp->_body里面
        if (ret == false)
        {
            return;
        }
        std::string mime = Util::ExtMime(req_path); // 根据文件拓展名来获取对应的MIME类型
        rsp->SetHeader("Content-Type", mime);       // 直接设置进这个HTTP响应中的响应报头去
        return;
    }

    // 功能性请求的分类处理
    void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers)
    {
        // 在对应请求方法的路由表中,查找是否含有对应资源请求的处理函数,有则调用,没有则返回404
        // 思想:路由表存储的是键值对 -- 正则表达式 & 处理函数
        // 使用正则表达式,对请求的资源路径进行正则匹配,匹配成功就使用对应函数进行处理
        //  /numbers/(\d+)       /numbers/12345
        for (auto &handler : handlers) // 遍历方法路由表
        {
            const std::regex &re = handler.first;                     // 方法路由表的左边:正则表达式
            const Handler &functor = handler.second;                  // 方法路由表的右边:请求处理函数
            bool ret = std::regex_match(req._path, req._matches, re); // 根据资源请求路径来根方法路由表里面的正则表达式来进行匹配
            if (ret == false)                                         // 匹配失败,说明当前方法路由表没有对应的处理函数
            {
                continue;
            }
            // 匹配成功,说明方法路由表里面有对应的请求处理函数,调用对应的请求处理函数
            return functor(req, rsp); // 传入请求信息,和空的rsp,执行处理函数
        }
        rsp->_statu = 404; // 未找到对应的处理函数,返回404
    }

    // 路由分发函数:根据请求方法分发到不同的路由表
    void Route(HttpRequest &req, HttpResponse *rsp)
    {
        // 首先我们需要对请求进行分辨,是一个静态资源请求,还是一个功能性请求
        //    静态资源请求,则进行静态资源的处理
        //    功能性请求,则需要通过几个请求路由表来确定是否有处理函数
        //    既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405
        if (IsFileHandler(req) == true)
        {
            // 是一个静态资源请求,则进行静态资源请求的处理
            return FileHandler(req, rsp);
        }
        else//是一个功能性请求,则根据请求方法分发到不同的路由表
        {
            // 根据请求方法分发到不同的路由表
            if (req._method == "GET" || req._method == "HEAD")
            {
                return Dispatcher(req, rsp, _get_route);
            }
            else if (req._method == "POST")
            {
                return Dispatcher(req, rsp, _post_route);
            }
            else if (req._method == "PUT")
            {
                return Dispatcher(req, rsp, _put_route);
            }
            else if (req._method == "DELETE")
            {
                return Dispatcher(req, rsp, _delete_route);
            }
        }
        //既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405
        rsp->_statu = 405; // Method Not Allowed,不支持的请求方法
        return;
    }

    // 新连接建立时的回调函数:设置上下文
    void OnConnected(const PtrConnection &conn)
    {
        conn->SetContext(HttpContext());          // 构造了一个HttpContext对象作为连接上下文
        DBG_LOG("NEW CONNECTION %p", conn.get()); // 调试日志
    }

    // 消息到达时的回调函数:缓冲区数据解析+处理
    void OnMessage(const PtrConnection &conn, Buffer *buffer)
    {
        while (buffer->ReadAbleSize() > 0)//缓冲区可读大小>0
        {
            //在协议处理上下文HttpContext类里面,我们完全实现了
            //确保从输入缓冲区里面读取我们HTTP的一个完整的请求
            //将读取到的数据解析存放到HttpContext类里面的HttpRequest _request;成员变量里面
            //后续操作我们完全只需要借助这个HttpContext类里面的HttpRequest _request;成员变量来对数据进行处理。
            
            //一.获取HttpContext类里面的HttpRequest _request;成员变量
            // 1. 获取协议上下文------掌握我们这个消息处理的怎么样了
            HttpContext* context = std::any_cast<HttpContext>(conn->GetContext());//获取这个连接的协议上下文
            // 2. 通过上下文对缓冲区数据进行解析,得到HttpRequest对象
            //    如果缓冲区的数据解析出错,就直接回复出错响应
            //    如果解析正常,且请求已经获取完毕,才开始去进行处理
            context->RecvHttpRequest(buffer);//从输入缓冲区里面读取HTTP请求报文,并组织成一个HTTP请求对象 _request; 
            HttpRequest &req = context->Request();//获取解析后的HTTP请求对象

            //二.根据_request里面的数据来组织
            HttpResponse rsp(context->RespStatu()); // 创建HTTP响应对象,初始状态码从上下文获取
            if (context->RespStatu() >= 400)//如果说状态码>=400,就说明是出现错误了
            {
                // 进行错误响应,关闭连接
                ErrorHandler(req, &rsp);                        // 填充一个错误显示页面数据到rsp中
                WriteReponse(conn, req, rsp);                   // 组织响应发送给客户端
                context->ReSet();                               // 重置上下文
                buffer->MoveReadOffset(buffer->ReadAbleSize()); // 更新读位置,出错了就把缓冲区数据清空
                conn->Shutdown();                               // 关闭连接
                return;
            }
            if (context->RecvStatu() != RECV_HTTP_OVER)
            {
                // 当前请求还没有接收完整,则退出,等新数据到来再重新继续处理
                return;
            }
            
            //执行到这里,说明我们的HTTP响应是完整的,我们就算是获取到了一个完整的HTTP响应

            // 3. 请求路由 + 业务处理
            Route(req, &rsp); // 路由分发函数,根据请求方法分发到对应的路由表,并且调用对应的处理函数
            // 4. 对HttpResponse进行组织发送
            WriteReponse(conn, req, rsp);
            // 5. 重置上下文
            context->ReSet();
            // 6. 根据长短连接判断是否关闭连接或者继续处理
            if (rsp.Close() == true)
                conn->Shutdown(); // 短链接则直接关闭
        }
        return;
    }

public:
    // 构造函数:初始化端口号和超时时间
    HttpServer(int port, int timeout = DEFALT_TIMEOUT) : _server(port)
    {
        _server.EnableInactiveRelease(timeout);                                                                            // 设置非活跃连接释放
        _server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1));                    // 设置连接建立回调
        _server.SetMessageCallback(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2)); // 设置消息到达回调
    }

    // 设置静态资源根目录
    void SetBaseDir(const std::string &path)
    {
        assert(Util::IsDirectory(path) == true); // 确保路径是目录
        _basedir = path;
    }

    /*设置/添加,请求(请求的正则表达)与处理函数的映射关系*/
    // 注册GET请求处理函数
    void Get(const std::string &pattern, const Handler &handler)
    {
        _get_route.push_back(std::make_pair(std::regex(pattern), handler));
    }
    // 注册POST请求处理函数
    void Post(const std::string &pattern, const Handler &handler)
    {
        _post_route.push_back(std::make_pair(std::regex(pattern), handler));
    }
    // 注册PUT请求处理函数
    void Put(const std::string &pattern, const Handler &handler)
    {
        _put_route.push_back(std::make_pair(std::regex(pattern), handler));
    }
    // 注册DELETE请求处理函数
    void Delete(const std::string &pattern, const Handler &handler)
    {
        _delete_route.push_back(std::make_pair(std::regex(pattern), handler));
    }

    // 设置线程池线程数量
    void SetThreadCount(int count)
    {
        _server.SetThreadCount(count);
    }

    // 启动服务器监听
    void Listen()
    {
        _server.Start();
    }
};

至于一些其他的小接口,我没什么好说的,就很简单。

相关推荐
郑州光合科技余经理2 小时前
私有化B2B订货系统实战:核心模块设计与代码实现
java·大数据·开发语言·后端·架构·前端框架·php
Htojk2 小时前
openssl签发自签名证书的流程
网络协议·https·ssl
北京耐用通信3 小时前
如何用耐达讯自动化Profibus总线光纤中继器解决变频器长距离通信干扰问题?
人工智能·物联网·网络协议·自动化·信息与通信
万岳软件开发小城3 小时前
直播电商系统源码搭建直播带货APP/小程序的完整流程
小程序·php·软件开发·直播带货系统源码·直播电商app开发
傣味洋芋4 小时前
WebSocket
网络·vue.js·websocket·网络协议
Hi202402174 小时前
HTTPS流量分析-网关抓包与解密全解析
网络协议·http·https
小李独爱秋4 小时前
计算机网络经典问题透视:可以通过哪些方案改造互联网,使互联网能够适合于传送音频/视频数据?
运维·服务器·网络协议·计算机网络·音视频
Sammyyyyy4 小时前
PHP 8.6 新特性预览,更简洁的语法与更严谨的类型控制
android·php·android studio
不知疲倦的仄仄4 小时前
HTTP解析/版本变化/TSL
网络协议·tcp/ip·macos