【项目】仿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();}
};