HttpServer整合模块设计与实现(http模块五)

目录

类功能

类定义

类实现

编译测试

源码路标


类功能

类定义

// HttpServer模块功能设计
class HttpServer
{
private:using Handler = std::function<void(const HttpRequest &, HttpResponse &)>;std::unordered_map<std::string, Handler> _get_route;std::unordered_map<std::string, Handler> _post_route;std::unordered_map<std::string, Handler> _put_route;std::unordered_map<std::string, Handler> _delete_route;std::string _basedir; // 静态资源根目录TcpServer _server;private:void WriteReponse(); // 将HttpResponse中的要素按照http协议格式进行组织,发生void FileHandler();  // 静态资源请求void Dispatcher();   // 功能性请求的分类处理(即查找请求)void Route();void OnConnected(); // 设置上下文void OnMessage();   // 缓冲区数据解析+处理public:HttpServer();void SetBaseDir(const std::string &path);void Get(const std::string &pattern, Handler &handler);void Post(const std::string &pattern, Handler &handler);void Put(const std::string &pattern, Handler &handler);void Delete(const std::string &pattern, Handler &handler);void SetThreadCount(int count);void EnableInactiveRelease(int timeout);void Listen();
};

类实现

// HttpServer模块功能设计
class HttpServer
{
private:using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;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 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::StatuDesc(rsp->_statu);body += "</h1>";body += "</body>";body += "</html>";// 2. 将页面数据,当作响应正文,放入rsp中rsp->SetContent(body, "text/html");}// 将HttpResponse中的要素按照http协议格式进行组织,发送void WriteReponse(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.HasHeader("Content-Length") == false){rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));}if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == 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_str;rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(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::ValidPath(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;}// 静态资源的请求处理 -- 将静态资源文件的数据读取出来,放到rsp的_body中,并设置mimevoid 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){// 1. 对请求进行分辨,是一个静态资源请求,还是一个功能性请求//    静态资源请求,则进行静态资源的处理//    功能性请求,则需要通过几个请求路由表来确定是否有处理函数//    既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回404if (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());DBG_LOG("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对象//  2.1 如果缓冲区的数据解析出错,就直接回复出错响应//  2.2 如果解析正常,且请求已经获取完毕,才开始去进行处理context->RecvHttpRequest(buffer);HttpRequest &req = context->Request();HttpResponse rsp(context->RespStatu());if (context->RespStatu() >= 400){// 进行错误响应,关闭连接ErrorHandler(req, &rsp); // 填充一个错误显示页面数据到rsp中WriteReponse(conn, req, rsp);context->ReSet();buffer->MoveReadOffset(buffer->ReadAbleSize()); // 出错了就把缓冲区数据清空conn->Shutdown();                               // 关闭连接return;}if (context->RecvStatu() != RECV_HTTP_OVER)return; // 当前请求还没有接收完整,则退出,等新数据到来再重新继续处理// 3. 请求路由 + 业务处理Route(req, &rsp);// 4. 对HttpResponse进行组织发送WriteReponse(conn, req, rsp);// 5. 重置上下文context->ReSet();// 6. 根据长短连接判断是否关闭连接或者继续处理if (rsp.Close() == true)conn->Shutdown(); // 短连接则直接关闭}return;}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){_basedir = path;}// 设置/添加,请求(请求的正则表达式)与处理函数的映射关系void Get(const std::string &pattern, Handler &handler){_get_route.push_back(std::make_pair(std::regex(pattern), handler));}void Post(const std::string &pattern, Handler &handler){_post_route.push_back(std::make_pair(std::regex(pattern), handler));}void Put(const std::string &pattern, Handler &handler){_put_route.push_back(std::make_pair(std::regex(pattern), handler));}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();}
};

编译测试

未发现编译语法上的错误

源码路标

鉴于对前文进行了修正,所以特此将源码更新

#include "../server.hpp"
#include <sys/stat.h>
#include <fstream>
#include <regex>#define DEFALT_TIMEOUT 30class Util
{
public:// 字符串分割函数static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *arry){size_t offset = 0;// 有10个字符,offset是查找的起始位置,范围应该是0~9,offset==10就代表已经越界了,返回查找的位置while (offset < src.size()){size_t pos = src.find(sep, offset); // 在src字符串偏移量offset处,开始向后查找sep字符/子串,返回查找到的位置if (pos == std::string::npos)       // 没有找到特定的字符{// 将剩余的部分当做一个子串,放入arry中if (pos == src.size())break;arry->push_back(src.substr(offset));return arry->size();}if (pos == offset){offset = pos + sep.size();continue; // 当前字符串是一个空串,没有内容}arry->push_back(src.substr(offset, pos - offset));offset = pos + sep.size();}return arry->size();}// 读取文件的所有内容,将读取到的内容放到一个容器中static bool ReadFile(const std::string &filename, std::string *buf){std::ifstream ifs(filename, std::ios::binary); // 以二进制的方式读取if (ifs.is_open() == false){printf("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); // c_str()返回的是一个const,所以不行if (ifs.good() == false){printf("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 &buf){std::ofstream ofs(filename, std::ios::binary | std::ios::trunc);if (ofs.is_open() == false){printf("OPEN %s FILE FAILED!!", filename.c_str());return false;}ofs.write(buf.c_str(), buf.size());if (ofs.good() == false){ERR_LOG("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 == true){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';else if (c >= 'a' && c <= 'z')return c - 'a' + 10; // 注意要加10,16进制中,字母a从11开始else 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] == '%'){char v1 = HEXTOI(url[i + 1]);char v2 = HEXTOI(url[i + 2]);char v = v1 * 16 + v2;i += 2;continue;}res += url[i];}return res;}// 响应状态码的描述信息获取static std::string StatuDesc(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 "Unknow";}// 根据文件后缀名获取文件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);auto it = _mime_msg.find(ext);if (it == _mime_msg.end())return "application/octet-stream";return it->second;}// 判断一个文件是否是一个目录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> subdir;Split(path, "/", &subdir);int level = 0;for (auto &dir : subdir){if (dir == ".."){level--; // 任意一层走出相对根目录,就认为有问题if (level < 0)return false;continue;}level++;}return true;}
};// HttpRequest请求模块
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 match; // smatch 内并没有clear的接口,所以使用交换进行清理_matches.swap(match);_headers.clear();_params.clear();}// 插入头部字段void SetHeader(const std::string &key, const std::string &val){_headers.insert(std::make_pair(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(std::make_pair(key, val));}// 判断是否有某个指定的查询字符串bool HasParam(const std::string &key) const{auto it = _params.find(key);if (it == _params.end())return false;return true;}// 获取指定的查询字符串std::string GetParam(const std::string &key) const{auto it = _params.find(key);if (it == _params.end())return "";return it->second;}// 获取正文长度size_t ContentLength() const{// 下面的字符串肯定是已经被解析放置在_headers内了// Content-Length: 1234\r\nbool ret = HasHeader("Content-Length");if (ret == false)return 0;std::string clen = GetHeader("Content-Length");return std::stol(clen); // 字符转长整型}// 判断是否是短链接bool Close() const{// 没有Connection字段,或者有Connection字段但是值是close,则是短链接,否则就是长链接// keep-alive是长链接的意思if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")return true;return false;}
};// HttpResponse响应模块功能设计
class HttpResponse
{
public:int _statu;bool _redirect_flag; // 重定向标志std::string _body;std::string _redirect_url; // 重定向地址std::unordered_map<std::string, std::string> _headers;public:HttpResponse() : _redirect_flag(false), _statu(200) {}HttpResponse(int statu) : _redirect_flag(false), _statu(statu) {}void ReSet() // 重置{_statu = 200;_redirect_flag = false;_body.clear();_redirect_url.clear();_headers.clear();}// 插入头部字段void SetHeader(const std::string &key, const std::string &val){_headers.insert(std::make_pair(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("Cnotent-Type", type);}// 默认为302,临时重定向,301为永久重定向void SetRedirect(std::string &url, int statu = 302){_statu = statu;_redirect_flag = true;_redirect_url = url;}// 判断是否是短链接bool Close(){// 没有Connection字段,或者有Connection字段但是值是close,则是短链接,否则就是长链接// keep-alive是长链接的意思if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")return true;return false;}
};// HttpContext接收请求上下文模块功能设计
typedef enum
{RECV_HTTP_ERROR,RECV_HTTP_LINE,RECV_HTTP_HEAD,RECV_HTTP_BODY,RECV_HTTP_OVER
} HttpRecvStatu;#define MAX_LINE 8192 // 8K
class HttpContext
{
private:int _resp_statu;           // 响应状态码HttpRecvStatu _recv_statu; // 当前接收及解析的阶段状态HttpRequest _request;      // 已经解析得到的请求信息
private:// 解析请求行bool ParseHttpLine(const std::string &line){std::smatch matches;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_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}// 0 : GET /qingfeng/login?user=xiaoming&pass=123123 HTTP/1.1// 1 : GET// 2 : /qingfeng/login// 3 : user=xiaoming&pass=123123// 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_arry;std::string query_string = matches[3];// 查询字符串的格式, key=val&key=val......, 先以 & 符号进行分割,得到各个子串Util::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_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUESTreturn false;}std::string key = Util::UrlDecode(str.substr(0, pos), true);std::string val = Util::UrlDecode(str.substr(pos + 1), true);_request.SetParam(key, val);}return true;}// 获取请求行bool RecvHttpLine(Buffer *buf){if (_recv_statu != RECV_HTTP_LINE)return false;// 1. 获取一行数据std::string line = buf->GetLineAndPop();// 2. 需要考虑的一些要素,缓冲区的数据不足一行, 获取的一行数据超大if (line.size() == 0){// 缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的if (buf->ReadAbleSize() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URI TOO LONG 查看statu文件中可以看到414状态对应的信息return false;}// 缓冲区中数据不足一行,但是也不多,就等待新数据的到来return true;}if (line.size() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URI TOO LONGreturn false;}bool ret = ParseHttpLine(line);if (ret == false)return false;// 首行处理完毕,进入头部处理阶段_recv_statu = RECV_HTTP_HEAD;return true;}bool RecvHttpHead(Buffer *buf){// 状态不对直接返回if (_recv_statu != RECV_HTTP_HEAD)return false;// 一行一行取出数据,直到遇到空行为止,头部的格式 key: val\r\nkey: val\r\n......while (1){std::string line = buf->GetLineAndPop();// 2. 需要考虑的一些要素,缓冲区的数据不足一行, 获取的一行数据超大if (line.size() == 0){// 缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的if (buf->ReadAbleSize() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URI TOO LONG 查看statu文件中可以看到414状态对应的信息return false;}// 缓冲区中数据不足一行,但是也不多,就等待新数据的到来return true;}if (line.size() > MAX_LINE){_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; // URI 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 val = line.substr(pos + 2);_request.SetHeader(key, val);return true;}bool RecvHttpBody(Buffer *buf){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_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_statu = RECV_HTTP_OVER;return true;}//  3.2 缓冲区中的数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来_request._body.append(buf->ReadPosition(), buf->ReadAbleSize());buf->MoveReadOffset(buf->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 *buf){// 不同的状态,做不同的事情,但是这里不要break,因为处理完请求行后,应该立即处理头部,而不是退出等待新数据的到来// 后面并不需要break,因为要保证后续都解析完,并且不用担心接收失败// 在各个函数的开头就检查了处理阶段,如果不对就会错误返回了switch (_recv_statu){case RECV_HTTP_LINE:RecvHttpLine(buf);case RECV_HTTP_HEAD:RecvHttpHead(buf);case RECV_HTTP_BODY:RecvHttpBody(buf);}return;}
};// HttpServer模块功能设计
class HttpServer
{
private:using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;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 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::StatuDesc(rsp->_statu);body += "</h1>";body += "</body>";body += "</html>";// 2. 将页面数据,当作响应正文,放入rsp中rsp->SetContent(body, "text/html");}// 将HttpResponse中的要素按照http协议格式进行组织,发送void WriteReponse(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.HasHeader("Content-Length") == false){rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));}if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == 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_str;rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(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::ValidPath(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;}// 静态资源的请求处理 -- 将静态资源文件的数据读取出来,放到rsp的_body中,并设置mimevoid 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){// 1. 对请求进行分辨,是一个静态资源请求,还是一个功能性请求//    静态资源请求,则进行静态资源的处理//    功能性请求,则需要通过几个请求路由表来确定是否有处理函数//    既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回404if (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());DBG_LOG("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对象//  2.1 如果缓冲区的数据解析出错,就直接回复出错响应//  2.2 如果解析正常,且请求已经获取完毕,才开始去进行处理context->RecvHttpRequest(buffer);HttpRequest &req = context->Request();HttpResponse rsp(context->RespStatu());if (context->RespStatu() >= 400){// 进行错误响应,关闭连接ErrorHandler(req, &rsp); // 填充一个错误显示页面数据到rsp中WriteReponse(conn, req, rsp);context->ReSet();buffer->MoveReadOffset(buffer->ReadAbleSize()); // 出错了就把缓冲区数据清空conn->Shutdown();                               // 关闭连接return;}if (context->RecvStatu() != RECV_HTTP_OVER)return; // 当前请求还没有接收完整,则退出,等新数据到来再重新继续处理// 3. 请求路由 + 业务处理Route(req, &rsp);// 4. 对HttpResponse进行组织发送WriteReponse(conn, req, rsp);// 5. 重置上下文context->ReSet();// 6. 根据长短连接判断是否关闭连接或者继续处理if (rsp.Close() == true)conn->Shutdown(); // 短连接则直接关闭}return;}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::IsDirectory(path) == true);_basedir = path;}// 设置/添加,请求(请求的正则表达式)与处理函数的映射关系void Get(const std::string &pattern, const Handler &handler){_get_route.push_back(std::make_pair(std::regex(pattern), handler));}void Post(const std::string &pattern, const Handler &handler){_post_route.push_back(std::make_pair(std::regex(pattern), handler));}void Put(const std::string &pattern, const Handler &handler){_put_route.push_back(std::make_pair(std::regex(pattern), handler));}void Delete(const std::string &pattern, const Handler &handler){_delete_route.push_back(std::make_pair(std::regex(pattern), handler));}void SetThreadCount(int count){_server.SetThreadCount(count);}void Listen(){_server.Start();}
};

测试参考下文

HTTP服务器简单编译测试-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/750199.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ISIS接口认证实验简述

默认情况下&#xff0c;ISIS接口认证通过在ISIS协议数据单元&#xff08;PDU&#xff09;中添加认证字段&#xff0c;例如&#xff1a;一个密钥或密码&#xff0c;用于验证发送方的身份。 ISIS接口认证防止未经授权的设备加入到网络中&#xff0c;并确保邻居之间的通信是可信的…

实战:django项目环境搭建(pycharm,virtualBox)

django项目环境搭建 一.创建虚拟环境二.创建PyCharm远程连接 一.创建虚拟环境 需要用到的软件&#xff1a;PyCharm&#xff0c;VirtualBox虚拟机。 1.打开虚拟机终端&#xff0c;创建新的虚拟环境 Book。 2.在虚拟环境中创建新的文件夹 library&#xff0c;cd命令进入该文件…

【四 (6)数据可视化之 Grafana安装、页面介绍、图表配置】

目录 文章导航一、Grafana介绍[✨ 特性]二、安装和配置1、安装2、权限配置&#xff08;账户/团队/用户&#xff09;①用户管理②团队管理③账户管理④看板权限 3、首选项配置4、插件管理①数据源插件②图表插件③应用插件④插件安装方式一⑤安装方式二 三、数据源管理1、添加数…

【STM32定时器 TIM小总结】

STM32 TIM详解 TIM介绍定时器类型基本定时器通用定时器高级定时器常用名词时序图预分频时序计数器时序图 定时器中断配置图定时器定时 代码调试 TIM介绍 定时器&#xff08;Timer&#xff09;是微控制器中的一个重要模块&#xff0c;用于生成定时和延时信号&#xff0c;以及处…

Vue3+TypeScript 学习回顾,温故而知新

文章简介&#xff1a; &#xff08;1&#xff09;简介&#xff1a; 在 Vue3 中编码规范如下&#xff1a; 编码语言: JavaScript代码风格: 组合式API选项式、API简写形式: setup语法糖 &#xff08;2&#xff09;复习内容&#xff1a; 1.核心: ref、reactive、computed、w…

路由器端口转发远程桌面控制:一电脑连接不同局域网的另一电脑

一、引言 路由器端口转发&#xff1a;指在路由器上设置一定的规则&#xff0c;将外部的数据包转发到内部指定的设备或应用程序。这通常需要对路由器进行一些配置&#xff0c;以允许外部网络访问内部网络中的特定服务和设备。端口转发功能可以实现多种应用场景&#xff0c;例如远…

游戏引擎中的动画基础

一、动画技术简介 视觉残留理论 - 影像在我们的视网膜上残留1/24s。 游戏中动画面临的挑战&#xff1a; 交互&#xff1a;游戏中的玩家动画需要和场景中的物体进行交互。实时&#xff1a;最慢需要在1/30秒内算完所有的场景渲染和动画数据。&#xff08;可以用动画压缩解决&am…

用SeaTunnel从SQL Server向Elasticsearch同步数据

文章目录 引言I 步骤1.1 环境准备1.2 配置JDBC插件1.3 编写SeaTunnel任务配置II Enable Sql Server CDC引言 SeaTunnel 的官网 https://seatunnel.apache.org/ Support SQL Server Version: server:2008 (Or later version for information only)Supported DataSource Info: …

抖去推无人直播+矩阵托管+AI文案撰写一体化工具如何开发搭建

一、 开发和搭建抖去推无人直播矩阵托管AI文案撰写一体化工具需要以下步骤&#xff1a; 确定功能需求&#xff1a;确定抖去推无人直播、矩阵托管和AI文案撰写的具体功能需求&#xff0c;如直播推流、直播管理、托管服务、AI文案生成等。 技术选型&#xff1a;选择适合开发该工…

CSS3技巧38:3D 翻转数字效果

博主其它CSS3 3D的文章&#xff1a; CSS3干货4&#xff1a;CSS中3D运用_css 3d-CSDN博客 CSS3干货5&#xff1a;CSS中3D运用-2_中3d-2-CSDN博客 CSS3干货6&#xff1a;CSS中3D运用-3_css3d 使用-CSDN博客 最近工作上烦心的事情太多&#xff0c;只有周末才能让我冷静一下 cod…

HTTPS(超文本传输安全协议)工作过程

一、简述HTTPS HTTPS超文本传输协议&#xff08;全称&#xff1a;Hypertext Transfer Protocol Secure &#xff09;&#xff0c;是以安全为目标的 HTTP 通道&#xff0c;在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 。HTTPS 在HTTP 的基础下加入SSL&#x…

Linux第78步_使用原子整型操作来实现“互斥访问”共享资源

使用原子操作来实现“互斥访问”LED灯设备&#xff0c;目的是每次只允许一个应用程序使用LED灯。 1、创建MyAtomicLED目录 输入“cd /home/zgq/linux/Linux_Drivers/回车” 切换到“/home/zgq/linux/Linux_Drivers/”目录 输入“mkdir MyAtomicLED回车”&#xff0c;创建MyA…

Jmeter---分布式

分布式&#xff1a;多台机协作&#xff0c;以集群的方式完成测试任务&#xff0c;可以提高测试效率。 分布式架构&#xff1a;控制机&#xff08;分发任务&#xff09;与多台执行机&#xff08;执行任务&#xff09; 环境搭建&#xff1a; 不同的测试机上安装 Jmeter 配置基…

2024最新手赚手机软件APP下载排行网站源码及应用商店源码

这是一款简洁蓝色的手机软件下载应用排行、平台和最新发布网站&#xff0c;采用响应式织梦模板。主要包括主页、APP列表页、APP详情介绍页、新闻资讯列表、新闻详情页、关于我们等模块页面。 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/88898956 更…

政安晨:【深度学习处理实践】(九)—— Transformer架构

咱们接着这个系列的上一篇文章继续&#xff1a; 政安晨&#xff1a;【深度学习处理实践】&#xff08;八&#xff09;—— 表示单词组的两种方法&#xff1a;集合和序列https://blog.csdn.net/snowdenkeke/article/details/136762323 Transformer是一种架构&#xff0c;用于在…

k8s之图形界面DashBoard【九】

文章目录 9. DashBoard9.1 部署Dashboard9.2 使用DashBoard 镇场 9. DashBoard 之前在kubernetes中完成的所有操作都是通过命令行工具kubectl完成的。其实&#xff0c;为了提供更丰富的用户体验&#xff0c;kubernetes还开发了一个基于web的用户界面&#xff08;Dashboard&…

vscode jupyter 如何关闭声音

网上之前搜的zen模式失败 仅仅降低sound失败 #以下是成功方式&#xff1a; 首先确保user和remote的声音都是0&#xff1a; 然后把user和remote的以下设置都设置为off就行了&#xff01; 具体操作参考 https://stackoverflow.com/questions/54173462/how-to-turn-off-or-on-so…

从大模型到Agentscope——分布式Multi-Agent应用开发与部署

目录 Why需要分布式 案例 多进程的分布书版本能快速提升速度 分布式的挑战 AgentScope分布式解决 方案 实现RPC Agent 基于Actor模式的并行调度缺点&#xff1a;需要Agent内部决定消息传递目标 被调用的Agent立即返回占位符placeholder to_dist: 开启自动将单机进行扩展…

5 分钟小工具:使用 dive 分析 docker 镜像

需求 拿到一个镜像之后&#xff0c;我想知道&#xff1a; 分层查看镜像里都有哪些文件各层使用了什么命令构建的这个镜像镜像里比较大的文件有哪些&#xff08;可能需要优化&#xff09; dive 工具介绍 dive 工具可以做这些分析。dive 的 github 地址是 wagoodman/dive&…

观察者模式的理解和引用

1.前言 在之前的H5小游戏中&#xff0c;对于长连接发送的不同类型数据包的处理&#xff0c;是通过switch语句进行处理的&#xff0c;于是在自己的代码中出现了大量的case分支&#xff0c;不方便进行维护和后期的版本迭代。于是在老师的指导下&#xff0c;开始寻求使用观察者模…