> 技术文档 > 【仿Mudou库one thread per loop式并发服务器实现】HTTP协议模块实现

【仿Mudou库one thread per loop式并发服务器实现】HTTP协议模块实现


HTTP协议模块实现

  • 1. Util模块
  • 2. HttpRequest模块
  • 3. HttpResponse模块
  • 4. HttpContext模块
  • 5. HttpServer模块

1. Util模块

这个模块是一个工具模块,主要提供HTTP协议模块所用到的一些工具函数,比如url编解码,文件读写…等。

【仿Mudou库one thread per loop式并发服务器实现】HTTP协议模块实现

#include \"server.hpp\"#include #include #include static std::unordered_map<int,std::string> _status_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\"}};static std::unordered_map<std::string,std::string> _mine_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\"},};class Until{ public: //字符串分割函数,将src字符串按照sep字符进⾏分割,得到的各个字串放到arry中,最终返回字串的数量 static int Split(const std::string& src,const std::string& sep,std::vector<std::string>* array) { size_t offset = 0; // 有10个字符,offset是查找的起始位置,范围应该是0~9,offset==10就代表已经越界了 while(offset < src.size()) { size_t pos = src.find(sep,offset); if(pos == std::string::npos)//没有找到特定的字符 {  //将剩余的部分当作⼀个字串,放⼊arry中  array->push_back(src.substr(offset));  return array->size(); } //abc....de if(pos == offset) {  offset = pos + sep.size();  continue;//当前字串是⼀个空的,没有内容 } array->push_back(src.substr(offset,pos - offset)); offset = pos + sep.size(); } return array->size(); } //读取⽂件的所有内容,将读取的内容放到⼀个Buffer中 static bool ReadFile(const std::string& filename,std::string* buf) { std::ifstream ifs(filename,std::ios::binary); if(ifs.is_open() == false) { LOG_FATAL(\"OPEN %s FILE FAILED\",filename.c_str()); return false; } size_t fsize = 0; ifs.seekg(0,ifs.end);//跳转读写位置到末尾 fsize = ifs.tellg();//获取当前读写位置相对于起始位置的偏移量,从末尾偏移刚好就是⽂件大小 ifs.seekg(0,ifs.beg);//跳转到起始位置 buf->resize(fsize);//开辟文件大小的空间 ifs.read(&(*buf)[0],fsize); if(ifs.good() == false) { LOG_FATAL(\"READ %s FILE FAILED\",filename.c_str()); ifs.close(); return false; } ifs.close(); return true; } //向文件写入数据 static bool WriteFile(const std::string filename,const std::string& in) { std::ofstream ofs(filename,std::ios::binary | std::ios::trunc); if(ofs.is_open() == false) { LOG_FATAL(\"OPEN %s FILE FAILED\",filename.c_str()); return false; } ofs.write(in.c_str(),in.size()); if(ofs.good() == false) { LOG_FATAL(\"Write %s FILE FAILED\",filename.c_str()); ofs.close(); return false; } ofs.close(); return true; } //URL编码,避免URL中资源路径与查询字符串中的特殊字符与HTTP请求中特殊字符产⽣歧义 //编码格式:将特殊字符的ascii值,转换为两个16进制字符,前缀%, C++ -> C%2B%2B //不编码的特殊字符: RFC3986⽂档规定 . - _ ~ 字⺟,数字属于绝对不编码字符 //RFC3986⽂档规定,编码格式 %HH  //W3C标准中规定,查询字符串中的空格,需要编码为+, 解码则是+转空格 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 == \'~\' || isalnum(c)) {  res += \'c\';  continue; } if(c == \' \' && convert_space_to_plus) {  res += \'+\';  continue; } //剩下的字符都是需要编码成为 %HH 格式 char tmp[4] = {0}; //snprintf 与 printf⽐较类似,都是格式化字符串,只不过⼀个是打印,⼀个是放到⼀块空间中 snprintf(tmp,4,\"%%%02X\",c); res += tmp; } return res; } static char HEXTOI(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_space_to_plus) { //遇到了%,则将紧随其后的2个字符,转换为数字,第⼀个数字左移4位,然后加上第二个数字 + -> 2b %2b->2 << 4 + 11 std::string res; for(int i = 0; i < url.size(); ++i) { if(url[i] == \'+\' && convert_space_to_plus) {  res += \' \';  continue; } if(url[i] == \'%\' && i + 2 < url.size()) {  //字符是以整数形成存储的  char v1 = HEXTOI(url[i + 1]) << 4;  char v2 = HEXTOI(url[i + 2]);  char c = v1 + v2;  res += c;  i += 2;  continue; } res += url[i]; } return res; } //获取响应状态码的描述信息 static std::string StatusDesc(int statu) { auto it = _status_msg.find(statu); if(it != _status_msg.end()) { return it->second; } return \"Unknow\"; } //根据文件后缀名获取文件mime static std::string ExMime(const std::string& filename) {  // a.b.txt 先获取⽂件扩展名 size_t pos = filename.rfind(\'.\'); if(pos == std::string::npos) { return \"application/octet-stream\"; } //根据扩展名,获取mim std::string ext = filename.substr(pos); auto it = _mine_msg.find(ext); if(it != _mine_msg.end()) { return it->second; } return \"application/octet-stream\"; } //判断一个文件是否是一个目录 static bool IsDirectory(const std::string& filename) { struct stat st; int ret = stat(filename.c_str(),&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.c_str(),&st); if(ret < 0) { return false; } return S_ISREG(st.st_mode); } //http请求的资源路径有效性判断 // /index.html --- 前边的/叫做相对根目录 映射的是某个服务器上的⼦目录 // 想表达的意思就是,客⼾端只能请求相对根⽬录中的资源,其他地⽅的资源都不予理会 // /../login, 这个路径中的..会让路径的查找跑到相对根⽬录之外,这是不合理的,不安全的 static bool ValidPath(const std::string& path) { //思想:按照/进⾏路径分割,根据有多少⼦目录,计算目录深度,有多少层,深度不能⼩于0 std::vector<std::string> res; Split(path,\"/\",&res); int level = 0; for(auto& s : res) { if(s == \"..\") {  --level;  //任意⼀层⾛出相对根目录,就认为有问题  if(level < 0)  { return false;  }  continue; } ++level; } return true; }};

2. HttpRequest模块

这个模块是HTTP请求数据模块,用于保存HTTP请求数据被解析后的各项请求元素信息。
【仿Mudou库one thread per loop式并发服务器实现】HTTP协议模块实现
HttpRequest模块:

http请求信息模块:存储HTTP请求信息要素,提供简单的功能性接口

请求信息要素:
请求行:请求方法,URL,协议版本
URL:资源路径,查询字符串
GET /search/1234?word=C++&en=utf8 HTTP/1.1
请求头部:key: value\\r\\nkey: value\\r\\n…
Content-Length: 0\\r\\n
正文

要素:请求方法,资源路径,查询字符串,头部字段,正文,协议版本
std:smatch保存首行使用regex正则进行解析后,所提取的数据,比如提取资源路径中的数字…

功能性接口:

  1. 将成员变量设置为公有成员,便于直接访问
  2. 提供查询字符串,以及头部字段的单个查询和获取,插入功能
  3. 获取正文长度
  4. 判断长连接&短链接Connection:close/keep-alive
//HttpRequest模块,存储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: HttpRequest():_version(\"Http/1.1\"){} void ReSet() { _method.clear(); _path.clear(); _version = \"Http/1.1\"; _body.clear(); std::smatch newmatches; _matches.swap(newmatches); _headers.clear(); _params.clear(); } //插入头部字段 void SetHeader(const std::string& key,const std::string& val) { _headers.insert({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({key,val}); } //判断是否有某个指定的查询字符串 bool HasParam(const std::string& key) { auto it = _params.find(key); if(it == _params.end()) { return false; } return true; } //获取指定的查询字符串 std::string GetParamr(const std::string& key) { auto it = _params.find(key); if(it == _params.end()) { return \"\"; } return it->second; } //获取正文长度 size_t ContentLength() { // Content-Length: 1234\\r\\n bool ret = HasHeader(\"Content-Length\"); if(ret == false) { return 0; } return std::stol(GetHeader(\"Content-Length\")); } //判断是否是短连接 bool Close() const { // 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接 if(HasHeader(\"Connection\") == true && GetHeader(\"Connection\") == \"keep-alive\") { return false; } return true; }};

3. HttpResponse模块

这个模块是HTTP响应数据模块,用于业务处理后设置并保存HTTP响应数据的的各项元素信息,最终会被按照HTTP协议响应格式组织成为响应信息发送给客户端。

【仿Mudou库one thread per loop式并发服务器实现】HTTP协议模块实现

HttpResponse模块:

功能:存储HTTP响应信息要素,提供简单的功能性接口

响应信息要素:

  1. 响应状态码
  2. 头部字段
  3. 响应正文
  4. 重定向信息(是否进行了重定向的标志,重定向的路径)

功能性接口:

  1. 为了便于成员的访问,因此将成员设置为公有成员
  2. 头部字段的新增,查询,获取
  3. 正文的设置
  4. 重定向的设置
  5. 长短连接的判断
//HttpResponse模块,存储Http响应信息要素,提供简单的功能性接口class HttpResponse{ public: int _status;//响应码 bool _redirect_flag;//是否重定向 std::string _body;//正文 std::string _redirect_url;//重定向url std::unordered_map<std::string,std::string> _headers;//头部字段 public: HttpResponse():_status(200),_redirect_flag(false){} HttpResponse(int status):_status(status),_redirect_flag(false){} void ReSet() { _status = 200; _redirect_flag = false; _body.clear(); _redirect_url.clear(); _headers.clear(); } //插入头部字段 void SetHeader(const std::string& key,const std::string& val) { _headers.insert({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 SetRedirect(const std::string& url,int status = 302) { _redirect_flag = true; _redirect_url = url; _status = status; } //判断是否是短连接 bool Close() { // 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接 if(HasHeader(\"Connection\") == true && GetHeader(\"Connection\") == \"keep-alive\") { return false; } return true; }};

4. HttpContext模块

这个模块是一个HTTP请求接收的上下文模块,主要是为了防支在一次接收的数据中,不是一个完整的HTTP请求,则解析过程并未完成,无法进行完整的请求处理,需要在下次接收到新数据后继续根据上下文进行解析,最终得到一个HttpRequest请求信息对象,因此在请求数据的接收以及解析部分需要一个上下文来进行控制接收和处理节奏。

【仿Mudou库one thread per loop式并发服务器实现】HTTP协议模块实现

typedef enum{ RECV_HTTP_ERROR, RECV_HTTP_LINE, RECV_HTTP_HEAD, RECV_HTTP_BODY, RECV_HTTP_OVER}HttpRecvStatus;#define MAX_BYTE 8 * 1024//HttpContext请求接收上下文模块,记录HTTP请求的接收以及处理进度class HttpContext{ private: int _resp_status;//响应状态码,解析请求出错时设置 HttpRecvStatus _recv_status;//当前接收及解析的阶段状态 HttpRequest _request;//已经解析得到的请求信息 private: //接收请求行 bool RecvHttpLine(Buffer* buf) { if (_recv_status != RECV_HTTP_LINE) return false; //1. 获取一行数据,带有末尾的换行 \\n \\r\\n std::string line = buf->GetLineAndPop(); //2. 需要考虑的⼀些要素:缓冲区中的数据不足一行, 获取的一行数据超大 if(line.size() == 0) { //缓冲区中的数据不足一行,则需要判断缓冲区的可读数据⻓度,如果很长了都不足一行,这是有问题的 if(buf->ReadAbleSize() > MAX_BYTE) {  _recv_status = RECV_HTTP_ERROR;  _resp_status = 414 ;//\"URI Too Long\"  return false; } //缓冲区中数据不足一行,但是也不多,就等等新数据的到来 return true; } if(line.size() > MAX_BYTE) { if(buf->ReadAbleSize() > MAX_BYTE) {  _recv_status = RECV_HTTP_ERROR;  _resp_status = 414 ;//\"URI Too Long\"  return false; } } bool ret = ParseHttpLine(line); if(ret == false) { return false; } //首行处理完毕,进⼊头部获取阶段 _recv_status = RECV_HTTP_HEAD; return true; } //解析请求行 bool ParseHttpLine(const std::string& line) { std::smatch matches; //std::regex::icase忽略大小写 std::regex e(\"(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\\\?(.*))? (HTTP/1\\\\.[01])(?:\\n|\\r\\n)?\", std::regex::icase); bool ret = std::regex_match(line, matches, e); if (ret == false) { _recv_status = RECV_HTTP_ERROR; _resp_status = 400 ;//\"Bad Request\" return false; } //0 : GET /bitejiuyeke/login?user=xiaoming&pass=123123 HTTP/1.1 //1 : GET //2 : /bitejiuyeke/login //3 : user=xiaoming&pass=123123 //4 : HTTP/1.1 //请求⽅法的获取 _request._method = matches[1]; //小写转成大写,为了下面HttpServer模块中用大写的请求方法进行判断时不会因为大小写出现不匹配的情况 std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper); //资源路径的获取,需要进⾏URL解码操作,但是不需要+转空格 _request._path = Until::UrlDecode(matches[2],false); //协议版本的获取 _request._version = matches[4]; //查询字符串的获取与处理 std::vector<std::string> query_string_arry; std::string query_string = matches[3]; //查询字符串的格式 key=val&key=val....., 先以 & 符号进⾏分割,得到各个字串 Until::Split(query_string,\"&\",&query_string_arry); //针对各个字串,以 = 符号进⾏分割,得到key 和val, 得到之后也需要进⾏URL解码 for(auto& str : query_string_arry) { size_t pos = str.find(\"=\"); if(pos == std::string::npos) {  _recv_status = RECV_HTTP_ERROR;  _resp_status = 400 ;//\"Bad Request\"  return false; } std::string key = Until::UrlDecode(str.substr(0,pos),true); std::string val = Until::UrlDecode(str.substr(pos + 1),true); _request._params.insert({key,val}); } return true; } //接收请求头部 bool RecvHttpHead(Buffer* buf) { if(_recv_status != RECV_HTTP_HEAD) return false; //1. 一⾏一行取出数据,直到遇到空行为⽌, 头部的格式 key: val\\r\\nkey: val\\r\\n\\r\\n while(1) { std::string line = buf->GetLineAndPop(); //2. 需要考虑的⼀些要素:缓冲区中的数据不足一行, 获取的一行数据超大 if(line.size() == 0) {  //缓冲区中的数据不足一行,则需要判断缓冲区的可读数据⻓度,如果很长了都不足一行,这是有问题的  if(buf->ReadAbleSize() > MAX_BYTE)  { _recv_status = RECV_HTTP_ERROR; _resp_status = 414;//\"URI Too Long\" return false;  }  //缓冲区中数据不足一行,但是也不多,就等等新数据的到来  return true; } if(line.size() > MAX_BYTE) {  _recv_status = RECV_HTTP_ERROR;  _resp_status = 414;  return false; } //遇到空行头部提取结束 if(line == \"\\n\" || line == \"\\r\\n\") {  break; } bool ret = ParseHttpHead(line); if(ret == false) {  return false; } } //头部处理完毕,进入正文获取阶段 _recv_status = RECV_HTTP_BODY; return true; } //解析请求头部 bool ParseHttpHead(std::string& line) { //key: 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_status = RECV_HTTP_ERROR; _resp_status = 400; return false; } std::string key = line.substr(0,pos); std::string val = line.substr(pos + 2); _request.SetHeader(key,val); return true; } //接收请求正文 bool RecvHttpBody(Buffer* buf) { if(_recv_status != RECV_HTTP_BODY) return false; //1. 获取正文长度 size_t content_length = _request.ContentLength(); if(content_length == 0) { //没有正⽂,则请求接收解析完毕 _recv_status = RECV_HTTP_OVER; return true; } //2. 当前已经接收了多少正文,其实就是往 _request._body 中放了多少数据了 size_t real_len = content_length - _request._body.size();//实际还需要接收的正⽂长度 //3. 接收正文放到body中,但是也要考虑当前缓冲区中的数据,是否是全部的正⽂ // 3.1 缓冲区中数据,包含了当前请求的所有正文,则取出所需的数据 if(buf->ReadAbleSize() >= real_len) { _request._body.append(buf->ReadPosition(),real_len); buf->MoveReadOffset(real_len); _recv_status = RECV_HTTP_OVER; return true; } // 3.2 缓冲区中数据,⽆法满⾜当前正文的需要,数据不足,取出数据,后续等待新数据到来 _request._body.append(buf->ReadPosition(),buf->ReadAbleSize()); buf->MoveReadOffset(buf->ReadAbleSize()); return true; } public: HttpContext():_recv_status(RECV_HTTP_LINE),_resp_status(200){} void ReSet() { _recv_status = RECV_HTTP_LINE; _resp_status = 200; _request.ReSet(); } //接收解析遇到错误时返回对应的错误码 int RespStatus() { return _resp_status; } //当前请求接收解析到那个阶段 HttpRecvStatus RecvStatus() { return _recv_status; } //返回请求解析后的HttpRequest对象 HttpRequest &Request() { return _request; } //接收并解析HTTP请求 void RecvHttpRequest(Buffer* buf) { //不同的状态,做不同的事情,但是这⾥不要break, 因为处理完请求⾏后,应该⽴即处理头部,⽽不是退出等新数据 switch(_recv_status) { case RECV_HTTP_LINE: RecvHttpLine(buf); case RECV_HTTP_HEAD: RecvHttpHead(buf); case RECV_HTTP_BODY: RecvHttpBody(buf); } return; } };

5. HttpServer模块

这个模块是最终给组件使用者提供的HTTP服务器模块了,用于以简单的接口实现HTTP服务器的搭建。

HttpServer模块内部包含有一个TcpServer对象:TcpServer对象实现服务器的搭建
HttpServer模块内部包含有两个提供给TcpServer对象的接口:连接建立成功设置上下文接口,数据处理接口。

HttpServer模块内部包含有一个hash-map表存储请求与处理函数的映射表:组件使用者向HttpServer设置哪些请求应该使用哪些函数进行处理,等TcpServer收到对应的请求就会使用对应的函数进行处理。

【仿Mudou库one thread per loop式并发服务器实现】HTTP协议模块实现

HttpServer模块:用于实现Http服务器的搭建

首先要给不同的请求方法的请求路径设置对应的回调函数。也就是设计一张请求路由表。

【仿Mudou库one thread per loop式并发服务器实现】HTTP协议模块实现
设计一张请求路由表:

表中记录了针对哪个请求,应该使用哪个函数来进行业务处理的映射关系。
当服务器收到了一个请求,就在请求路由表中,查找有没有对应请求的处理函数,如果有,则执行对应的处理函数即可。说白了,什么请求,怎么处理,由用户来设定,服务器收到了请求只需要执行函数即可。

这样做的好处:用户只需要实现业务处理函数,然后将请求与处理函数的映射关系,添加到服务器中。

而服务器只需要接收数据,解析数据,查找路由表映射关系,执行业务处理函数。
要实现简便的搭建HTTP服务器,所需要的要素和提供的功能。

要素:

  1. GET请求的路由映射表
  2. POST请求的路由映射表
  3. PUT请求的路由映射表
  4. DELETE请求的路由映射表—路由映射表记录对应请求方法的请求资源路径与对应业务处理函数映射关系–更多是功能性请求的处理
  5. 静态资源相对根目录-实现静态资源请求的处理
  6. 高性能TCP服务器—进行连接的IO操作

公有接口:

  1. 添加请求—处理函数映射信息(GET/POST/PUT/DELETE)
  2. 设置静态资源根目录
  3. 设置是否启动超时连接关闭
  4. 设置线程池中线程数量
  5. 启动服务器

私有接口:

  1. OnConnected—用于给TcpServer设置协议上下文
  2. OnMessage----用于进行缓冲区数据解析处理
  3. 请求的路由查找
  4. 静态资源请求查找和处理
  5. 功能性请求的查找和处理
  6. 组织响应进行回复

服务器处理流程:

  1. 从socket接收数据,放到接收缓冲区
  2. 调用OnMessage回调函数进行业务处理
  3. 对请求进行解析,得到了一个HttpRequest结构,包含了所有的请求要素
  4. 进行请求的路由查找-找到对应请求的处理方法
    a. 静态资源请求–一些实体文件资源的请求,html,image.…
    将静态资源文件的数据读取出来,填充到HttpResponse结构中
    b. 功能性请求—在请求路由映射表中查找处理函数,找到了则执行函数
    具体的业务处理,并进行HttpResponse结构的数据填充
  5. 对静态资源请求/功能性请求进行处理完毕后,得到了一个填充了响应信息的HttpResponse对象,组织http格式响应,进行发送。
//HttpServer模块,用于实现HttpServer服务器的搭建const std::string html_404 = \"/404.html\";const std::string home_page = \"index.html\";#define DEFALT_TIMEOUT 10class HttpServer{ private: //请求路由表 using Handler = std::function<void(const HttpRequest&,HttpResponse*)>; //请求资源路径我们用正则表达式 //比如 /number/1、/number/2、/number/3 这样的资源路径 //对应的业务处理函数都是一样的。如果一个路径给没有必要。 //因此请求路径用正则表达式,比如number/d+ 这个匹配上面三个 //正则表达式可以用来判断字符串中有没有某个子串 using Handlers = std::vector<std::pair<std::regex,Handler>>; Handlers _get_route; Handlers _post_route; Handlers _put_route; Handlers _delete_route; std::string _basedir;//静态资源根⽬录 TcpServer _server; private: void ErrHandler(const HttpRequest& req,HttpResponse* rsp) { //1. 组织⼀个错误展示页⾯ std::string body; body += \"\"; body += \"\"; body += \"\"; body += \"\"; body += \"\"; body += \"

\"; body += std::to_string(rsp->_status); body += \" \"; body += Until::StatusDesc(rsp->_status); body += \"

\"
; body += \"\"; body += \"\"; //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() && rsp->HasHeader(\"Content-Length\") == false) { rsp->SetHeader(\"Content-Length\",std::to_string(rsp->_body.size())); } if(!rsp->_body.empty() && rsp->HasHeader(\"Content-Typy\") == false) { rsp->SetHeader(\"Content-Type\",\"application/octet-stream\"); } if(rsp->_redirect_flag == true) { rsp->SetHeader(\"Location\",rsp->_redirect_url); } //2. 将rsp中的要素,按照http协议格式进⾏组织 std::stringstream rsp_string; rsp_string << req._version << \" \" << std::to_string(rsp->_status) << \" \" << Until::StatusDesc(rsp->_status) << \"\\r\\n\"; for(auto& header : rsp->_headers) { rsp_string << header.first << \": \" << header.second << \"\\r\\n\"; } rsp_string << \"\\r\\n\"; rsp_string << rsp->_body; //3. 发送数据 conn->Send(rsp_string.str().c_str(),rsp_string.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(Until::ValidPath(req._path) == false) { return false; } // 4. 请求的资源必须存在,且是⼀个普通⽂件 // 有⼀种请求⽐较特殊 -- ⽬录:/, 这种情况给后边默认追加⼀个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 += home_page; } if (Until::IsRegular(req_path) == false) { return false; } return true; } //静态资源的请求处理 --- 将静态资源⽂件的数据读取出来,放到rsp的_body中, 并设置mime void FileHandler(const HttpRequest& req,HttpResponse* rsp) { std::string req_path = _basedir + req._path; if(req._path.back() == \'/\') { //默认首页 req_path += home_page; } bool ret = Until::ReadFile(req_path,&rsp->_body); if(ret == false)//进来这里已经判断过资源肯定是存在的,否则就不会进入静态资源的处理 { return; } std::string mime = Until::ExMime(req_path); rsp->SetHeader(\"Content-Type\",mime); return; } //功能性请求的的分类处理 void Dispatcher(HttpRequest& req,HttpResponse* rsp,const 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; } //传⼊请求信息,和空的rsp,执⾏处理函数 return functor(req,rsp); } //返回错误码404页面 rsp->_status = 404; // std::string path = _basedir + html_404; // bool ret = Until::ReadFile(path,&rsp->_body); // if(ret == false)//错误路径是我们自己设置肯定是存在的 // { // return; // } // return rsp->SetHeader(\"Content-Type\",\"text/html\"); } //路由选择 void Route(HttpRequest& req,HttpResponse* rsp) { //1. 对请求进⾏分辨,是⼀个静态资源请求,还是⼀个功能性请求 // 静态资源请求,则进⾏静态资源的处理 // 功能性请求,则需要通过⼏个请求路由表来确定是否有处理函数 // 既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405 if(IsFileHandler(req)) { //是⼀个静态资源请求, 则进⾏静态资源请求的处理 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->_status = 405;// Method Not Allowed return; } //连接建立后给对应Connection对象设置一个协议上下文 void OnConnected(const PtrConnection& conn) { conn->SetContent(HttpContext()); } //缓存区数据解析+处理 void OnMessage(const PtrConnection& conn,Buffer* buf) { while(buf->ReadAbleSize() > 0) { //1. 获取上下⽂ //获取在连接建立好就给每个Connection设置HttpContext上下文 HttpContext* context = conn->GetContent()->Get<HttpContext>(); //2. 通过上下⽂对缓冲区数据进⾏解析,得到HttpRequest对象 // 2.1 如果缓冲区的数据解析出错,就直接回复出错响应 // 2.2 如果解析正常,且请求已经获取完毕,才开始去进⾏处理 context->RecvHttpRequest(buf); HttpRequest& rep = context->Request(); HttpResponse rsp(context->RespStatus()); //if(context->RecvStatus() == RECV_HTTP_ERROR) if (context->RespStatus() >= 400) { //进⾏错误响应,关闭连接 //填充⼀个错误显⽰页⾯数据到rsp中 ErrHandler(rep,&rsp); //组织响应发送给客户端 WriteResponse(conn,rep,&rsp); //一定要做下面两步,不然出错了,关闭连接时,接收缓存区还有数据关闭连接的时候先去先处理接收缓存区数据 //但是当前上下文状态一直是RECV_HTTP_ERROR,因此每次去接收缓存区根本拿不到数据,所有在这里死循环 //造成内存资源不足,服务器奔溃退出 //因此在这里把上下文状态重置RECV_HTTP_LINE可以每次都从接收缓存区拿到数据 //直到最后接收缓存区数据不足一行,从下面退出,然后真正的去关闭连接 context->ReSet(); //这里也可以,出错了就把接收缓冲区数据清空,也就不会在多次调用了 buf->MoveReadOffset(buf->ReadAbleSize()); //关闭连接 conn->Shutdown(); return; } //当前请求还没有接收完整,则退出,等新数据到来再重新继续处理 if(context->RecvStatus() != RECV_HTTP_OVER) { return; } //3. 请求路由 + 业务处理 Route(rep,&rsp); //4. 对HttpResponse进⾏组织发送 WriteResponse(conn,rep,&rsp); //5. 重置上下⽂,避免影响下次解析 context->ReSet(); //6. 根据⻓短连接判断是否关闭连接或者继续处理 if(rsp.Close() == true) { //短链接则直接关闭 return conn->Shutdown(); } } return; } public: HttpServer(uint16_t 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(Until::IsDirectory(path) == true); _basedir = path; } void Get(const std::string& parttern,const Handler& handler) { _get_route.push_back(std::make_pair(std::regex(parttern),handler)); } void Post(const std::string& parttern,const Handler& handler) { _post_route.push_back(std::make_pair(std::regex(parttern),handler)); } void Put(const std::string& parttern,const Handler& handler) { _put_route.push_back(std::make_pair(std::regex(parttern),handler)); } void Delete(const std::string& parttern,const Handler& handler) { _delete_route.push_back(std::make_pair(std::regex(parttern),handler)); } void SetThreadCount(int count) { _server.SetThreadCount(count); } void Start() { _server.Start(); }};