【项目日记】仿mudou的高并发服务器 --- 实现HTTP服务器

news2024/11/30 15:33:41

在这里插入图片描述

对于生命,你不妨大胆一点,
因为我们始终要失去它。
--- 尼采 ---

✨✨✨项目地址在这里 ✨✨✨

✨✨✨https://gitee.com/penggli_2_0/TcpServer✨✨✨


仿mudou的高并发服务器

  • 1 前言
  • 2 Util工具类
  • 3 HTTP协议
    • 3.1 HTTP请求
    • 3.2 HTTP应答
  • 4 上下文解析模块
  • 5 HTTP服务器对象

1 前言

上一篇文章我们基本实现了高并发服务器所需的基础模块,通过TcpServer类可以快速搭建一个TCP服务器。我们的最终目的是使用这个高并发服务器去实现一些业务,那么在网络通信中,我们就可以来实现一下HTTP服务。让浏览器可以访问获取数据。

为了实现HTTP服务器首要的工作就是实现HTTP协议,协议是网络通信的基础!只有确定了协议我们才能正常解析请求报文,并组织应答报文,可以让浏览器成功获取数据。

完成HTTP协议之后,就是设计一种报文解析模块,可以从缓冲区中获取数据,进行解析数据,得到完整请求。

最终将这些整合为一个HTTP服务器模块,设计回调函数,实现HTTP服务器的功能!

2 Util工具类

在HTTP服务器处理中,经常需要一些常用操作,比如切分字符串,编码转换,通过状态码找到对应状态解析… Util工具类就是用来实现这些功能的类!

  1. SplitStr
    • 功能:根据指定的分隔符 sep 将字符串 src 切分成多个子字符串,并将这些子字符串存储在 sub 向量中。
    • 返回值:返回切分后的子字符串数量。
  2. ReadFile
    • 功能:以二进制方式读取文件 filename 的内容到字符串 buf 中。
    • 返回值:如果文件打开和读取成功,返回 true;否则返回 false
  3. WriteFile
    • 功能:以二进制方式将字符串 buf 的内容写入到文件 filename 中,如果文件已存在则覆盖。
    • 返回值:如果文件打开和写入成功,返回 true;否则返回 false
  4. UrlEncode
    • 功能:对字符串 url 进行 URL 编码,可以选择是否将空格编码为 +
    • 返回值:返回编码后的字符串。
  5. HexToC
    • 功能:将十六进制字符转换为对应的整数值。
    • 返回值:返回转换后的整数值。
  6. UrlDecode
    • 功能:对字符串 url 进行 URL 解码,可以选择是否将 + 解码为空格。
    • 返回值:返回解码后的字符串。
  7. StatuDesc
    • 功能:根据给定的状态码 code 返回对应的状态描述。
    • 返回值:返回状态描述字符串,如果状态码未知,则返回 “Unkonw”。
  8. ExtMime
    • 功能:根据 URL 的扩展名返回对应的 MIME 类型。
    • 返回值:返回 MIME 类型字符串,如果扩展名未知,则返回 “application/octet-stream”。
  9. IsLegPath
    • 功能:检查字符串 path 是否是合法的路径,主要检查是否存在非法的 “…” 使用。
    • 返回值:如果路径合法,返回 true;否则返回 false
  10. IsDir
    • 功能:检查给定的路径 dir 是否是一个目录。
    • 返回值:如果是目录,返回 true;否则返回 false
  11. IsRegular
    • 功能:检查给定的路径 dir 是否是一个常规文件。
    • 返回值:如果是常规文件,返回 true;否则返回 false
// 公共方法类
class Util
{
public:
    static ssize_t SplitStr(const std::string &src, const std::string &sep, std::vector<std::string> &sub)
    {
        // 根据sep分隔符切分字符串
        int offset = 0; // 偏移量
        while (offset < src.size())
        {
            size_t pos = src.find(sep, offset);
            // 没有找到sep
            if (pos == std::string::npos)
            {
                // 直接将offset后的字符串当成子串
                sub.push_back(src.substr(offset));
                break;
            }
            // 找到了sep
            else
            {
                size_t len = pos - offset;
                if (len == 0)
                {
                    offset++;
                    continue;
                }

                sub.push_back(src.substr(offset, len));
                offset += len; // 偏移量向后移动
            }
        }
        return sub.size();
    }
    static bool ReadFile(const std::string &filename, std::string *buf)
    {
        std::ifstream ifs(filename, std::ios::binary); // 以读方式打开文件,采取二进制读取方式
        if (ifs.is_open() == false)
        {
            LOG(ERROR, "Open %s Failed!\n", filename.c_str());
            return false;
        }
        // 获取文件大小
        ifs.seekg(0, ifs.end);  // 将读取位置移动到文件末尾
        size_t n = ifs.tellg(); // 此时的偏移量即为文件大小
        ifs.seekg(0, ifs.beg);  // 将读取位置移动到到文件开头

        buf->resize(n); // 将缓冲区大小设置为文件大小
        // 进行写入
        ifs.read(&(*buf)[0], n);
        // 关闭文件
        ifs.close();
        return true;
    }
    static bool WriteFile(const std::string &filename, const std::string &buf)
    {
        std::ofstream ofs(filename, std::ios::binary | std::ios::trunc); // 使用写方式打开进行二进制覆盖写
        if (ofs.is_open() == false)
        {
            LOG(ERROR, "Open %s Failed!\n", filename.c_str());
            return false;
        }
        // 进行写入
        ofs.write(&buf[0], buf.size());
        if (ofs.good() == false)
        {
            LOG(ERROR, "Write %s Failed!\n", filename.c_str());
            return false;
        }
        ofs.close();
        return true;
    }

    static std::string UrlEncode(const std::string &url, bool is_space_encode)
    {
        std::string ret;
        // 进行编码
        for (auto ch : url)
        {
            //. - _ ~ 四个字符绝对不编码
            // 字母与数字不见编码
            if (ch == '.' || ch == '-' || ch == '_' || ch == '~' || isalnum(ch))
            {
                ret += ch;
                continue;
            }
            // 空格编码为 +
            if (ch == ' ' && is_space_encode)
            {
                ret += '+';
                continue;
            }
            // 其余字符进行编码
            char buf[4]; // 编码格式 %___
            snprintf(buf, 4, "%%%02X", ch);
            ret += buf;
        }
        return ret;
    }
    // URL解码
    static char HexToC(char c)
    {
        if (c >= '0' && c <= '9')
        {
            return c - '0';
        }
        else if (c >= 'a' && c <= 'z')
        {
            return c - 'a' + 10;
        }
        else if (c >= 'A' && c <= 'Z')
        {
            return c - 'A' + 10;
        }
        return -1;
    }
    static std::string UrlDecode(const std::string &url, bool is_space_decode)
    {
        std::string res;
        // 遍历字符串 遇到%就进行解码
        for (int i = 0; i < url.size(); i++)
        {
            if (url[i] == '%')
            {
                char v1 = HexToC(url[i + 1]);
                char v2 = HexToC(url[i + 2]);
                char c = (v1 << 4) + v2;
                res += c;
                i += 2;
                continue;
            }
            else if (url[i] == '+' && is_space_decode)
            {
                res += ' ';
                continue;
            }
            else
            {
                res += url[i];
            }
        }
        return res;
    }
    // 返回状态码
    static std::string StatuDesc(int code)
    {
        auto ret = _statu_msg.find(code);
        if (ret == _statu_msg.end())
        {
            return "Unkonw";
        }
        return ret->second;
    }
    // 解析文件后缀
    static std::string ExtMime(const std::string &url)
    {
        size_t pos = url.rfind('.');
        // 没有找到返回
        if (pos == std::string::npos)
        {
            LOG(DEBUG, "没有找到'.'\n");
            return "applicantion/octet-stream";
        }
        std::string str = url.substr(pos);
        LOG(DEBUG, "文件类型:%s\n", str.c_str());
        auto it = _mime_msg.find(str);
        if (it == _mime_msg.end())
        {
            return "applicantion/octet-stream";
        }
        return it->second;
    }
    // 检查是否是合法路径
    static bool IsLegPath(const std::string &path)
    {
        // 采用计数法
        int level = 0;
        std::vector<std::string> subdir;
        int ret = SplitStr(path, "..", subdir);
        if (ret < 0)
            return false;
        for (auto &s : subdir)
        {
            if (s == "..")
            {
                level--;
                if (level < 0)
                    return false;
                continue;
            }
            else
                level++;
        }
        return true;
    }
    static bool IsDir(const std::string &dir)
    {
        struct stat st;
        int n = ::stat(dir.c_str(), &st);
        if (n < 0)
            return false;
        return S_ISDIR(st.st_mode);
    }
    static bool IsRegular(const std::string &dir)
    {
        struct stat st;
        int n = ::stat(dir.c_str(), &st);
        if (n < 0)
            return false;
        return S_ISREG(st.st_mode);
    }
};

3 HTTP协议

3.1 HTTP请求

http协议的请求格式是这样的:

  1. 请求行:包含请求方法,资源路径URL,HTTP版本
  2. 请求报头:以键值对的形式储存必要信息
  3. 空行:用于识别正文
  4. 请求正文:储存本次请求的正文
    在这里插入图片描述
    针对这个结构我们可以搭建一个HTTP请求的基础框架:
class
{
public:
	std::string _method;                                   // 请求方法
    std::string _path;                                     // 查询路径
    std::string _version;                                  // 协议版本
    std::string _body;                                     // 请求正文
    std::smatch _matches;                                  // 资源路径的正则提取解析
    std::unordered_map<std::string, std::string> _headers; // 请求报头
    std::unordered_map<std::string, std::string> _params;  // 查询字符串
	
};

然后继续设置一些接口:

  1. 插入头部字段的接口
  2. 检查请求中是否有该头部字段
  3. 插入查询字符串
  4. 检查请求中是否有该查询字符串
  5. 获取查询字符串
  6. 获取正文长度
  7. 是否为长连接
class HttpRequest
{
public:
    std::string _method;                                   // 请求方法
    std::string _path;                                     // 查询路径
    std::string _version;                                  // 协议版本
    std::string _body;                                     // 请求正文
    std::smatch _matches;                                  // 资源路径的正则提取解析
    std::unordered_map<std::string, std::string> _headers; // 请求报头
    std::unordered_map<std::string, std::string> _params;  // 查询字符串
public:
    // 重置请求
    void Reset()
    {
        _method.clear();
        _path.clear();
        _version.clear();
        _body.clear();
        std::smatch tmp;
        _matches.swap(tmp);
        _headers.clear();
        _params.clear();
    }
    // 插入头部字段
    void SetHeader(const std::string &key, const std::string &val)
    {
        _headers.insert(std::make_pair(key, val));
    }
    // 判断是否有该头部字段
    bool HasHeader(const std::string &key) const
    {
        auto it = _headers.find(key);
        if (it == _headers.end())
        {
            return false;
        }
        return true;
    }
    // 获取头部字段
    std::string GetHeader(const std::string &key) const
    {
        auto it = _headers.find(key);
        if (it == _headers.end())
        {
            return "";
        }
        return it->second;
    }
    // 插入查询字符串
    void SetParam(const std::string &key, const std::string &val)
    {
        _params.insert(std::make_pair(key, val));
    }
    // 判断是否有该查询字符串
    bool HasParam(const std::string &key)
    {
        auto it = _params.find(key);
        if (it == _params.end())
        {
            return false;
        }
        return true;
    }
    // 获取查询字符串
    std::string GetParam(const std::string &key)
    {
        auto it = _params.find(key);
        if (it == _params.end())
        {
            return "";
        }
        return it->second;
    }
    // 获取正文长度
    size_t ContentLength()
    {
        bool ret = HasHeader("Content-Length");
        if (ret)
        {
            // 转换为长整形
            return std::stol(GetHeader("Content-Length"));
        }
        return 0;
    }
    bool Close() const
    {
        // 没有Connection字段或者Connection字段是close 就是短连接
        if (HasHeader("Connection") == true && GetHeader("Connection") == "close")
        {
            return true;
        }
        return false;
    }
};

这样一个基础的HTTP请求结构就设计好了!

3.2 HTTP应答

http协议的应答格式是这样的:

  1. 状态行:包含HTTP版本,状态码,状态码描述
  2. 应答报头:储存必要信息
  3. 换行符:用于识别正文
  4. 正文:储存应答的正文结构

在这里插入图片描述
根据应答结构,我们可以搭建其应答框架:

  1. 设置头部字段
  2. 获取头部字段
  3. 设置正文
  4. 设置应答状态
  5. 是否是长连接
class HttpResponse
{
public:
    int _statu;                                            // 状态码
    bool _rediect_flag;                                    // 重定向标志
    std::string _rediect_url;                              // 重定向的路径
    std::string _body;                                     // 响应正文
    std::unordered_map<std::string, std::string> _headers; // 响应报头

public:
    HttpResponse(int statu) : _statu(statu) {}
    // 重置响应
    void Reset()
    {
    }
    // 插入头部字段
    void SetHeader(const std::string &key, const std::string &val)
    {
        _headers.insert(std::make_pair(key, val));
    }
    // 判断是否有该头部字段
    bool HasHeader(const std::string &key)
    {
        auto it = _headers.find(key);
        if (it == _headers.end())
        {
            return false;
        }
        return true;
    }
    // 获取头部字段
    std::string GetHeader(const std::string &key)
    {
        auto it = _headers.find(key);
        if (it == _headers.end())
        {
            return "";
        }
        return it->second;
    }
    void SetContent(const std::string &body, const std::string &type = "text/html")
    {
        _body = body;
        SetHeader("Content-Type", type);
    }
    void SetRediret(const std::string &url, int statu = 302)
    {
        _statu = statu;
        _rediect_flag = true;
        _rediect_url = url;
    }
    bool Close()
    {
        // 没有Connection字段或者Connection字段是close 就是短连接
        if (HasHeader("Connection") == true && GetHeader("Connection") == "close")
        {
            return true;
        }
        return false;
    }
};

这样HTTP协议的请求与应答我们就完成了!可以进一步进行请求与应答的解析工作了!

4 上下文解析模块

针对应答的反序列化,我们不在协议模块中直接进行设置,因为我们无法保证连接一次就可以获取完整的报文结构,所以在一个连接中要维护一个上下文结构,可以在多次处理时知道本次处理应该从何处进行!

在这个上下文中首先我们就需要一个状态变量,可以标识当前应该处理什么字段:

    RECV_HTTP_ERROR --- 处理出错
    RECV_HTTP_LINE --- 处理请求行
    RECV_HTTP_HEAD --- 处理头部字段
    RECV_HTTP_BODY --- 处理正文
    RECV_HTTP_OVER --- 处理完成

每一个上下文都匹配一个请求对象,将解析好的字段储存到这个请求对象中:

  1. 处理请求行:处理请求行时使用正则表达式快速进行处理,注意URL编码的转换,请求方法的大小写以及拆分出查询字符串!
  2. 处理头部字段:一行一行的进行处即可,直到遇到空行!
  3. 处理正文:从缓冲区读取出正文长度的数据,不够继续等待,够了就返回。

需要注意的是,获取数据时不一定会获取到预期的数据,一定要做好情况分类,保证正常读取!
避免出现数据过长,数据不足等情况!

上下文每次解析都将数据及时储存到该上下文中对应的请求对象中!

typedef enum
{
    RECV_HTTP_ERROR,
    RECV_HTTP_LINE,
    RECV_HTTP_HEAD,
    RECV_HTTP_BODY,
    RECV_HTTP_OVER
} HttpRecvStatu;

static const int MAX_SIZE = 8192;

class HttpContext
{
private:
    int _resp_statu;           // 响应状态码
    HttpRequest _request;      // 请求信息
    HttpRecvStatu _recv_statu; // 解析状态
private:
    bool ParseHttpLine(const std::string &line)
    {
        // 对请求行进行正则表达式解析
        // 设置解析方法: 忽略大小写!
        // std::regex re("(GET|HEAD|POST|PUT|DELETE) ([^?]+)\\?(.*) (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
        std::regex re("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
        //(GET|HEAD|POST|PUT|DELETE) 获取GET...请求方法
        //([^?]+) 匹配若干个 非?字符 直到? --- 获取资源路径
        //\\?(.*) \\?表示匹配原始?字符 (.*)访问到空格 ---获取请求参数
        //(HTTP/1\\.[01]) 匹配HTTP/1. 01任意一个字符
        //(?:\n|\r\n)? 匹配\n或者\r\n (?: ...)表示匹配摸个格式字符串但是不提取 .结尾的?表示前面的表达式0次或1次
        std::smatch matches;
        bool ret = std::regex_match(line, matches, re);
        if (ret == false)
        {
            LOG(ERROR, "regex_match failed\n");
            _resp_statu = 400; // Bad Reauest!
            return false;
        }
        // 0:GET /a/b/c/search?q=keyword&lang=en HTTP/1.1
        // 1:GET
        // 2:/a/b/c/search
        // 3:q=keyword&lang=en
        // 4:HTTP/1.1
        _request._method = matches[1];
        // 请求方法统一转换为大写
        std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);
        _request._path = Util::UrlDecode(matches[2], false);
        _request._version = matches[4];
        // 对查询字符串进行解析
        std::string str = matches[3];
        std::vector<std::string> substr;
        // 进行切分字符串
        Util::SplitStr(str, "&", substr);
        // 遍历容器
        for (auto s : substr)
        {
            // 寻找'='
            size_t pos = s.find("=");
            if (pos == std::string::npos)
            {
                LOG(ERROR, "ParseHttpLine Failed\n");
                _recv_statu = RECV_HTTP_ERROR;
                _resp_statu = 400; // BAD Resquest
                return false;
            }
            // 找到了 ‘=’
            std::string key = Util::UrlDecode(s.substr(0, pos), true);
            std::string value = Util::UrlDecode(s.substr(pos + 1), true);
            LOG(INFO, "查询字符串%s: %s\n", key.c_str(), value.c_str());
            _request.SetParam(key, value);
        }

        return true;
    }
    // 解析请求行
    bool RecvHttpLine(Buffer *buf)
    {
        if (_recv_statu != RECV_HTTP_LINE)
            return false;
        // 获取一行数据 带有\r\n
        std::string line = buf->GetLineAndPop();
        if (line.size() == 0)
        {
            // 缓存区中没有完整的一行数据 进行分类讨论
            // 如果缓冲区数据大于极限值
            if (buf->ReadAbleSize() > MAX_SIZE)
            {
                _resp_statu = 414; // URL TOO LONG
                _recv_statu = RECV_HTTP_ERROR;
                return false;
            }
            // 反之不处理
            return true;
        }
        // 一行的数据过长
        if (line.size() > MAX_SIZE)
        {
            _resp_statu = 414; // URL TOO LONG
            _recv_statu = RECV_HTTP_ERROR;
            return false;
        }
        // 进行解析
        bool ret = ParseHttpLine(line);
        if (ret == false)
            return false;
        // 请求行解析完毕 开始解析请求报头
        _recv_statu = RECV_HTTP_HEAD;
        return true;
    }
    // 解析报头
    bool RecvHttpHead(Buffer *buf)
    {
        if (_recv_statu != RECV_HTTP_HEAD)
            return false;
        // 解析请求报头直到遇到空行
        while (1)
        {
            std::string line = buf->GetLineAndPop();
            // LOG(DEBUG, "line:%s\n", line.c_str());
            if (line.size() == 0)
            {
                // 缓存区中没有完整的一行数据 进行分类讨论
                // 如果缓冲区数据大于极限值
                if (buf->ReadAbleSize() > MAX_SIZE)
                {
                    // LOG(ERROR, "line too long\n");
                    _resp_statu = 414; // URL TOO LONG
                    _recv_statu = RECV_HTTP_ERROR;
                    return false;
                }
                // 反之不处理 等待新数据到来
                // LOG(ERROR, "wait new buffer\n");
                return true;
            }
            // 一行的数据过长
            if (line.size() > MAX_SIZE)
            {
                // LOG(ERROR, "line too long\n");
                _resp_statu = 414; // URL TOO LONG
                _recv_statu = RECV_HTTP_ERROR;
                return false;
            }
            if (line == "\n" || line == "\r\n")
            {
                // LOG(ERROR, "line is empty\n");
                break;
            }
            // LOG(INFO, "line正常 进行解析处理");
            //  去除换行 \r \n
            if (line.back() == '\n')
                line.pop_back();
            if (line.back() == '\r')
                line.pop_back();
            // 进行解析
            bool ret = ParseHttpHead(line);
            if (ret == false)
                return false;
        }
        // 头部解析完成 继续解析正文
        _recv_statu = RECV_HTTP_BODY;
        return true;
    }
    bool ParseHttpHead(const std::string &line)
    {
        // 每一行都是key: val\r\n 格式
        // LOG(DEBUG, "ParseHttpHead:%s\n", line.c_str());
        // 进行解析即可
        size_t pos = line.find(": ");
        if (pos == std::string::npos)
        {
            LOG(ERROR, "ParseHttpLine Failed\n");
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 400; // BAD Resquest
            return false;
        }
        std::string key = line.substr(0, pos);
        std::string val = line.substr(pos + 2);
        // LOG(DEBUG, "%s: %s\n", key.c_str(), val.c_str());
        _request.SetHeader(key, val);
        return true;
    }
    bool RecvHttpBody(Buffer *buf)
    {
        if (_recv_statu != RECV_HTTP_BODY)
            return false;
        // 获取正文长度
        size_t len = _request.ContentLength();
        // 没有正文 直接读取完毕
        if (len == 0)
        {
            _recv_statu = RECV_HTTP_OVER;
            return true;
        }
        // 当前已经接受了多少数据 _request._body
        size_t relen = len - _request._body.size();
        // 接收正文放到body中 但是要考虑当前缓冲区中的数据是否是全部的报文
        // 缓冲区数据包含所有正文
        if (relen <= buf->ReadAbleSize())
        {
            // 加到_request.body的后面
            _request._body.append(buf->ReadPos(), relen);
            buf->MoveReadOffset(relen);
            _recv_statu = RECV_HTTP_OVER;
            return true;
        }
        // 缓冲区无法满足正文
        _request._body.append(buf->ReadPos(), buf->ReadAbleSize());
        buf->MoveReadOffset(buf->ReadAbleSize());
        return true;
    }

public:
    HttpContext() : _resp_statu(200), _recv_statu(RECV_HTTP_LINE) {}
    int RespStatu() { return _resp_statu; }
    HttpRequest &Request() { return _request; }
    HttpRecvStatu RecvStatu() { return _recv_statu; }
    // 重置上下文
    void Reset()
    {
        _resp_statu = 200;
        _recv_statu = RECV_HTTP_LINE;
        _request.Reset();
    }
    void RecvhttpRequest(Buffer *buf)
    {
        // 根据不同的状态 处理不同情况
        // 处理完不要break 因为处理完 可以继续进行处理下面的数据 而不是直接退出等待新数据!
        switch (_recv_statu)
        {
        case RECV_HTTP_LINE:
            RecvHttpLine(buf);
        case RECV_HTTP_HEAD:
            RecvHttpHead(buf);
        case RECV_HTTP_BODY:
            RecvHttpBody(buf);
        }
        return;
    }
};

5 HTTP服务器对象

现在,HTTP协议我们实现了,可以通过协议进行通信!如何通过缓冲区获取请求的上下文方法我们也实现了,可以在缓冲区中读取数据,即使一次没有发送全,下一次可以继续在原有进度上继续进行解析!

那么接下来,我们对这些功能进行一个整合封装,实现HTTP服务器的功能!

首先这个模块中有请求方法/资源路径 与 函数指针的映射关系表,可以根据http请求的url找到对应的资源

  • 表中记录了对于哪个请求,应该使用哪一个函数来进行业务处理
  • 当服务器收到一个请求,就要在请求路由表中,查找是否存在对应的处理函数,没有就返回404 Not Found
  • 这样做的好处是用户只需要实现业务处理函数,然后将请求与函数的对应关系添加到服务器中,服务器只需要接收数据,解析数据,查找路由表映射关系,执行业务处理函数!

要实现简便的搭建Http服务器,所需的要素和提供的功能有以下几项:

  1. GET请求的路由映射表 — 功能性请求的处理
  2. POST请求的路由映射表
  3. PUT请求的路由映射表
  4. DELETE请求的路由映射表
  5. 高性能TCP服务器 — 进行连接的IO操作
  6. 静态资源相对根目录 — 实现静态资源的处理

再来看服务器的处理流程,只有熟悉了服务器处理流程,才能明白代码逻辑然后进行功能实现!

  1. 从Socket接收数据。放到接收缓冲区
  2. 调用OnMessage回调函数进行业务处理
  3. 对请求进行解析,得到一个HttpRequest结构,包含所有的请求要素
  4. 进行请求的路由查找 — 找到对应请求的处理方法
    • 静态资源请求 — 一些实体文件资源的请求
    • 功能性请求 — 在请求中根据路由映射表查找处理函数
  5. 对静态资源请求/功能性请求进行处理完毕后,得到了一个填充了响应信息的HttpReaponse对象,组成http格式报文
成员变量:
  1. GET请求的路由映射表 _get_route — 通过正则表达式映射处理函数
  2. POST请求的路由映射表 _post_route
  3. PUT请求的路由映射表 _put_route
  4. DELETE请求的路由映射表 _delete_route
  5. 静态资源根目录 _basedir
  6. TcpServer服务器 _server
私有成员函数
  1. 设置上下文 OnConnect(const PtrConn& conn):给连接设置空的上下文

  2. 缓冲区数据解析+处理 OnMessage(const PtrConn& conn , Buffer *buf):只要缓冲区里有数据就持续处理首先先获取上下文,通过上下文对缓冲区数据进行处理得到HttpRequest对象,根据状态码>= 400判断解析结果 ,如果解析出错 ,直接回复出错响应 ErrorHandler(req , rsp) 并关闭连接! 请求解析不完整 直接return 等待下一次处理。直到解析完毕 才去进行数据处理。然后进行请求路由Route(req ,&rsp) 在路由中进行数据处理业务处理,处理后得到应答报文,对HttpResponse 进行组织发送 WriteResponse(const PtrConn& conn , req , rsp)此时重置连接的上下文!根据长短连接判断是否要关闭连接或者继续保持连接

  3. 路由查找 Route:对请求进行判断,是请求静态资源还是功能性请求

    • 静态资源请求 :判断是否是静态资源请求,然后进行静态资源的处理
    • 功能性请求 : 通过req的请求方法判断使用哪一个路由表,使用Dispatch进行任务派发
    • 既不是静态资源一般是功能性请求 就返回404!
  4. 判断是否是静态资源请求 IsFileHandler:首先必须设置了静态资源根目录,请求方法必须是GET / HEAD
    ,请求的资源路径必须是合法路径,请求的资源必须存在! 当请求路径是"/"要补全一个初始页面 index.html,注意合并_basedir得到真正的路径!

  5. 静态资源的请求处理 FileHandler:将静态资源的数据读取出来,放到rsp的正文中,直接读取路径上的文件放到正文中,获取mime文件类型,添加到头部字段Content-Type!

  6. 功能性请求的任务分发 Dispatcher:在对应路由表中寻找是否有对应请求的处理函数,有就直接进行调用 没有就返回404。路由表中储存的是 正则表达式->处理函数 的键值对。使用正则表达式进行匹配 ,匹配成功就进行执行函数

  7. 发送应答WriteResponse:将HttpReaponse应答按照http应答格式进行组织发送 ,首先完善头部字段 ,然后将rsp的元素按照http协议的格式进行组织,最终发送数据

  8. 处理错误应答ErrorHandler: 提供一个错误展示页面,将页面数据当作响应正文放入rsp中

公有成员函数:
  1. 构造函数
  2. 插入关系映射到GET路由表、POST路由表、PUT路由表、DELETE路由表。
  3. 设置静态资源根目录
  4. 设置线程数量
  5. 启动Http服务器
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 _delete_route; // DELETE方法处理函数映射表
    Handlers _put_route;    // PUT方法处理函数映射表
    std::string _basedir;
    TcpServer _server;

public:
    // 设置空白上下文
    void OnConnect(const PtrConn &conn)
    {
        conn->SetContext(HttpContext());
        LOG(INFO, "NEW CONNECTION :%p\n", this);
    }
    void ErrorHandler(const HttpRequest &req, HttpResponse *rsp)
    {
        // 提供一个错误展示页面
        std::string body;
        body += "<!DOCTYPE html>";
        body += "<html lang='en'>";
        body += "<head>";
        body += "<meta charset='UTF-8'>";
        body += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";
        body += "<title>Error " + std::to_string(rsp->_statu) + " - Server Error</title>";
        body += "<style>";
        body += "body { background-color: #f2f2f2; color: #333; font-family: Arial, sans-serif; }";
        body += "h1 { color: #d8000c; background-color: #ffbaba; border: 1px solid #d8d8d8; padding: 10px; text-align: center; }";
        body += "div.container { max-width: 600px; margin: 50px auto; padding: 20px; background-color: #fff; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }";
        body += "</style>";
        body += "</head>";
        body += "<body>";
        body += "<div class='container'>";
        body += "<h1>";
        body += "Error " + std::to_string(rsp->_statu) + " - " + Util::StatuDesc(rsp->_statu);
        body += "</h1>";
        body += "<p>We're sorry, but something went wrong.</p>";
        body += "</div>";
        body += "</body>";
        body += "</html>";

        // 将页面数据,当作响应正文,放入rsp中
        rsp->SetContent(body, "text/html");
    }

    // 缓冲区数据解析+处理
    void OnMessage(const PtrConn &conn, Buffer *buf)
    {
        while (buf->ReadAbleSize() > 0)
        {
            // 从连接中获取上下文
            HttpContext *context = conn->GetContext()->Get<HttpContext>();
            // 从缓冲区中获取数据 处理后得到Request
            context->RecvhttpRequest(buf);
            HttpRequest req = context->Request();
            // 根据请求构建应答
            HttpResponse rsp(context->RespStatu());
            // 根据状态码判断处理结果
            // LOG(DEBUG, "res->statu :%d\n", rsp._statu);
            // 状态码大于400说明解析出错 直接退出
            if (context->RespStatu() >= 400)
            {
                // 重置上下文
                context->Reset();
                // 清空缓冲区
                buf->MoveReadOffset(buf->ReadAbleSize());
                // 获取错误响应
                ErrorHandler(req, &rsp);
                // 发送错误请求
                WriteResponse(conn, req, rsp);
                // 关闭连接
                conn->Shutdown();
                return;
            }
            // 如果解析没有完成就等待下一次处理
            if (context->RecvStatu() != RECV_HTTP_OVER)
            {
                // 退出等待新数据到来 重新进行处理
                return;
            }
            // 请求解析完成进行处理
            Route(req, &rsp);
            LOG(INFO, "%s\n", rsp._body.c_str());
            if (rsp._statu >= 400)
            {
                // 获取错误响应
                ErrorHandler(req, &rsp);
                // 发送错误请求
                WriteResponse(conn, req, rsp);
                // 重置上下文
                context->Reset();
                // 关闭连接
                conn->Shutdown();
                return;
            }
            // 获取应答
            WriteResponse(conn, req, rsp);
            // 重置上下文
            context->Reset();
            // 根据长短连接判断是否需要关闭连接
            if (rsp.Close() == true)
                conn->Shutdown();
        }
        return;
    }

    bool Route(HttpRequest &req, HttpResponse *rsp)
    {
        // 判断是否是静态资源处理
        if (IsFileHandler(req) == true)
            return FileHandler(req, rsp);

        // 判断是否实际功能性请求
        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);
        // 不是静态请求也不是功能性请求
        else
        {
            rsp->_statu = 405; // Method Not Allowed
            return false;
        }
    }
    // 判断是否是静态资源
    bool IsFileHandler(HttpRequest &req)
    {
        // 首先_basedir必须存在
        if (_basedir.empty() == true)
            return false;
        // 请求方法必须是 GET / HEAD
        if (req._method != "GET" && req._method != "HEAD")
            return false;
        // 请求路径必须是合法路径
        if (Util::IsLegPath(req._path) == false)
            return false;
        // 请求的资源必须存在
        std::string req_path = _basedir + req._path;
        // 如果直接请求的网络根目录要补全一个初始页面
        if (req_path.back() == '/')
            req_path += "index.html";
        if (Util::IsRegular(req_path) == false)
            return false;
        // req请求路径的真正路径
        req._path = req_path;
        return true;
    }
    // 静态资源的请求处理
    bool FileHandler(HttpRequest &req, HttpResponse *rsp)
    {
        LOG(INFO, "静态资源请求:%s\n", req._path.c_str());
        // 将请求资源读取到应答正文中
        bool ret = Util::ReadFile(req._path, &rsp->_body);
        if (ret == false)
        {
            // 数据读取失败
            LOG(ERROR, "数据读取失败\n");
            return false;
        }
        // 获取文件类型mime
        std::string mime = Util::ExtMime(req._path);
        LOG(DEBUG, "Content-Type:%s\n", mime.c_str());
        // 添加到应答报头
        rsp->SetHeader("Content-Type", mime);
        return true;
    }
    // 功能性请求的任务分发
    bool Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers)
    {
        // LOG(INFO, "%s 功能性请求:%s\n", req._method.c_str(), req._path.c_str());
        //  首先根据路由表找到目标
        for (auto &handler : handlers)
        {
            const std::regex &re = handler.first;
            // 根据这个正则表达式进行解析
            bool ret = std::regex_match(req._path, req._matches, re);
            if (ret == false)
                continue;
            // 找到了就进行执行函数
            Handler Functor = handler.second;
            Functor(req, rsp);
            return true;
        }
        // 没有找到目标
        LOG(DEBUG, "404 Not Found\n");
        rsp->_statu = 404; // 设置为Not Found
        return false;
    }
    // 将HttpReaponse应答按照http应答格式进行组织发送
    void WriteResponse(const PtrConn &conn, const HttpRequest &req, HttpResponse &rsp)
    {
        // 首先先完善头部字段
        if (req.Close() == true)
            rsp.SetHeader("Connection", "close");
        else
            rsp.SetHeader("Connection", "keep-alive");
        if (rsp._body.empty() == true && rsp.HasHeader("Content-Length") == false)
            rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));
        if (rsp._body.empty() == true && rsp.HasHeader("Content-Type") == false)
            rsp.SetHeader("Content-Type", "application/octet-stream");
        if (rsp._rediect_flag == true)
            rsp.SetHeader("Location", rsp._rediect_url);
        // 将rsp组织成http格式的应答报文
        std::stringstream rsp_str;
        rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(rsp._statu) << "\r\n";
        for (auto &it : rsp._headers)
        {
            rsp_str << it.first << ": " << it.second << "\r\n";
        }
        rsp_str << "\r\n";
        rsp_str << rsp._body << "\r\n";
        // 进行发送
        // LOG(INFO, "WriteResponse Send :%s \n", rsp_str.str().c_str());
        conn->Send(rsp_str.str().c_str(), rsp_str.str().size());
    }

public:
    HttpServer(int port, int timeout = DEFALT_TIMEOUT) : _server(port)
    {
        _server.SetConnectCB(std::bind(&HttpServer::OnConnect, this, std::placeholders::_1));
        _server.SetMessageCB(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));
        _server.EnableActiveRelease(timeout); // 设置超时时间
    }
    // 插入关系映射到GET路由表
    void GET(const std::string &pattern, const Handler &func) { _get_route.push_back(std::make_pair(std::regex(pattern), func)); }
    void POST(const std::string &pattern, const Handler &func) { _post_route.push_back(std::make_pair(std::regex(pattern), func)); }
    void PUT(const std::string &pattern, const Handler &func) { _put_route.push_back(std::make_pair(std::regex(pattern), func)); }
    void DELETE(const std::string &pattern, const Handler &func) { _delete_route.push_back(std::make_pair(std::regex(pattern), func)); }
    void SetBaseDir(const std::string &dir)
    {
        assert(Util::IsDir(dir) == true);
        _basedir = dir;
    }
    // 设置服务器线程数量
    void SetThreadSize(size_t size)
    {
        _server.SetThreadSize(size);
    }
    // 启动服务器
    void Start()
    {
        _server.Start();
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2250513.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从0在自己机器上部署AlphaFold 3

本文介绍如何在自己本地机器上安装AlphaFold 3。 在10月份&#xff0c;Google DeepMind的首席执行官Demis Hassabis和高级研究科学家John M. Jumper所领导的团队&#xff0c;利用AI技术成功预测了几乎所有已知蛋白质的结构&#xff0c;开发出备受赞誉的AlphaFold&#xff0c;并…

faiss库中ivf-sq(ScalarQuantizer,标量量化)代码解读-6

调试 经过gdb调试获取的调用栈内容如下&#xff0c;链接&#xff1a; 步骤函数名称文件位置说明1faiss::IndexFlatCodes::add/faiss/IndexFlatCodes.cpp:24在 add 方法中&#xff0c;检查是否已经训练完成&#xff0c;准备添加向量到索引中。2std::vector<unsigned char&g…

SQL进阶——子查询与视图

在SQL中&#xff0c;子查询和视图是两种强大的技术&#xff0c;用于处理复杂的查询、提高代码的重用性以及优化查询性能。子查询允许开发者在查询中嵌套其他查询&#xff0c;而视图则是对复杂查询的封装&#xff0c;可以简化开发工作并提高代码的可维护性。 本章将深入探讨如何…

【论文阅读】 Learning to Upsample by Learning to Sample

论文结构目录 一、之前的上采样器二、DySample概述三、不同上采样器比较四、整体架构五、设计过程&#xff08;1&#xff09;初步设计&#xff08;2&#xff09;第一次修改&#xff08;3&#xff09;第二次修改&#xff08;4&#xff09;第三次修改 六、DySample四种变体七、复…

Online Judge——【前端项目初始化】项目通用布局开发及初始化

目录 一、新建layouts二、更新App.vue文件三、选择一个布局&#xff08;Layout&#xff09;四、通用菜单Menu的实现菜单路由改为读取路由文件 五、绑定跳转事件六、同步路由到菜单项 一、新建layouts 这里新建一个专门存放布局的布局文件layouts&#xff1a; 然后在该文件夹&…

uniapp首页样式,实现菜单导航结构

实现菜单导航结构 1.导入字体图标库需要的文件 2.修改引用路径iconfont.css 3.导入到App.vue中 <style>import url(./static/font/iconfont.css); </style>导航区域代码 VUE代码 <template><view class"home"><!-- 导航区域 --><…

《代码随想录》刷题笔记——栈与队列篇【java实现】

文章目录 用栈实现队列用队列实现栈有效的括号我的解法代码随想录 删除字符串中的所有相邻重复项我的解法代码随想录栈解法字符串充当栈※双指针 逆波兰表达式求值我的解法代码随想录 滑动窗口最大值我的解法暴力解法暴力解法一点优化单调队列 代码随想录单调队列 前 K 个高频元…

STM32 ADC --- 知识点总结

STM32 ADC — 知识点总结 文章目录 STM32 ADC --- 知识点总结cubeMX中配置注解单次转换模式、连续转换模式、扫描模式单通道采样的情况单次转换模式&#xff1a;连续转换模式&#xff1a; 多通道采样的情况禁止扫描模式&#xff08;单次转换模式或连续转换模式&#xff09;单次…

UaGateway:实现OPC DA和OPC UA的高效转换

随着工业4.0和智能制造的深入推进&#xff0c;工业自动化系统之间的互联互通需求日益迫切。UaGateway作为一种高效的协议转换工具&#xff0c;正在成为各类工业应用中不可或缺的桥梁。本文将重点介绍UaGateway在实现OPC DA到OPC UA转换方面的主要功能、应用场景和实际案例。 Ua…

安能物流 All in TiDB 背后的故事与成果

导读 在数字化转型的浪潮中&#xff0c;安能物流通过技术创新不断提升物流效率&#xff0c;迈出了全链路 All in TiDB 的重要一步。本文将深入探讨安能物流如何选择 TiDB 作为核心数据库&#xff0c;以应对高并发、数据处理能力和系统可扩展性等挑战。通过 TiDB 的弹性扩展能力…

回声消除延时估计的一些方法

在音频信号处理&#xff0c;尤其是在回声消除和语音通信中&#xff0c;延时估计是一个至关重要的任务。回声消除技术旨在减少或消除在语音通信中由于信号反射而产生的回声。为了有效地实现这一点&#xff0c;系统需要准确估计发送信号和接收信号之间的延迟。通过了解延迟&#…

从简单的自动化脚本到复杂的智能助手:Agent技术的实践与应用

现代软件开发中&#xff0c;Agent技术正在悄然改变着我们构建应用程序的方式。一个Agent就像是一个能独立完成特定任务的智能助手&#xff0c;它可以感知环境、作出决策并采取行动。让我们通过实际案例&#xff0c;深入了解如何运用Agent技术来构建智能系统。 想象你正在开发一…

postman使用正则表达式提取数据实战篇!

之前篇章中postman多接口关联使用的是通过JSON提取器的方式进行提取。 除了JSON提取器提取数据外还可通过另一种方式——正则表达式来提取数据。 1、使用正则表达式提取器实现接口关联&#xff0c;match匹配 正则匹配表达式将需要提取的字段key:value都放入表达式中&#xff…

Flume 与 Kafka 整合实战

目录 一、Kafka 作为 Source【数据进入到kafka中&#xff0c;抽取出来】 &#xff08;一&#xff09;环境准备与配置文件创建 &#xff08;二&#xff09;创建主题 &#xff08;三&#xff09;测试步骤 二、Kafka 作为 Sink数据从别的地方抽取到kafka里面】 &#xff08;…

存储服务器一般做是做什么阵列?详细列举一下

存储服务器通常使用 RAID&#xff08;Redundant Array of Independent Disks&#xff09; 阵列技术来管理磁盘&#xff0c;以提高数据的性能、可靠性和可用性。所选择的 RAID 类型取决于存储服务器的具体用途和需求&#xff0c;比如性能要求、容量需求、容错能力等。 以下是存…

无人机的起降装置:探索起飞和降落的秘密 !

一、起降系统的运行方式 起飞方式 垂直起飞&#xff1a;小型无人机通常采用垂直起飞方式&#xff0c;利用螺旋桨产生的升力直接从地面升起。这种方式适用于空间有限或需要快速起飞的场景。 跑道起飞&#xff1a;大型无人机或需要较长起飞距离的无人机&#xff0c;可能会采用…

代码随想录day01--数组

两数之和 题目 地址&#xff1a;https://leetcode.cn/problems/two-sum/ 给定一个整数数组 nums 和一个目标值 target&#xff0c;请你在该数组中找出和为目标值的那 两个 整数&#xff0c;并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数…

Webpack前端工程化进阶系列(二) —— HMR热模块更新(图文+代码)

前言 之前更新过一篇Webpack文章:Webpack入门只看这一篇就够了(图文代码)&#xff0c;没想到颇受好评&#xff0c;很快就阅读量就破万了hhh&#xff0c;应读者私信的要求&#xff0c;决定继续更新Webpack进阶系列的文章&#xff01; 进入今天的主题 —— HMR 热模块替换(HotM…

Flink的双流join理解

如何保证Flink双流Join准确性和及时性、除了窗口join还存在哪些实现方式、究竟如何回答才能完全打动面试官呢。。你将在文中找到答案。 1 引子 1.1 数据库SQL中的JOIN 我们先来看看数据库SQL中的JOIN操作。如下所示的订单查询SQL&#xff0c;通过将订单表的id和订单详情表ord…

【MYSQL数据库相关知识介绍】

MySQL 在我们日常技术中是一个广泛使用的开源关系型数据库管理系统&#xff0c;所以作为测试同学&#xff0c;掌握mysql的相关知识是必不可少的技能之一&#xff0c;所以小编从软件测试的角色出发&#xff0c;来整理一些跟测试相关的知识&#xff0c;希望能够帮助到大家。 一、…