当前位置: 首页 > news >正文

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

HTTP协议模块实现

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

1. Util模块

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

在这里插入图片描述

#include "server.hpp"
#include <fstream>
#include <sys/stat.h>
#include <regex>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....deif(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 + 11std::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";}//根据文件后缀名获取文件mimestatic std::string ExMime(const std::string& filename){   // a.b.txt 先获取⽂件扩展名size_t pos = filename.rfind('.');if(pos == std::string::npos){return "application/octet-stream";}//根据扩展名,获取mimstd::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){//思想:按照/进⾏路径分割,根据有多少⼦目录,计算目录深度,有多少层,深度不能⼩于0std::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请求数据被解析后的各项请求元素信息。
在这里插入图片描述
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\nbool 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协议响应格式组织成为响应信息发送给客户端。

在这里插入图片描述

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;//重定向urlstd::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请求信息对象,因此在请求数据的接收以及解析部分需要一个上下文来进行控制接收和处理节奏。

在这里插入图片描述

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\nstd::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\nwhile(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收到对应的请求就会使用对应的函数进行处理。

在这里插入图片描述

HttpServer模块:用于实现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 10
class 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 += "<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->_status);body += " ";body += Until::StatusDesc(rsp->_status);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() && 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.pngstd::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中, 并设置mimevoid 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/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;}//传⼊请求信息,和空的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. 对请求进⾏分辨,是⼀个静态资源请求,还是⼀个功能性请求// 静态资源请求,则进⾏静态资源的处理// 功能性请求,则需要通过⼏个请求路由表来确定是否有处理函数// 既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405if(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 Allowedreturn;}//连接建立后给对应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();}
};

相关文章:

  • Java中如何创建操作线程
  • 【Tip】MathType中输入空格符号
  • Indocia启动$INDO代币预售第一阶段 - 100% 社区安全,具有真正的盈利潜力
  • 【Python】如何查找电脑上的Python解释器
  • 【回眸】error: failed to compile `xxxxxx`重装rust环境
  • Unocss 类名基操, tailwindcss 类名
  • 【错误记录】Windows 命令行程序循环暂停问题分析 ( 设置 “ 命令记录 “ 选项 | 启用 “ 丢弃旧的副本 “ 选项 | 将日志重定向到文件 )
  • SpringBoot和微服务学习记录Day3
  • Java 自动装箱与拆箱:基本数据类型与包装类的转换
  • 【Java面试笔记:基础】1.谈谈你对Java平台的理解?
  • pip永久换镜像地址
  • 解决Chrome浏览器访问https提示“您的连接不是私密连接”的问题
  • DSRAM介绍
  • 【NCCL】transport建立(一)
  • c++学习之---vector
  • 【集群IP管理分配技术_DHCP】二、DHCP核心功能与技术实现
  • 实训Day-1 漏洞攻击实战
  • 深入解析React.lazy与Suspense:现代React应用的性能优化利器
  • 【网络安全】CI/CD 流水线漏洞
  • 动态监控进程
  • 三江购物:因自身商业需要,第二大股东阿里泽泰拟减持不超3%公司股份
  • 世界读书日|南京图书馆开了首个网络文学主题阅读空间
  • 部分人员无资质展业、投资建议无合理依据,天相财富被责令改正
  • 嘉兴乌镇一化工公司仓库火灾后,当地召开火灾警示现场会
  • 俄总统新闻秘书:乌克兰问题谈判相当艰难
  • 农业农村部原党组书记、部长唐仁健被提起公诉