0


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

在这里插入图片描述

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


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

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


仿mudou的高并发服务器

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
// 公共方法类classUtil{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);// 没有找到sepif(pos == std::string::npos){// 直接将offset后的字符串当成子串
                sub.push_back(src.substr(offset));break;}// 找到了sepelse{
                size_t len = pos - offset;if(len ==0){
                    offset++;continue;}

                sub.push_back(src.substr(offset, len));
                offset += len;// 偏移量向后移动}}return sub.size();}staticboolReadFile(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());returnfalse;}// 获取文件大小
        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();returntrue;}staticboolWriteFile(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());returnfalse;}// 进行写入
        ofs.write(&buf[0], buf.size());if(ofs.good()==false){LOG(ERROR,"Write %s Failed!\n", filename.c_str());returnfalse;}
        ofs.close();returntrue;}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解码staticcharHexToC(char c){if(c >='0'&& c <='9'){return c -'0';}elseif(c >='a'&& c <='z'){return c -'a'+10;}elseif(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;}elseif(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;}// 检查是否是合法路径staticboolIsLegPath(const std::string &path){// 采用计数法int level =0;
        std::vector<std::string> subdir;int ret =SplitStr(path,"..", subdir);if(ret <0)returnfalse;for(auto&s : subdir){if(s ==".."){
                level--;if(level <0)returnfalse;continue;}else
                level++;}returntrue;}staticboolIsDir(const std::string &dir){structstat st;int n =::stat(dir.c_str(),&st);if(n <0)returnfalse;returnS_ISDIR(st.st_mode);}staticboolIsRegular(const std::string &dir){structstat st;int n =::stat(dir.c_str(),&st);if(n <0)returnfalse;returnS_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. 是否为长连接
classHttpRequest{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:// 重置请求voidReset(){
        _method.clear();
        _path.clear();
        _version.clear();
        _body.clear();
        std::smatch tmp;
        _matches.swap(tmp);
        _headers.clear();
        _params.clear();}// 插入头部字段voidSetHeader(const std::string &key,const std::string &val){
        _headers.insert(std::make_pair(key, val));}// 判断是否有该头部字段boolHasHeader(const std::string &key)const{auto it = _headers.find(key);if(it == _headers.end()){returnfalse;}returntrue;}// 获取头部字段
    std::string GetHeader(const std::string &key)const{auto it = _headers.find(key);if(it == _headers.end()){return"";}return it->second;}// 插入查询字符串voidSetParam(const std::string &key,const std::string &val){
        _params.insert(std::make_pair(key, val));}// 判断是否有该查询字符串boolHasParam(const std::string &key){auto it = _params.find(key);if(it == _params.end()){returnfalse;}returntrue;}// 获取查询字符串
    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"));}return0;}boolClose()const{// 没有Connection字段或者Connection字段是close 就是短连接if(HasHeader("Connection")==true&&GetHeader("Connection")=="close"){returntrue;}returnfalse;}};

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

3.2 HTTP应答

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

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

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

  1. 设置头部字段
  2. 获取头部字段
  3. 设置正文
  4. 设置应答状态
  5. 是否是长连接
classHttpResponse{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){}// 重置响应voidReset(){}// 插入头部字段voidSetHeader(const std::string &key,const std::string &val){
        _headers.insert(std::make_pair(key, val));}// 判断是否有该头部字段boolHasHeader(const std::string &key){auto it = _headers.find(key);if(it == _headers.end()){returnfalse;}returntrue;}// 获取头部字段
    std::string GetHeader(const std::string &key){auto it = _headers.find(key);if(it == _headers.end()){return"";}return it->second;}voidSetContent(const std::string &body,const std::string &type ="text/html"){
        _body = body;SetHeader("Content-Type", type);}voidSetRediret(const std::string &url,int statu =302){
        _statu = statu;
        _rediect_flag =true;
        _rediect_url = url;}boolClose(){// 没有Connection字段或者Connection字段是close 就是短连接if(HasHeader("Connection")==true&&GetHeader("Connection")=="close"){returntrue;}returnfalse;}};

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

4 上下文解析模块

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

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

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

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

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

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

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

typedefenum{
    RECV_HTTP_ERROR,
    RECV_HTTP_LINE,
    RECV_HTTP_HEAD,
    RECV_HTTP_BODY,
    RECV_HTTP_OVER
} HttpRecvStatu;staticconstint MAX_SIZE =8192;classHttpContext{private:int _resp_statu;// 响应状态码
    HttpRequest _request;// 请求信息
    HttpRecvStatu _recv_statu;// 解析状态private:boolParseHttpLine(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!returnfalse;}// 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 Resquestreturnfalse;}// 找到了 ‘=’
            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);}returntrue;}// 解析请求行boolRecvHttpLine(Buffer *buf){if(_recv_statu != RECV_HTTP_LINE)returnfalse;// 获取一行数据 带有\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;returnfalse;}// 反之不处理returntrue;}// 一行的数据过长if(line.size()> MAX_SIZE){
            _resp_statu =414;// URL TOO LONG
            _recv_statu = RECV_HTTP_ERROR;returnfalse;}// 进行解析bool ret =ParseHttpLine(line);if(ret ==false)returnfalse;// 请求行解析完毕 开始解析请求报头
        _recv_statu = RECV_HTTP_HEAD;returntrue;}// 解析报头boolRecvHttpHead(Buffer *buf){if(_recv_statu != RECV_HTTP_HEAD)returnfalse;// 解析请求报头直到遇到空行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;returnfalse;}// 反之不处理 等待新数据到来// LOG(ERROR, "wait new buffer\n");returntrue;}// 一行的数据过长if(line.size()> MAX_SIZE){// LOG(ERROR, "line too long\n");
                _resp_statu =414;// URL TOO LONG
                _recv_statu = RECV_HTTP_ERROR;returnfalse;}if(line =="\n"|| line =="\r\n"){// LOG(ERROR, "line is empty\n");break;}// LOG(INFO, "line正常 进行解析处理");//  去除换行 \r \nif(line.back()=='\n')
                line.pop_back();if(line.back()=='\r')
                line.pop_back();// 进行解析bool ret =ParseHttpHead(line);if(ret ==false)returnfalse;}// 头部解析完成 继续解析正文
        _recv_statu = RECV_HTTP_BODY;returntrue;}boolParseHttpHead(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 Resquestreturnfalse;}
        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);returntrue;}boolRecvHttpBody(Buffer *buf){if(_recv_statu != RECV_HTTP_BODY)returnfalse;// 获取正文长度
        size_t len = _request.ContentLength();// 没有正文 直接读取完毕if(len ==0){
            _recv_statu = RECV_HTTP_OVER;returntrue;}// 当前已经接受了多少数据 _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;returntrue;}// 缓冲区无法满足正文
        _request._body.append(buf->ReadPos(), buf->ReadAbleSize());
        buf->MoveReadOffset(buf->ReadAbleSize());returntrue;}public:HttpContext():_resp_statu(200),_recv_statu(RECV_HTTP_LINE){}intRespStatu(){return _resp_statu;}
    HttpRequest &Request(){return _request;}
    HttpRecvStatu RecvStatu(){return _recv_statu;}// 重置上下文voidReset(){
        _resp_statu =200;
        _recv_statu = RECV_HTTP_LINE;
        _request.Reset();}voidRecvhttpRequest(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服务器
classHttpServer{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:// 设置空白上下文voidOnConnect(const PtrConn &conn){
        conn->SetContext(HttpContext());LOG(INFO,"NEW CONNECTION :%p\n",this);}voidErrorHandler(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");}// 缓冲区数据解析+处理voidOnMessage(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;}boolRoute(HttpRequest &req, HttpResponse *rsp){// 判断是否是静态资源处理if(IsFileHandler(req)==true)returnFileHandler(req, rsp);// 判断是否实际功能性请求if(req._method =="GET"|| req._method =="HEAD")returnDispatcher(req, rsp, _get_route);elseif(req._method =="POST")returnDispatcher(req, rsp, _post_route);elseif(req._method =="PUT")returnDispatcher(req, rsp, _put_route);elseif(req._method =="DELETE")returnDispatcher(req, rsp, _delete_route);// 不是静态请求也不是功能性请求else{
            rsp->_statu =405;// Method Not Allowedreturnfalse;}}// 判断是否是静态资源boolIsFileHandler(HttpRequest &req){// 首先_basedir必须存在if(_basedir.empty()==true)returnfalse;// 请求方法必须是 GET / HEADif(req._method !="GET"&& req._method !="HEAD")returnfalse;// 请求路径必须是合法路径if(Util::IsLegPath(req._path)==false)returnfalse;// 请求的资源必须存在
        std::string req_path = _basedir + req._path;// 如果直接请求的网络根目录要补全一个初始页面if(req_path.back()=='/')
            req_path +="index.html";if(Util::IsRegular(req_path)==false)returnfalse;// req请求路径的真正路径
        req._path = req_path;returntrue;}// 静态资源的请求处理boolFileHandler(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");returnfalse;}// 获取文件类型mime
        std::string mime =Util::ExtMime(req._path);LOG(DEBUG,"Content-Type:%s\n", mime.c_str());// 添加到应答报头
        rsp->SetHeader("Content-Type", mime);returntrue;}// 功能性请求的任务分发boolDispatcher(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);returntrue;}// 没有找到目标LOG(DEBUG,"404 Not Found\n");
        rsp->_statu =404;// 设置为Not Foundreturnfalse;}// 将HttpReaponse应答按照http应答格式进行组织发送voidWriteResponse(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路由表voidGET(const std::string &pattern,const Handler &func){ _get_route.push_back(std::make_pair(std::regex(pattern), func));}voidPOST(const std::string &pattern,const Handler &func){ _post_route.push_back(std::make_pair(std::regex(pattern), func));}voidPUT(const std::string &pattern,const Handler &func){ _put_route.push_back(std::make_pair(std::regex(pattern), func));}voidDELETE(const std::string &pattern,const Handler &func){ _delete_route.push_back(std::make_pair(std::regex(pattern), func));}voidSetBaseDir(const std::string &dir){assert(Util::IsDir(dir)==true);
        _basedir = dir;}// 设置服务器线程数量voidSetThreadSize(size_t size){
        _server.SetThreadSize(size);}// 启动服务器voidStart(){
        _server.Start();}};
标签: 服务器 http 运维

本文转载自: https://blog.csdn.net/JLX_1/article/details/143987400
版权归原作者 叫我龙翔 所有, 如有侵权,请联系我们删除。

“【项目日记】仿mudou的高并发服务器 --- 实现HTTP服务器”的评论:

还没有评论