【项目】仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器(Http板块)

本文主要是介绍【项目】仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器(Http板块),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【项目】仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器(Http板块)

  • 一、思路图
  • 二、Util板块
    • 1、Splite板块(分词)
      • (1)代码
      • (2)测试及测试结果
        • i、第一种测试
        • ii、第二种测试
        • iii、第三种测试
    • 2、ReadFile板块(从文件读取)
      • (1)代码及设计思想
      • (2)测试及测试结果
    • 3、WriteFile板块(向文件写入)
      • (1)代码及设计思想
      • (2)测试及测试结果
    • 4、UrlEnode(Url编码)
      • (1)代码及设计思想
      • (2)测试及测试结果
        • i、测试结果1
        • ii、测试结果2
    • 5、UrlDecode(Url解码)
      • (1)代码及设计思路
      • (2)运行及运行结果
        • i、测试结果1
        • ii、测试结果2
    • 6、HttpDes(HTTP响应状态码和描述信息)
      • (1)代码及设计思想
      • (2)测试及测试结果
        • i、测试结果1
        • ii、测试结果2
    • 7、ExtMime(根据文件后缀名获取mime)
      • (1)代码及设计思路
      • (2)运行及运行结果
        • i、测试结果1
        • ii、测试结果2
    • 8、IsCatalogue(判断一个文件是否是目录)&&IsRegular(判断一个文件是否是普通文件)
      • (1)代码及设计思想
      • (2)测试及测试结果
    • 9、VaildPath(HTTP资源路径的有效性的判断)
      • (1)代码及设计思路
      • (2)测试及测试结果
  • 二、HttpRequest板块
    • 1、设计思维导图
    • 2、代码设计
  • 三、HttpResponse板块
    • 1、设计思维导图
    • 2、代码设计
  • 四、HttpContext板块
    • 1、设计思维导图
    • 2、代码部分
      • (1)接收命令行(RecvHttpLine)
      • (2)解析命令行(ParseHttpLine)
      • (3)接收头部(RecvHttpHead)
      • (4)解析头部(ParseHttpHead)
      • (5)接收正文(RecvHttpBody)
      • (6)重置(Reset)+返回三个私有成员函数+接收并解析HTTP请求(RecvHttpRequest)
      • (7)总体代码
  • 五、HttpServer板块
    • 1、设计思维导图
    • 2、代码部分


一、思路图

在这里插入图片描述

二、Util板块

1、Splite板块(分词)

(1)代码

我们写下面的代码主要需要考虑到三种分割方式:
1、abc 这种很简单的没有分隔符的单词,直接就是将offset到pos-offset的位置插入到array中即可
2、abc, 这种后面只有一个分隔符的单词,先将前面abc插入到array中,后面的直接不进入循环直接退出即可
3、abc,/,cdf 这种中间有很多个,的情况下,我们只需要一直continue即可。

// 分割字符串,将目标src中的字符串以sep分割出来放到array中
size_t Splite(const std::string &src, const std::string &sep, std::vector<std::string>* array)
{size_t offset = 0; // 偏移量// 如果字符串的范围是0-9,我们假如说是offset到了10的话就是越界了,所以不用等号while (offset < src.size()){size_t pos = src.find(sep, offset); // 让src从offset位置往后进行查找sep,并分割放到array中if (pos == std::string::npos){if (pos == src.size()) break; // 都到最后一个位置了,没必要往后走了// 没查到这sep的位置:abc 比如这个逗号是separray->push_back(src.substr(offset)); // 从offset到最后一个位置return array->size();}// 到这里就是找到sep分隔符的位置了if (pos == offset) // pos点位刚好是offset,这种情况一般是:abc,,,,,,,cde中间一连串逗号{offset = pos + sep.size();continue;}array->push_back(src.substr(offset, pos - offset)); // abc,cde 从offset位置到pos - offset位置offset = pos + sep.size();}return array->size();
}

(2)测试及测试结果

i、第一种测试

在这里插入图片描述

在这里插入图片描述

ii、第二种测试

在这里插入图片描述
在这里插入图片描述

iii、第三种测试

在这里插入图片描述
在这里插入图片描述

2、ReadFile板块(从文件读取)

(1)代码及设计思想

先打开文件用ifstream:
在这里插入图片描述
再将字符串偏移到文档的最后一个位置,再从当前位置计算出整个文档的大小,之后便将文档偏移到开头位置,将我们要存入的缓冲区中开辟这个文档的大小,再读入到文件中,别忘了关闭文件哦!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

        // 读取文件内容static bool ReadFile(const std::string& filename, std::string* buff){std::ifstream ifs(filename, std::ios::binary); // 先打开文件if (ifs.is_open() == false) // 打开失败了{printf("OPEN %s IS FAILED", filename.c_str());return false;}size_t fsize = 0; // 定义ifstream长度// 先偏移到最后一个位置ifs.seekg(0, ifs.end);// 计算fsize位置fsize = ifs.tellg();// 再重新偏移到开头ifs.seekg(0, ifs.beg);// 开辟文件大小buff->resize(fsize);// 读入到文件中ifs.read(&(*buff)[0], fsize);if (ifs.good() == false){// 读入失败了printf("READ %s IS FAILED", filename.c_str());ifs.close();return false;}ifs.close();return true;}

(2)测试及测试结果

在这里插入图片描述

在这里插入图片描述

3、WriteFile板块(向文件写入)

(1)代码及设计思想

打开文件并写入,就这么简单!

        // 向文件写入内容static bool WriteFile(const std::string &filename, const std::string &buff){std::ofstream ofs(filename, std::ios::binary | std::ios::trunc); // trunc 表示截断if (ofs.is_open() == false){printf("OPEN %s IS FAILED", filename.c_str());return false;}ofs.write(buff.c_str(), buff.size());if (ofs.good() == false){printf("WRITE %s IS FAILED", filename.c_str());ofs.close();return false;}ofs.close();return true;}

(2)测试及测试结果

在这里插入图片描述

在这里插入图片描述

4、UrlEnode(Url编码)

(1)代码及设计思想

其实思想很简单啦,主要还是特殊字符的处理,我们只要搞懂了我们下面注释的文字的特殊字符那么就完全没问题了。
在这里插入图片描述在这里插入图片描述

        // URL编码,避免URL中资源路径与查询字符串中的特殊字符与HTTP请求中特殊字符产生歧义// 编码格式:将特殊字符的ascii值,转换为两个16进制字符,前缀%C++->c%2B%2B// 不编码的特殊字符:RFC3986文档中规定的URL绝对不编码字符:.-_~以及字母和数字// 还有一个就是在不同的一些标准中的特殊处理:// W3C标准规定中规定param中的空格必须被编码为+,解码是+转空格// RFC2396中规定URI中的保留字符需要转换为%HH格式。static std::string UrlEncode(const std::string &url, bool convert_space_to_plus){std::string res;for (auto& c : url){if (c == '.' || c == '-' || c == '_' || c == '~'){res += c;continue;}if (c == ' ' && convert_space_to_plus == true){res += '+';continue;}if (isalnum(c)){res += c;continue;}// 剩下的只用转码为%HH格式了char temp[4] = {0};snprintf(temp, 4, "%%%02X", c);res += temp;}return res;}

(2)测试及测试结果

i、测试结果1

在这里插入图片描述

在这里插入图片描述

ii、测试结果2

在这里插入图片描述
在这里插入图片描述

5、UrlDecode(Url解码)

(1)代码及设计思路

这里就单纯的将编码完成后的字符进行解码,我们最注意的是在字母字符的时候需要+10。

        static char HTOI(char c){if (c >= '0' && c <= '9'){return c - '0';}if (c >= 'a' && c <= 'z'){return c - 'a' + 10;}if (c >= 'A' && c <= 'Z'){return c - 'A' + 10;}return -1;}// URL解码static std::string UrlDecode(const std::string& url, bool convert_plus_to_space){// 遇到了%,则将紧随其后的2个字符,转换为数字,第一个数字左移4位,然后加上第二个数字  + -> 2b  %2b->2 << 4 + 11std::string res;for (int i = 0; i < url.size(); i++){if (url[i] == '+' && convert_plus_to_space == true){res += ' ';continue;}if (url[i] == '%' && (i + 2) < url.size()){char v1 = HTOI(url[i + 1]);char v2 = HTOI(url[i + 2]);char v = v1 * 16 + v2;res += v;i += 2;continue;}res += url[i];}return res;}

(2)运行及运行结果

i、测试结果1

在这里插入图片描述
在这里插入图片描述

ii、测试结果2

在这里插入图片描述

在这里插入图片描述

6、HttpDes(HTTP响应状态码和描述信息)

(1)代码及设计思想

用一个unordered_map来存储,然后用find迭代器来找!

        // HTTP状态码和描述信息static std::string HttpDes(int statu){std::unordered_map<int, std::string> _statu_msg = {{100,  "Continue"},{101,  "Switching Protocol"},{102,  "Processing"},{103,  "Early Hints"},{200,  "OK"},{201,  "Created"},{202,  "Accepted"},{203,  "Non-Authoritative Information"},{204,  "No Content"},{205,  "Reset Content"},{206,  "Partial Content"},{207,  "Multi-Status"},{208,  "Already Reported"},{226,  "IM Used"},{300,  "Multiple Choice"},{301,  "Moved Permanently"},{302,  "Found"},{303,  "See Other"},{304,  "Not Modified"},{305,  "Use Proxy"},{306,  "unused"},{307,  "Temporary Redirect"},{308,  "Permanent Redirect"},{400,  "Bad Request"},{401,  "Unauthorized"},{402,  "Payment Required"},{403,  "Forbidden"},{404,  "Not Found"},{405,  "Method Not Allowed"},{406,  "Not Acceptable"},{407,  "Proxy Authentication Required"},{408,  "Request Timeout"},{409,  "Conflict"},{410,  "Gone"},{411,  "Length Required"},{412,  "Precondition Failed"},{413,  "Payload Too Large"},{414,  "URI Too Long"},{415,  "Unsupported Media Type"},{416,  "Range Not Satisfiable"},{417,  "Expectation Failed"},{418,  "I'm a teapot"},{421,  "Misdirected Request"},{422,  "Unprocessable Entity"},{423,  "Locked"},{424,  "Failed Dependency"},{425,  "Too Early"},{426,  "Upgrade Required"},{428,  "Precondition Required"},{429,  "Too Many Requests"},{431,  "Request Header Fields Too Large"},{451,  "Unavailable For Legal Reasons"},{501,  "Not Implemented"},{502,  "Bad Gateway"},{503,  "Service Unavailable"},{504,  "Gateway Timeout"},{505,  "HTTP Version Not Supported"},{506,  "Variant Also Negotiates"},{507,  "Insufficient Storage"},{508,  "Loop Detected"},{510,  "Not Extended"},{511,  "Network Authentication Required"}};auto it = _statu_msg.find(statu);if (it != _statu_msg.end()){// 找到啦return it->second;}return "UnKonw";}

(2)测试及测试结果

i、测试结果1

在这里插入图片描述

在这里插入图片描述

ii、测试结果2

在这里插入图片描述

在这里插入图片描述

7、ExtMime(根据文件后缀名获取mime)

(1)代码及设计思路

        // 根据文件后缀名获取mimestatic std::string ExtMime(const std::string &filename){std::unordered_map<std::string, std::string> _mime_msg = {{".aac",        "audio/aac"},{".abw",        "application/x-abiword"},{".arc",        "application/x-freearc"},{".avi",        "video/x-msvideo"},{".azw",        "application/vnd.amazon.ebook"},{".bin",        "application/octet-stream"},{".bmp",        "image/bmp"},{".bz",         "application/x-bzip"},{".bz2",        "application/x-bzip2"},{".csh",        "application/x-csh"},{".css",        "text/css"},{".csv",        "text/csv"},{".doc",        "application/msword"},{".docx",       "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},{".eot",        "application/vnd.ms-fontobject"},{".epub",       "application/epub+zip"},{".gif",        "image/gif"},{".htm",        "text/html"},{".html",       "text/html"},{".ico",        "image/vnd.microsoft.icon"},{".ics",        "text/calendar"},{".jar",        "application/java-archive"},{".jpeg",       "image/jpeg"},{".jpg",        "image/jpeg"},{".js",         "text/javascript"},{".json",       "application/json"},{".jsonld",     "application/ld+json"},{".mid",        "audio/midi"},{".midi",       "audio/x-midi"},{".mjs",        "text/javascript"},{".mp3",        "audio/mpeg"},{".mpeg",       "video/mpeg"},{".mpkg",       "application/vnd.apple.installer+xml"},{".odp",        "application/vnd.oasis.opendocument.presentation"},{".ods",        "application/vnd.oasis.opendocument.spreadsheet"},{".odt",        "application/vnd.oasis.opendocument.text"},{".oga",        "audio/ogg"},{".ogv",        "video/ogg"},{".ogx",        "application/ogg"},{".otf",        "font/otf"},{".png",        "image/png"},{".pdf",        "application/pdf"},{".ppt",        "application/vnd.ms-powerpoint"},{".pptx",       "application/vnd.openxmlformats-officedocument.presentationml.presentation"},{".rar",        "application/x-rar-compressed"},{".rtf",        "application/rtf"},{".sh",         "application/x-sh"},{".svg",        "image/svg+xml"},{".swf",        "application/x-shockwave-flash"},{".tar",        "application/x-tar"},{".tif",        "image/tiff"},{".tiff",       "image/tiff"},{".ttf",        "font/ttf"},{".txt",        "text/plain"},{".vsd",        "application/vnd.visio"},{".wav",        "audio/wav"},{".weba",       "audio/webm"},{".webm",       "video/webm"},{".webp",       "image/webp"},{".woff",       "font/woff"},{".woff2",      "font/woff2"},{".xhtml",      "application/xhtml+xml"},{".xls",        "application/vnd.ms-excel"},{".xlsx",       "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},{".xml",        "application/xml"},{".xul",        "application/vnd.mozilla.xul+xml"},{".zip",        "application/zip"},{".3gp",        "video/3gpp"},{".3g2",        "video/3gpp2"},{".7z",         "application/x-7z-compressed"}};// a.b.txt 先获取文件扩展名size_t pos = filename.find_last_of('.'); // 从后往前找if (pos == std::string::npos){return "application/octet-stream"; // 二进制文件流}// 再根据扩展名选取mimestd::string ext = filename.substr(pos); // 先将这个pos位置截取到的文件名存起来auto it = _mime_msg.find(ext);if (it == _mime_msg.end()){return "application/octet-stream";}return it->second;}

(2)运行及运行结果

i、测试结果1

在这里插入图片描述

在这里插入图片描述

ii、测试结果2

在这里插入图片描述

在这里插入图片描述

8、IsCatalogue(判断一个文件是否是目录)&&IsRegular(判断一个文件是否是普通文件)

(1)代码及设计思想

在这里插入图片描述
在这里插入图片描述

        // 判断一个文件是否是目录static bool IsCatalogue(const std::string& filename){struct stat st;int ret = stat(filename, &st);if (ret < 0){return false;}return S_ISDIR(st.st_mode);}// 判断一个文件是否是普通文件static bool IsRegular(const std::string& filename){struct stat st;int ret = stat(filename, &st);if (ret < 0){return false;}return S_ISREG(st.st_mode);}

(2)测试及测试结果

在这里插入图片描述

在这里插入图片描述

9、VaildPath(HTTP资源路径的有效性的判断)

(1)代码及设计思路

       // HTTP资源路径的有效性的判断// /index.html  --- 前边的/叫做相对根目录  映射的是某个服务器上的子目录// 想表达的意思就是,客户端只能请求相对根目录中的资源,其他地方的资源都不予理会// /../login, 这个路径中的..会让路径的查找跑到相对根目录之外,这是不合理的,不安全的static bool VaildPath(){// 思想:按照/进行路径分割,根据有多少子目录,计算目录深度,有多少层,深度不能小于0std::vector<std::string> subdir;Split(path, "/", &subdir);int level = 0;for (auto &dir : subdir) {if (dir == "..") {level--;if (level < 0) {return false;}continue;}level++;}return true;}

(2)测试及测试结果

在这里插入图片描述

在这里插入图片描述

二、HttpRequest板块

1、设计思维导图

在这里插入图片描述

2、代码设计

这里我们先用手册了解一下我们的smatch和链接正文和链接:

smatch只能用swap来进行清空操作,其他字段均可用clear()来进行清空在这里插入图片描述

连接正文的格式为:格式为:Content-Length:1234\r\n

长连接和短链接判断:没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接
在这里插入图片描述
php的Http使用手册网站

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 smatches;_matches.swap(smatches);_headers.clear();_params.clear();}// 插入头部字段void SetHeader(const std::string& key, const std::string& val) // key-val键值对{_headers.insert(std::make_pair(key, val));}// 判断是否存在指定头部文件bool IsHeader(const std::string& key){auto it = _headers.find(key);if (it == _headers.end()){return false;}return true;}// 获取指定头部字段的值std::string GetHeader(std::string& key){auto it = _headers.find(key);if (it == _headers.end()){return "";}return it->second;}// 插入查询字符串void SetParam(const std::string& key, const std::string& val) // key-val键值对{_params.insert(std::make_pair(key, val));}// 判断是否存在指定头部文件bool IsParam(const std::string& key){auto it = _params.find(key);if (it == _params.end()){return false;}return true;}// 获取指定头部字段的值std::string GetHeader(std::string& key){auto it = _params.find(key);if (it == _params.end()){return "";}return it->second;}// 获取正文长度size_t ContentLength(){// 格式为:Content-Length:1234\r\nbool ret = IsHeader("Content-Length");if (ret == false){return 0;}std::string cotent_length = GetHeader("Content-Length");return std::stol(cotent_length);}bool Close(){// 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接if (IsHeader("Connection") == true && GetHeader("Connection") == "keep-alive"){return false;}}
};

三、HttpResponse板块

1、设计思维导图

在这里插入图片描述

2、代码设计

class HttpResponse
{public:int _statu; // 状态bool _redirectflag; // 重定向标志(判断是否要进行重定向)std::string _body; // 正文部分std::string _redirecturl; // 重定向urlstd::unordered_map<std::string, std::string> _headers; // 头部public:HttpResponse(): _redirectflag(false), _statu(200){}HttpResponse(int statu): _redirectflag(false), _statu(statu){}// 重置void Reset(){_statu = 200;_redirectflag = false;_body.clear();_redirecturl.clear();_headers.clear();}// 设置头部字段void SetHeader(const std::string& key, const std::string& val){_headers.insert(std::make_pair(key, val));}// 判断是否存在指定的头部文件bool IsHeader(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 SetRedirect(const std::string &url, int statu = 302) {_statu = statu;_redirectflag = true;_redirecturl = url;}bool Clear(){// 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接if (IsHeader("Connection") == true && GetHeader("Connection") == "keep-alive"){return false;}return true;}
};

四、HttpContext板块

1、设计思维导图

在这里插入图片描述

2、代码部分

(1)接收命令行(RecvHttpLine)

        // 接收HTTP行bool RecvHttpLine(Buffer* buff){if (_recv_statu != RECV_HTTP_LINE) return false;// 1、获取一行带有末尾的数据std::string line = buff->GetLineAndPop();// 2、我们需要考虑到缓冲区中的数据不足一行,则需要判断缓冲区可读的数据的多少,假如说是数据多的但没有进行读取的话,那么就是错误了if (line.size() == 0){if (buff->ReadAbleSize() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URL TOO LONGreturn false;}// 缓冲区数据不多不到一行,静静等待其他信息的到来return true;}if (line.size() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URL TOO LONGreturn false;}bool ret = ParseHttpLine(line); // 解析命令行if (ret == false){return false;}//首行处理完毕,进入头部获取阶段_recv_statu = RECV_HTTP_HEAD;return true;}

(2)解析命令行(ParseHttpLine)

        // 解析命令行bool ParseHttpLine(const std::string& line){std::smatch matches;// 请求的方法有右边五种:GET|HEAD|POST|PUT|DELETE  std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");bool ret = std::regex_match(str, matches, e);if (ret == false){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}// 0:GET /jiangrenhai/login?usr=jrh&pass=123456 HTTP/1.1// 1:GET// 2:/jiangrenhai/login// 3:usr=jrh&pass=123456// 4:HTTP/1.1// 请求方法的获取_request._method = matches[1];std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);// 资源路径的获取,需要进行URL解码操作,但是不需要+转空格_request._path = Util::UrlDecode(matches[2], false);// 资源版本的获取_request._version = matches[4];// 查询字符串的获取与处理std::vector<std::string> query_string_array;std::string query_string = matches[3];// 查询字符串的格式,usr=jrh&pass=123456这个以&为间隔号Util::Splite(query_string, "&", &query_string_array);// 我们针对每个等于号进行分割,分割出不同的字串for (auto& str : query_string_array){size_t pos = str.find("=");if (pos == std::string::npos){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}std::string key = Util::UrlDecode(str.substr(0, pos), true); // 需要+转空格std::string value = Util::UrlDecode(str.substr(pos + 1), true);_request.SetParam(key, value);}return true;}

(3)接收头部(RecvHttpHead)

        // 接收头部bool RecvHttpHead(Buffer* buff){if (_recv_statu != RECV_HTTP_HEAD) return false;while(1){// 1、获取一行带有末尾的数据std::string line = buff->GetLineAndPop();// 2、我们需要考虑到缓冲区中的数据不足一行,则需要判断缓冲区可读的数据的多少,假如说是数据多的但没有进行读取的话,那么就是错误了if (line.size() == 0){if (buff->ReadAbleSize() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URL TOO LONGreturn false;}// 缓冲区数据不多不到一行,静静等待其他信息的到来return true;}if (line.size() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URL TOO LONGreturn false;}if (line == "\n" || line == "\r\n"){break; // 都到首行末尾了,直接跳出循环了}bool ret = ParseHttpHead(line); // 解析命令行if (ret == false){return false;}}//首行处理完毕,进入头部获取阶段_recv_statu = RECV_HTTP_BODY;return true;}

(4)解析头部(ParseHttpHead)

        // 解析头部bool ParseHttpHead(std::string& line){//key: val\r\nkey: val\r\n....if (line.back() == '\n') line.pop_back(); // 末尾是换行则去掉换行字符if (line.back() == '\r') line.pop_back(); // 末尾是回车则去掉回车字符size_t pos = line.find(": ");if (pos == std::string::npos){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}std::string key = line.substr(0, pos); // 需要+转空格std::string value = line.substr(pos + 1);_request.SetHeader(key, value);return true;}

(5)接收正文(RecvHttpBody)

        // 接收正文bool RecvHttpBody(Buffer* buff){if (_recv_statu != RECV_HTTP_BODY) return false;// 1.获取正文长度size_t content_length = _request.ContentLength();if (content_length == 0){_recv_statu = RECV_HTTP_OVER;return true;}// 2、当前已经接收了多少正文 其实就是_request._body中放了多少数据size_t real_length = content_length - _request._body.size();// 3.接收正文放到body中,但是也要考虑当前缓冲区中的数据,是否是全部的正文//   3.1 缓冲区中数据,包含了当前请求的所有正文,则取出所需的数据if (buff->ReadAbleSize() >= real_length){_request._body.append(buff->ReadPos(), real_length);buff->ReadOffset(real_length);_recv_statu = RECV_HTTP_OVER;return true;}//   3.2 缓冲区中数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来_request._body.append(buff->ReadPos(), buff->ReadAbleSize());buff->ReadOffset(buff->ReadAbleSize());return true;}

(6)重置(Reset)+返回三个私有成员函数+接收并解析HTTP请求(RecvHttpRequest)

        // 重置void Reset(){_resp_statu = 200;_recv_statu = RECV_HTTP_LINE;_request.ReSet();}// 返回响应状态码int RespStatu() { return _resp_statu; }// 返回接收及解析的状态HttpRecvStatu RecvStatu() { return _recv_statu; }// 返回已经解析到的请求信息HttpRequest& Request() { return _request; }// 接收并解析http请求void RecvHttpRequest(Buffer* buff){// 这里不同break是因为需要都进行操作,从line往头部往正文方向都要进行操作switch (_recv_statu){case RECV_HTTP_LINE: RecvHttpLine(buff);case RECV_HTTP_HEAD: RecvHttpHead(buff);case RECV_HTTP_BODY: RecvHttpBody(buff);}return;}

(7)总体代码

typedef enum 
{RECV_HTTP_ERROR,RECV_HTTP_LINE,RECV_HTTP_HEAD,RECV_HTTP_BODY,RECV_HTTP_OVER
}HttpRecvStatu;
#define MAX_LINE 8192
class HttpContext
{private:int _resp_statu; // 响应状态码HttpRecvStatu _recv_statu; // 当前接收及解析的阶段状态HttpRequest _request; // 已经解析得到的请求信息private:// 解析命令行bool ParseHttpLine(const std::string& line){std::smatch matches;// 请求的方法有右边五种:GET|HEAD|POST|PUT|DELETE  std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");bool ret = std::regex_match(line, matches, e);if (ret == false){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}// 0:GET /jiangrenhai/login?usr=jrh&pass=123456 HTTP/1.1// 1:GET// 2:/jiangrenhai/login// 3:usr=jrh&pass=123456// 4:HTTP/1.1// 请求方法的获取_request._method = matches[1];std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);// 资源路径的获取,需要进行URL解码操作,但是不需要+转空格_request._path = Util::UrlDecode(matches[2], false);// 资源版本的获取_request._version = matches[4];// 查询字符串的获取与处理std::vector<std::string> query_string_array;std::string query_string = matches[3];// 查询字符串的格式,usr=jrh&pass=123456这个以&为间隔号Util::Splite(query_string, "&", &query_string_array);// 我们针对每个等于号进行分割,分割出不同的字串for (auto& str : query_string_array){size_t pos = str.find("=");if (pos == std::string::npos){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}std::string key = Util::UrlDecode(str.substr(0, pos), true); // 需要+转空格std::string value = Util::UrlDecode(str.substr(pos + 1), true);_request.SetParam(key, value);}return true;}// 接收HTTP行bool RecvHttpLine(Buffer* buff){if (_recv_statu != RECV_HTTP_LINE) return false;// 1、获取一行带有末尾的数据std::string line = buff->GetLineAndPop();// 2、我们需要考虑到缓冲区中的数据不足一行,则需要判断缓冲区可读的数据的多少,假如说是数据多的但没有进行读取的话,那么就是错误了if (line.size() == 0){if (buff->ReadAbleSize() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URL TOO LONGreturn false;}// 缓冲区数据不多不到一行,静静等待其他信息的到来return true;}if (line.size() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URL TOO LONGreturn false;}bool ret = ParseHttpLine(line); // 解析命令行if (ret == false){return false;}//首行处理完毕,进入头部获取阶段_recv_statu = RECV_HTTP_HEAD;return true;}// 接收头部bool RecvHttpHead(Buffer* buff){if (_recv_statu != RECV_HTTP_HEAD) return false;while(1){// 1、获取一行带有末尾的数据std::string line = buff->GetLineAndPop();// 2、我们需要考虑到缓冲区中的数据不足一行,则需要判断缓冲区可读的数据的多少,假如说是数据多的但没有进行读取的话,那么就是错误了if (line.size() == 0){if (buff->ReadAbleSize() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URL TOO LONGreturn false;}// 缓冲区数据不多不到一行,静静等待其他信息的到来return true;}if (line.size() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URL TOO LONGreturn false;}if (line == "\n" || line == "\r\n"){break; // 都到首行末尾了,直接跳出循环了}bool ret = ParseHttpHead(line); // 解析命令行if (ret == false){return false;}}//首行处理完毕,进入头部获取阶段_recv_statu = RECV_HTTP_BODY;return true;}// 解析头部bool ParseHttpHead(std::string& line){//key: val\r\nkey: val\r\n....if (line.back() == '\n') line.pop_back(); // 末尾是换行则去掉换行字符if (line.back() == '\r') line.pop_back(); // 末尾是回车则去掉回车字符size_t pos = line.find(": ");if (pos == std::string::npos){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}std::string key = line.substr(0, pos); // 需要+转空格std::string value = line.substr(pos + 1);_request.SetHeader(key, value);return true;}// 接收正文bool RecvHttpBody(Buffer* buff){if (_recv_statu != RECV_HTTP_BODY) return false;// 1.获取正文长度size_t content_length = _request.ContentLength();if (content_length == 0){_recv_statu = RECV_HTTP_OVER;return true;}// 2、当前已经接收了多少正文 其实就是_request._body中放了多少数据size_t real_length = content_length - _request._body.size();// 3.接收正文放到body中,但是也要考虑当前缓冲区中的数据,是否是全部的正文//   3.1 缓冲区中数据,包含了当前请求的所有正文,则取出所需的数据if (buff->ReadAbleSize() >= real_length){_request._body.append(buff->ReadPos(), real_length);buff->ReadOffset(real_length);_recv_statu = RECV_HTTP_OVER;return true;}//   3.2 缓冲区中数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来_request._body.append(buff->ReadPos(), buff->ReadAbleSize());buff->ReadOffset(buff->ReadAbleSize());return true;}public:// 构造函数HttpContext(): _resp_statu(200), _recv_statu(RECV_HTTP_LINE){}// 重置void Reset(){_resp_statu = 200;_recv_statu = RECV_HTTP_LINE;_request.ReSet();}// 返回响应状态码int RespStatu() { return _resp_statu; }// 返回接收及解析的状态HttpRecvStatu RecvStatu() { return _recv_statu; }// 返回已经解析到的请求信息HttpRequest& Request() { return _request; }// 接收并解析http请求void RecvHttpRequest(Buffer* buff){// 这里不同break是因为需要都进行操作,从line往头部往正文方向都要进行操作switch (_recv_statu){case RECV_HTTP_LINE: RecvHttpLine(buff);case RECV_HTTP_HEAD: RecvHttpHead(buff);case RECV_HTTP_BODY: RecvHttpBody(buff);}return;}
};

五、HttpServer板块

1、设计思维导图

在这里插入图片描述
在这里插入图片描述

2、代码部分

具体细节直接上代码。

class HttpServer
{private:using Handler = std::function<void(const HttpRequest&, HttpResponse*)>;using Handlers = std::vector<std::pair<std::regex, Handler>>;Handlers _get_route; // getHandlers _post_route; // postHandlers _put_route; // putHandlers _delete_route; // deletestd::string _basedir; // 静态资源根目录--/home/jrh/wwwrootTcpServer _server;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::HttpDes(rsp->_statu);body += "</h1>";body += "</body>";body += "</html>";// 2. 将页面数据,当作响应正文,放入rsp中rsp->SetContent(body, "text/html");}// 将HttpResponse中的要素按照http协议格式进行组织并发送void WriteResponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp){// 1. 先完善头部字段if (req.Close() == true) {rsp.SetHeader("Connection", "close");}else {rsp.SetHeader("Connection", "keep-alive");}if (rsp._body.empty() == false && rsp.IsHeader("Content-Length") == false) {rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));}if (rsp._body.empty() == false && rsp.IsHeader("Content-Type") == false) {rsp.SetHeader("Content-Type", "application/octet-stream");}if (rsp._redirectflag == true) // 重定向{rsp.SetHeader("Location", rsp._redirecturl);}// 2. 将rsp中的要素,按照http协议格式进行组织std::stringstream rsp_str;// 协议版本+状态码+状态码描述+\r\nrsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::HttpDes(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::VaildPath(req._path) == false) {return false;}// 4. 请求的资源必须存在,且是一个普通文件//    有一种请求比较特殊 -- 目录:/, /image/, 这种情况给后边默认追加一个 index.html// index.html    /image/a.png// 前缀的相对根目录,也就是将请求路径转换为实际存在的路径  /image/a.png  ->   ./wwwroot/image/a.pngstd::string req_path = _basedir + req._path;//为了避免直接修改请求的资源路径,因此定义一个临时对象if (req._path.back() == '/')  {req_path += "index.html";}if (Util::IsRegular(req_path) == false) {return false;}return true;}// 静态资源的请求处理void FileHandler(const HttpRequest &req, HttpResponse *rsp){std::string req_path = _basedir + req._path;if (req._path.back() == '/')  {req_path += "index.html";}bool ret = Util::ReadFile(req_path, &rsp->_body);if (ret == false) {return;}std::string mime = Util::ExtMime(req_path);rsp->SetHeader("Content-Type", mime);return;}// 功能性请求的分类处理void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers){// 在对应请求方法的路由表中,查找是否含有对应资源请求的处理函数,有则调用,没有则返回404// 思想:路由表存储的时键值对 -- 正则表达式 & 处理函数// 使用正则表达式,对请求的资源路径进行正则匹配,匹配成功就使用对应函数进行处理//  /numbers/(\d+)       /numbers/12345for (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;}// 路线void Route(HttpRequest &req, HttpResponse *rsp){// 对请求进行分辨,是一个静态资源请求,还是一个功能性请求//   静态资源请求,则进行静态资源的处理//   功能性请求,则需要通过几个请求路由表来确定是否有处理函数//   既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405if (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);}rsp->_statu = 405; // Method Not Allowedreturn;}// 设置上下文void OnConnected(const PtrConnection &conn){conn->SetContext(HttpContext());DEBLOG("NEW CONNECTION %p", conn.get());}// 缓冲区数据解析和处理void OnMessage(const PtrConnection &conn, Buffer *buffer){while (buffer->ReadAbleSize() > 0){// 1. 获取上下文HttpContext *context = conn->GetContext()->get<HttpContext>();// 2、通过上下文对缓冲区数据进行解析,得到HttpRequest对象context->RecvHttpRequest(buffer);HttpRequest &req = context->Request(); // 返回响应HttpResponse rsp(context->RespStatu()); // 返回响应码//  (1) 如果缓冲区的数据解析出错,就直接回复出错响应//  (2) 如果解析正常,且请求已经获取完毕,才开始去进行处理if (context->RespStatu() >= 400){// 出错啦,进行错误响应,关闭连接ErrorHandler(req, &rsp); // 填充一个错误显示页面数据到rsp中WriteResponse(conn, req, rsp); // 组织响应发送给客户端context->Reset(); // 重置buffer->ReadOffset(buffer->ReadAbleSize()); // 出错了就把缓冲区数据清空conn->Shutdown(); // 关闭连接return; // 直接返回}// 3. 请求路由 + 业务处理Route(req, &rsp);// 4. 对HttpResponse进行组织发送WriteResponse(conn, req, rsp);// 5. 重置上下文context->Reset();// 6. 根据长短连接判断是否关闭连接或者继续处理if (rsp.Clear() == true) conn->Shutdown(); // 短链接则直接关闭}}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::IsCatalogue(path) == true);_basedir = path;}// Get方法void Get(const std::string& pattern, Handler& handler){_get_route.push_back(std::make_pair(std::regex(pattern), handler));}// Post方法void Post(const std::string& pattern, Handler& handler){_post_route.push_back(std::make_pair(std::regex(pattern), handler));}// Put方法void Put(const std::string& pattern, Handler& handler){_put_route.push_back(std::make_pair(std::regex(pattern), handler));}// Delete方法void Delete(const std::string& pattern, Handler& handler){_delete_route.push_back(std::make_pair(std::regex(pattern), handler));}// 设置线程长度void SetThreadCount(int count){_server.SetThreadCount(count);}// 监听void Listen(){_server.Start();}
};

这篇关于【项目】仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器(Http板块)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/942570

相关文章

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo