【项目日记】仿mudou的高并发服务器 --- 实现HTTP服务器
✨✨✨项目地址在这里 ✨✨✨
✨✨✨https://gitee.com/penggli_2_0/TcpServer✨✨✨
仿mudou的高并发服务器
- 1 前言
- 2 Util工具类
- 3 HTTP协议
- 3.1 HTTP请求
- 3.2 HTTP应答
- 4 上下文解析模块
- 5 HTTP服务器对象
1 前言
上一篇文章我们基本实现了高并发服务器所需的基础模块,通过TcpServer类可以快速搭建一个TCP服务器。我们的最终目的是使用这个高并发服务器去实现一些业务,那么在网络通信中,我们就可以来实现一下HTTP服务。让浏览器可以访问获取数据。
为了实现HTTP服务器首要的工作就是实现HTTP协议,协议是网络通信的基础!只有确定了协议我们才能正常解析请求报文,并组织应答报文,可以让浏览器成功获取数据。
完成HTTP协议之后,就是设计一种报文解析模块,可以从缓冲区中获取数据,进行解析数据,得到完整请求。
最终将这些整合为一个HTTP服务器模块,设计回调函数,实现HTTP服务器的功能!
2 Util工具类
在HTTP服务器处理中,经常需要一些常用操作,比如切分字符串,编码转换,通过状态码找到对应状态解析… Util工具类就是用来实现这些功能的类!
- SplitStr
- 功能:根据指定的分隔符
sep
将字符串src
切分成多个子字符串,并将这些子字符串存储在sub
向量中。 - 返回值:返回切分后的子字符串数量。
- 功能:根据指定的分隔符
- ReadFile
- 功能:以二进制方式读取文件
filename
的内容到字符串buf
中。 - 返回值:如果文件打开和读取成功,返回
true
;否则返回false
。
- 功能:以二进制方式读取文件
- WriteFile
- 功能:以二进制方式将字符串
buf
的内容写入到文件filename
中,如果文件已存在则覆盖。 - 返回值:如果文件打开和写入成功,返回
true
;否则返回false
。
- 功能:以二进制方式将字符串
- UrlEncode
- 功能:对字符串
url
进行 URL 编码,可以选择是否将空格编码为+
。 - 返回值:返回编码后的字符串。
- 功能:对字符串
- HexToC
- 功能:将十六进制字符转换为对应的整数值。
- 返回值:返回转换后的整数值。
- UrlDecode
- 功能:对字符串
url
进行 URL 解码,可以选择是否将+
解码为空格。 - 返回值:返回解码后的字符串。
- 功能:对字符串
- StatuDesc
- 功能:根据给定的状态码
code
返回对应的状态描述。 - 返回值:返回状态描述字符串,如果状态码未知,则返回 “Unkonw”。
- 功能:根据给定的状态码
- ExtMime
- 功能:根据 URL 的扩展名返回对应的 MIME 类型。
- 返回值:返回 MIME 类型字符串,如果扩展名未知,则返回 “application/octet-stream”。
- IsLegPath
- 功能:检查字符串
path
是否是合法的路径,主要检查是否存在非法的 “…” 使用。 - 返回值:如果路径合法,返回
true
;否则返回false
。
- 功能:检查字符串
- IsDir
- 功能:检查给定的路径
dir
是否是一个目录。 - 返回值:如果是目录,返回
true
;否则返回false
。
- 功能:检查给定的路径
- IsRegular
- 功能:检查给定的路径
dir
是否是一个常规文件。 - 返回值:如果是常规文件,返回
true
;否则返回false
。
- 功能:检查给定的路径
// 公共方法类
class Util
{
public:static ssize_t SplitStr(const std::string &src, const std::string &sep, std::vector<std::string> &sub){// 根据sep分隔符切分字符串int offset = 0; // 偏移量while (offset < src.size()){size_t pos = src.find(sep, offset);// 没有找到sepif (pos == std::string::npos){// 直接将offset后的字符串当成子串sub.push_back(src.substr(offset));break;}// 找到了sepelse{size_t len = pos - offset;if (len == 0){offset++;continue;}sub.push_back(src.substr(offset, len));offset += len; // 偏移量向后移动}}return sub.size();}static bool ReadFile(const std::string &filename, std::string *buf){std::ifstream ifs(filename, std::ios::binary); // 以读方式打开文件,采取二进制读取方式if (ifs.is_open() == false){LOG(ERROR, "Open %s Failed!\n", filename.c_str());return false;}// 获取文件大小ifs.seekg(0, ifs.end); // 将读取位置移动到文件末尾size_t n = ifs.tellg(); // 此时的偏移量即为文件大小ifs.seekg(0, ifs.beg); // 将读取位置移动到到文件开头buf->resize(n); // 将缓冲区大小设置为文件大小// 进行写入ifs.read(&(*buf)[0], n);// 关闭文件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){LOG(ERROR, "Open %s Failed!\n", filename.c_str());return false;}// 进行写入ofs.write(&buf[0], buf.size());if (ofs.good() == false){LOG(ERROR, "Write %s Failed!\n", filename.c_str());return false;}ofs.close();return true;}static std::string UrlEncode(const std::string &url, bool is_space_encode){std::string ret;// 进行编码for (auto ch : url){//. - _ ~ 四个字符绝对不编码// 字母与数字不见编码if (ch == '.' || ch == '-' || ch == '_' || ch == '~' || isalnum(ch)){ret += ch;continue;}// 空格编码为 +if (ch == ' ' && is_space_encode){ret += '+';continue;}// 其余字符进行编码char buf[4]; // 编码格式 %___snprintf(buf, 4, "%%%02X", ch);ret += buf;}return ret;}// URL解码static char HexToC(char c){if (c >= '0' && c <= '9'){return c - '0';}else if (c >= 'a' && c <= 'z'){return c - 'a' + 10;}else if (c >= 'A' && c <= 'Z'){return c - 'A' + 10;}return -1;}static std::string UrlDecode(const std::string &url, bool is_space_decode){std::string res;// 遍历字符串 遇到%就进行解码for (int i = 0; i < url.size(); i++){if (url[i] == '%'){char v1 = HexToC(url[i + 1]);char v2 = HexToC(url[i + 2]);char c = (v1 << 4) + v2;res += c;i += 2;continue;}else if (url[i] == '+' && is_space_decode){res += ' ';continue;}else{res += url[i];}}return res;}// 返回状态码static std::string StatuDesc(int code){auto ret = _statu_msg.find(code);if (ret == _statu_msg.end()){return "Unkonw";}return ret->second;}// 解析文件后缀static std::string ExtMime(const std::string &url){size_t pos = url.rfind('.');// 没有找到返回if (pos == std::string::npos){LOG(DEBUG, "没有找到'.'\n");return "applicantion/octet-stream";}std::string str = url.substr(pos);LOG(DEBUG, "文件类型:%s\n", str.c_str());auto it = _mime_msg.find(str);if (it == _mime_msg.end()){return "applicantion/octet-stream";}return it->second;}// 检查是否是合法路径static bool IsLegPath(const std::string &path){// 采用计数法int level = 0;std::vector<std::string> subdir;int ret = SplitStr(path, "..", subdir);if (ret < 0)return false;for (auto &s : subdir){if (s == ".."){level--;if (level < 0)return false;continue;}elselevel++;}return true;}static bool IsDir(const std::string &dir){struct stat st;int n = ::stat(dir.c_str(), &st);if (n < 0)return false;return S_ISDIR(st.st_mode);}static bool IsRegular(const std::string &dir){struct stat st;int n = ::stat(dir.c_str(), &st);if (n < 0)return false;return S_ISREG(st.st_mode);}
};
3 HTTP协议
3.1 HTTP请求
http协议的请求格式是这样的:
- 请求行:包含请求方法,资源路径URL,HTTP版本
- 请求报头:以键值对的形式储存必要信息
- 空行:用于识别正文
- 请求正文:储存本次请求的正文
针对这个结构我们可以搭建一个HTTP请求的基础框架:
class
{
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; // 查询字符串};
然后继续设置一些接口:
- 插入头部字段的接口
- 检查请求中是否有该头部字段
- 插入查询字符串
- 检查请求中是否有该查询字符串
- 获取查询字符串
- 获取正文长度
- 是否为长连接
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 tmp;_matches.swap(tmp);_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){auto it = _params.find(key);if (it == _params.end()){return false;}return true;}// 获取查询字符串std::string GetParam(const std::string &key){auto it = _params.find(key);if (it == _params.end()){return "";}return it->second;}// 获取正文长度size_t ContentLength(){bool ret = HasHeader("Content-Length");if (ret){// 转换为长整形return std::stol(GetHeader("Content-Length"));}return 0;}bool Close() const{// 没有Connection字段或者Connection字段是close 就是短连接if (HasHeader("Connection") == true && GetHeader("Connection") == "close"){return true;}return false;}
};
这样一个基础的HTTP请求结构就设计好了!
3.2 HTTP应答
http协议的应答格式是这样的:
- 状态行:包含HTTP版本,状态码,状态码描述
- 应答报头:储存必要信息
- 换行符:用于识别正文
- 正文:储存应答的正文结构
根据应答结构,我们可以搭建其应答框架:
- 设置头部字段
- 获取头部字段
- 设置正文
- 设置应答状态
- 是否是长连接
class HttpResponse
{
public:int _statu; // 状态码bool _rediect_flag; // 重定向标志std::string _rediect_url; // 重定向的路径std::string _body; // 响应正文std::unordered_map<std::string, std::string> _headers; // 响应报头public:HttpResponse(int statu) : _statu(statu) {}// 重置响应void Reset(){}// 插入头部字段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("Content-Type", type);}void SetRediret(const std::string &url, int statu = 302){_statu = statu;_rediect_flag = true;_rediect_url = url;}bool Close(){// 没有Connection字段或者Connection字段是close 就是短连接if (HasHeader("Connection") == true && GetHeader("Connection") == "close"){return true;}return false;}
};
这样HTTP协议的请求与应答我们就完成了!可以进一步进行请求与应答的解析工作了!
4 上下文解析模块
针对应答的反序列化,我们不在协议模块中直接进行设置,因为我们无法保证连接一次就可以获取完整的报文结构,所以在一个连接中要维护一个上下文结构,可以在多次处理时知道本次处理应该从何处进行!
在这个上下文中首先我们就需要一个状态变量,可以标识当前应该处理什么字段:
RECV_HTTP_ERROR --- 处理出错RECV_HTTP_LINE --- 处理请求行RECV_HTTP_HEAD --- 处理头部字段RECV_HTTP_BODY --- 处理正文RECV_HTTP_OVER --- 处理完成
每一个上下文都匹配一个请求对象,将解析好的字段储存到这个请求对象中:
- 处理请求行:处理请求行时使用正则表达式快速进行处理,注意URL编码的转换,请求方法的大小写以及拆分出查询字符串!
- 处理头部字段:一行一行的进行处即可,直到遇到空行!
- 处理正文:从缓冲区读取出正文长度的数据,不够继续等待,够了就返回。
需要注意的是,获取数据时不一定会获取到预期的数据,一定要做好情况分类,保证正常读取!
避免出现数据过长,数据不足等情况!
上下文每次解析都将数据及时储存到该上下文中对应的请求对象中!
typedef enum
{RECV_HTTP_ERROR,RECV_HTTP_LINE,RECV_HTTP_HEAD,RECV_HTTP_BODY,RECV_HTTP_OVER
} HttpRecvStatu;static const int MAX_SIZE = 8192;class HttpContext
{
private:int _resp_statu; // 响应状态码HttpRequest _request; // 请求信息HttpRecvStatu _recv_statu; // 解析状态
private:bool ParseHttpLine(const std::string &line){// 对请求行进行正则表达式解析// 设置解析方法: 忽略大小写!// std::regex re("(GET|HEAD|POST|PUT|DELETE) ([^?]+)\\?(.*) (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);std::regex re("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);//(GET|HEAD|POST|PUT|DELETE) 获取GET...请求方法//([^?]+) 匹配若干个 非?字符 直到? --- 获取资源路径//\\?(.*) \\?表示匹配原始?字符 (.*)访问到空格 ---获取请求参数//(HTTP/1\\.[01]) 匹配HTTP/1. 01任意一个字符//(?:\n|\r\n)? 匹配\n或者\r\n (?: ...)表示匹配摸个格式字符串但是不提取 .结尾的?表示前面的表达式0次或1次std::smatch matches;bool ret = std::regex_match(line, matches, re);if (ret == false){LOG(ERROR, "regex_match failed\n");_resp_statu = 400; // Bad Reauest!return false;}// 0:GET /a/b/c/search?q=keyword&lang=en HTTP/1.1// 1:GET// 2:/a/b/c/search// 3:q=keyword&lang=en// 4:HTTP/1.1_request._method = matches[1];// 请求方法统一转换为大写std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);_request._path = Util::UrlDecode(matches[2], false);_request._version = matches[4];// 对查询字符串进行解析std::string str = matches[3];std::vector<std::string> substr;// 进行切分字符串Util::SplitStr(str, "&", substr);// 遍历容器for (auto s : substr){// 寻找'='size_t pos = s.find("=");if (pos == std::string::npos){LOG(ERROR, "ParseHttpLine Failed\n");_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD Resquestreturn false;}// 找到了 ‘=’std::string key = Util::UrlDecode(s.substr(0, pos), true);std::string value = Util::UrlDecode(s.substr(pos + 1), true);LOG(INFO, "查询字符串%s: %s\n", key.c_str(), value.c_str());_request.SetParam(key, value);}return true;}// 解析请求行bool RecvHttpLine(Buffer *buf){if (_recv_statu != RECV_HTTP_LINE)return false;// 获取一行数据 带有\r\nstd::string line = buf->GetLineAndPop();if (line.size() == 0){// 缓存区中没有完整的一行数据 进行分类讨论// 如果缓冲区数据大于极限值if (buf->ReadAbleSize() > MAX_SIZE){_resp_statu = 414; // URL TOO LONG_recv_statu = RECV_HTTP_ERROR;return false;}// 反之不处理return true;}// 一行的数据过长if (line.size() > MAX_SIZE){_resp_statu = 414; // URL TOO LONG_recv_statu = RECV_HTTP_ERROR;return 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;// 解析请求报头直到遇到空行while (1){std::string line = buf->GetLineAndPop();// LOG(DEBUG, "line:%s\n", line.c_str());if (line.size() == 0){// 缓存区中没有完整的一行数据 进行分类讨论// 如果缓冲区数据大于极限值if (buf->ReadAbleSize() > MAX_SIZE){// LOG(ERROR, "line too long\n");_resp_statu = 414; // URL TOO LONG_recv_statu = RECV_HTTP_ERROR;return false;}// 反之不处理 等待新数据到来// LOG(ERROR, "wait new buffer\n");return true;}// 一行的数据过长if (line.size() > MAX_SIZE){// LOG(ERROR, "line too long\n");_resp_statu = 414; // URL TOO LONG_recv_statu = RECV_HTTP_ERROR;return false;}if (line == "\n" || line == "\r\n"){// LOG(ERROR, "line is empty\n");break;}// LOG(INFO, "line正常 进行解析处理");// 去除换行 \r \nif (line.back() == '\n')line.pop_back();if (line.back() == '\r')line.pop_back();// 进行解析bool ret = ParseHttpHead(line);if (ret == false)return false;}// 头部解析完成 继续解析正文_recv_statu = RECV_HTTP_BODY;return true;}bool ParseHttpHead(const std::string &line){// 每一行都是key: val\r\n 格式// LOG(DEBUG, "ParseHttpHead:%s\n", line.c_str());// 进行解析即可size_t pos = line.find(": ");if (pos == std::string::npos){LOG(ERROR, "ParseHttpLine Failed\n");_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD Resquestreturn false;}std::string key = line.substr(0, pos);std::string val = line.substr(pos + 2);// LOG(DEBUG, "%s: %s\n", key.c_str(), val.c_str());_request.SetHeader(key, val);return true;}bool RecvHttpBody(Buffer *buf){if (_recv_statu != RECV_HTTP_BODY)return false;// 获取正文长度size_t len = _request.ContentLength();// 没有正文 直接读取完毕if (len == 0){_recv_statu = RECV_HTTP_OVER;return true;}// 当前已经接受了多少数据 _request._bodysize_t relen = len - _request._body.size();// 接收正文放到body中 但是要考虑当前缓冲区中的数据是否是全部的报文// 缓冲区数据包含所有正文if (relen <= buf->ReadAbleSize()){// 加到_request.body的后面_request._body.append(buf->ReadPos(), relen);buf->MoveReadOffset(relen);_recv_statu = RECV_HTTP_OVER;return true;}// 缓冲区无法满足正文_request._body.append(buf->ReadPos(), buf->ReadAbleSize());buf->MoveReadOffset(buf->ReadAbleSize());return true;}public:HttpContext() : _resp_statu(200), _recv_statu(RECV_HTTP_LINE) {}int RespStatu() { return _resp_statu; }HttpRequest &Request() { return _request; }HttpRecvStatu RecvStatu() { return _recv_statu; }// 重置上下文void Reset(){_resp_statu = 200;_recv_statu = RECV_HTTP_LINE;_request.Reset();}void RecvhttpRequest(Buffer *buf){// 根据不同的状态 处理不同情况// 处理完不要break 因为处理完 可以继续进行处理下面的数据 而不是直接退出等待新数据!switch (_recv_statu){case RECV_HTTP_LINE:RecvHttpLine(buf);case RECV_HTTP_HEAD:RecvHttpHead(buf);case RECV_HTTP_BODY:RecvHttpBody(buf);}return;}
};
5 HTTP服务器对象
现在,HTTP协议我们实现了,可以通过协议进行通信!如何通过缓冲区获取请求的上下文方法我们也实现了,可以在缓冲区中读取数据,即使一次没有发送全,下一次可以继续在原有进度上继续进行解析!
那么接下来,我们对这些功能进行一个整合封装,实现HTTP服务器的功能!
首先这个模块中有请求方法/资源路径 与 函数指针的映射关系表,可以根据http请求的url找到对应的资源
- 表中记录了对于哪个请求,应该使用哪一个函数来进行业务处理
- 当服务器收到一个请求,就要在请求路由表中,查找是否存在对应的处理函数,没有就返回404 Not Found
- 这样做的好处是用户只需要实现业务处理函数,然后将请求与函数的对应关系添加到服务器中,服务器只需要接收数据,解析数据,查找路由表映射关系,执行业务处理函数!
要实现简便的搭建Http服务器,所需的要素和提供的功能有以下几项:
- GET请求的路由映射表 — 功能性请求的处理
- POST请求的路由映射表
- PUT请求的路由映射表
- DELETE请求的路由映射表
- 高性能TCP服务器 — 进行连接的IO操作
- 静态资源相对根目录 — 实现静态资源的处理
再来看服务器的处理流程,只有熟悉了服务器处理流程,才能明白代码逻辑然后进行功能实现!
- 从Socket接收数据。放到接收缓冲区
- 调用OnMessage回调函数进行业务处理
- 对请求进行解析,得到一个HttpRequest结构,包含所有的请求要素
- 进行请求的路由查找 — 找到对应请求的处理方法
- 静态资源请求 — 一些实体文件资源的请求
- 功能性请求 — 在请求中根据路由映射表查找处理函数
- 对静态资源请求/功能性请求进行处理完毕后,得到了一个填充了响应信息的HttpReaponse对象,组成http格式报文
- GET请求的路由映射表
_get_route
— 通过正则表达式映射处理函数 - POST请求的路由映射表
_post_route
- PUT请求的路由映射表
_put_route
- DELETE请求的路由映射表
_delete_route
- 静态资源根目录
_basedir
- TcpServer服务器
_server
-
设置上下文 OnConnect(const PtrConn& conn):给连接设置空的上下文
-
缓冲区数据解析+处理 OnMessage(const PtrConn& conn , Buffer *buf):只要缓冲区里有数据就持续处理首先先获取上下文,通过上下文对缓冲区数据进行处理得到HttpRequest对象,根据状态码>= 400判断解析结果 ,如果解析出错 ,直接回复出错响应 ErrorHandler(req , rsp) 并关闭连接! 请求解析不完整 直接return 等待下一次处理。直到解析完毕 才去进行数据处理。然后进行请求路由Route(req ,&rsp) 在路由中进行数据处理业务处理,处理后得到应答报文,对HttpResponse 进行组织发送 WriteResponse(const PtrConn& conn , req , rsp)此时重置连接的上下文!根据长短连接判断是否要关闭连接或者继续保持连接
-
路由查找 Route:对请求进行判断,是请求静态资源还是功能性请求
- 静态资源请求 :判断是否是静态资源请求,然后进行静态资源的处理
- 功能性请求 : 通过req的请求方法判断使用哪一个路由表,使用Dispatch进行任务派发
- 既不是静态资源一般是功能性请求 就返回404!
-
判断是否是静态资源请求 IsFileHandler:首先必须设置了静态资源根目录,请求方法必须是GET / HEAD
,请求的资源路径必须是合法路径,请求的资源必须存在! 当请求路径是"/"要补全一个初始页面 index.html,注意合并_basedir得到真正的路径! -
静态资源的请求处理 FileHandler:将静态资源的数据读取出来,放到rsp的正文中,直接读取路径上的文件放到正文中,获取mime文件类型,添加到头部字段Content-Type!
-
功能性请求的任务分发 Dispatcher:在对应路由表中寻找是否有对应请求的处理函数,有就直接进行调用 没有就返回404。路由表中储存的是
正则表达式->处理函数
的键值对。使用正则表达式进行匹配 ,匹配成功就进行执行函数 -
发送应答WriteResponse:将HttpReaponse应答按照http应答格式进行组织发送 ,首先完善头部字段 ,然后将rsp的元素按照http协议的格式进行组织,最终发送数据
-
处理错误应答ErrorHandler: 提供一个错误展示页面,将页面数据当作响应正文放入rsp中
- 构造函数
- 插入关系映射到GET路由表、POST路由表、PUT路由表、DELETE路由表。
- 设置静态资源根目录
- 设置线程数量
- 启动Http服务器
class HttpServer
{
private:using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;using Handlers = std::vector<std::pair<std::regex, Handler>>;Handlers _get_route; // GET方法处理函数映射表Handlers _post_route; // POST方法处理函数映射表Handlers _delete_route; // DELETE方法处理函数映射表Handlers _put_route; // PUT方法处理函数映射表std::string _basedir;TcpServer _server;public:// 设置空白上下文void OnConnect(const PtrConn &conn){conn->SetContext(HttpContext());LOG(INFO, "NEW CONNECTION :%p\n", this);}void ErrorHandler(const HttpRequest &req, HttpResponse *rsp){// 提供一个错误展示页面std::string body;body += "<!DOCTYPE html>";body += "<html lang='en'>";body += "<head>";body += "<meta charset='UTF-8'>";body += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";body += "<title>Error " + std::to_string(rsp->_statu) + " - Server Error</title>";body += "<style>";body += "body { background-color: #f2f2f2; color: #333; font-family: Arial, sans-serif; }";body += "h1 { color: #d8000c; background-color: #ffbaba; border: 1px solid #d8d8d8; padding: 10px; text-align: center; }";body += "div.container { max-width: 600px; margin: 50px auto; padding: 20px; background-color: #fff; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }";body += "</style>";body += "</head>";body += "<body>";body += "<div class='container'>";body += "<h1>";body += "Error " + std::to_string(rsp->_statu) + " - " + Util::StatuDesc(rsp->_statu);body += "</h1>";body += "<p>We're sorry, but something went wrong.</p>";body += "</div>";body += "</body>";body += "</html>";// 将页面数据,当作响应正文,放入rsp中rsp->SetContent(body, "text/html");}// 缓冲区数据解析+处理void OnMessage(const PtrConn &conn, Buffer *buf){while (buf->ReadAbleSize() > 0){// 从连接中获取上下文HttpContext *context = conn->GetContext()->Get<HttpContext>();// 从缓冲区中获取数据 处理后得到Requestcontext->RecvhttpRequest(buf);HttpRequest req = context->Request();// 根据请求构建应答HttpResponse rsp(context->RespStatu());// 根据状态码判断处理结果// LOG(DEBUG, "res->statu :%d\n", rsp._statu);// 状态码大于400说明解析出错 直接退出if (context->RespStatu() >= 400){// 重置上下文context->Reset();// 清空缓冲区buf->MoveReadOffset(buf->ReadAbleSize());// 获取错误响应ErrorHandler(req, &rsp);// 发送错误请求WriteResponse(conn, req, rsp);// 关闭连接conn->Shutdown();return;}// 如果解析没有完成就等待下一次处理if (context->RecvStatu() != RECV_HTTP_OVER){// 退出等待新数据到来 重新进行处理return;}// 请求解析完成进行处理Route(req, &rsp);LOG(INFO, "%s\n", rsp._body.c_str());if (rsp._statu >= 400){// 获取错误响应ErrorHandler(req, &rsp);// 发送错误请求WriteResponse(conn, req, rsp);// 重置上下文context->Reset();// 关闭连接conn->Shutdown();return;}// 获取应答WriteResponse(conn, req, rsp);// 重置上下文context->Reset();// 根据长短连接判断是否需要关闭连接if (rsp.Close() == true)conn->Shutdown();}return;}bool Route(HttpRequest &req, HttpResponse *rsp){// 判断是否是静态资源处理if (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);// 不是静态请求也不是功能性请求else{rsp->_statu = 405; // Method Not Allowedreturn false;}}// 判断是否是静态资源bool IsFileHandler(HttpRequest &req){// 首先_basedir必须存在if (_basedir.empty() == true)return false;// 请求方法必须是 GET / HEADif (req._method != "GET" && req._method != "HEAD")return false;// 请求路径必须是合法路径if (Util::IsLegPath(req._path) == false)return false;// 请求的资源必须存在std::string req_path = _basedir + req._path;// 如果直接请求的网络根目录要补全一个初始页面if (req_path.back() == '/')req_path += "index.html";if (Util::IsRegular(req_path) == false)return false;// req请求路径的真正路径req._path = req_path;return true;}// 静态资源的请求处理bool FileHandler(HttpRequest &req, HttpResponse *rsp){LOG(INFO, "静态资源请求:%s\n", req._path.c_str());// 将请求资源读取到应答正文中bool ret = Util::ReadFile(req._path, &rsp->_body);if (ret == false){// 数据读取失败LOG(ERROR, "数据读取失败\n");return false;}// 获取文件类型mimestd::string mime = Util::ExtMime(req._path);LOG(DEBUG, "Content-Type:%s\n", mime.c_str());// 添加到应答报头rsp->SetHeader("Content-Type", mime);return true;}// 功能性请求的任务分发bool Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers){// LOG(INFO, "%s 功能性请求:%s\n", req._method.c_str(), req._path.c_str());// 首先根据路由表找到目标for (auto &handler : handlers){const std::regex &re = handler.first;// 根据这个正则表达式进行解析bool ret = std::regex_match(req._path, req._matches, re);if (ret == false)continue;// 找到了就进行执行函数Handler Functor = handler.second;Functor(req, rsp);return true;}// 没有找到目标LOG(DEBUG, "404 Not Found\n");rsp->_statu = 404; // 设置为Not Foundreturn false;}// 将HttpReaponse应答按照http应答格式进行组织发送void WriteResponse(const PtrConn &conn, const HttpRequest &req, HttpResponse &rsp){// 首先先完善头部字段if (req.Close() == true)rsp.SetHeader("Connection", "close");elsersp.SetHeader("Connection", "keep-alive");if (rsp._body.empty() == true && rsp.HasHeader("Content-Length") == false)rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));if (rsp._body.empty() == true && rsp.HasHeader("Content-Type") == false)rsp.SetHeader("Content-Type", "application/octet-stream");if (rsp._rediect_flag == true)rsp.SetHeader("Location", rsp._rediect_url);// 将rsp组织成http格式的应答报文std::stringstream rsp_str;rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(rsp._statu) << "\r\n";for (auto &it : rsp._headers){rsp_str << it.first << ": " << it.second << "\r\n";}rsp_str << "\r\n";rsp_str << rsp._body << "\r\n";// 进行发送// LOG(INFO, "WriteResponse Send :%s \n", rsp_str.str().c_str());conn->Send(rsp_str.str().c_str(), rsp_str.str().size());}public:HttpServer(int port, int timeout = DEFALT_TIMEOUT) : _server(port){_server.SetConnectCB(std::bind(&HttpServer::OnConnect, this, std::placeholders::_1));_server.SetMessageCB(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));_server.EnableActiveRelease(timeout); // 设置超时时间}// 插入关系映射到GET路由表void GET(const std::string &pattern, const Handler &func) { _get_route.push_back(std::make_pair(std::regex(pattern), func)); }void POST(const std::string &pattern, const Handler &func) { _post_route.push_back(std::make_pair(std::regex(pattern), func)); }void PUT(const std::string &pattern, const Handler &func) { _put_route.push_back(std::make_pair(std::regex(pattern), func)); }void DELETE(const std::string &pattern, const Handler &func) { _delete_route.push_back(std::make_pair(std::regex(pattern), func)); }void SetBaseDir(const std::string &dir){assert(Util::IsDir(dir) == true);_basedir = dir;}// 设置服务器线程数量void SetThreadSize(size_t size){_server.SetThreadSize(size);}// 启动服务器void Start(){_server.Start();}
};
相关文章:
【项目日记】仿mudou的高并发服务器 --- 实现HTTP服务器
对于生命,你不妨大胆一点, 因为我们始终要失去它。 --- 尼采 --- ✨✨✨项目地址在这里 ✨✨✨ ✨✨✨https://gitee.com/penggli_2_0/TcpServer✨✨✨ 仿mudou的高并发服务器 1 前言2 Util工具类3 HTTP协议3.1 HTTP请求3.2 HTTP应答 4 上下文解析模块…...
【SpringBoot问题】IDEA中用Service窗口展示所有服务及端口的办法
1、调出Service窗口 打开View→Tool Windows→Service,即可显示。 2、正常情况应该已经出现SpringBoot,如下图请继续第三步 3、配置Service窗口的项目启动类型。微服务一般是Springboot类型。所以这里需要选择一下。 点击最后一个号,点击Ru…...
Ubuntu20.04运行LARVIO
文章目录 1.运行 Toyish 示例程序2.运行 ROS Nodelet参考 1.运行 Toyish 示例程序 LARVIO 提供了一个简化的toyish示例程序,适合快速验证和测试。 编译项目 进入 build 文件夹并通过 CMake 编译项目: mkdir build cd build cmake -D CMAKE_BUILD_TYPER…...
数字化转型背景下,高职院校计算机网络应用的革新策略
在当今信息化时代,计算机网络已经成为高职院校教育不可或缺的一部分,它不仅极大地丰富了教育资源,提高了交流的便捷性,还催生了多样化的教学模式。对于高职院校来说,加强计算机网络应用的建设不仅是顺应时代潮流的必然…...
mysql 里面的主表和子表是什么?如何创建主表和子表的关系
在MySQL数据库中,主表和子表的概念是基于表间关系的。它们通常通过外键约束来建立联系,这种关系有助于维护数据的完整性和一致性。以下是对MySQL中主表和子表的详细解释: 主表(父表) 定义:主表,…...
工程企业的成本管理系统软件应该有哪些特点?
工程企业的成本管理系统软件需要兼顾工程项目的复杂性和动态性,其功能特点应服务于成本核算、监控、优化与分析全生命周期管理,以下是关键特点: 一、核心功能特点 1. 全生命周期成本管理 覆盖范围: 从项目立项、投标、预算编制&…...
【前端开发】实战:课表安排(HTML + JavaScript + Vue3 + Vant)
后端开发 主要定义了三个核心部分:每周周期(WeekDays)、每天节次(TimeLessons) 和 每天节次详情(Details) 每周周期(WeekDays) 存储了每周的七天(通常是从周一…...
nodejs建立TCP服务器端和TCP客户端之间的连接
TCP服务器端,看名字也知道是建立在服务器上面的 //获取模块 const net require(net); //创建server服务器 const servernet.createServer(); //与客户端建立连接 server.on(connection,function(socket){console.log(客户端与服务器连接已经建立);//socket是客户端…...
Pytorch使用手册-What is torch.nn really?(专题九)
我们建议将本教程作为 notebook 而不是脚本运行。要下载 notebook(.ipynb)文件,请点击页面顶部的链接。 PyTorch 提供了精心设计的模块和类,如 torch.nn、torch.optim、Dataset 和 DataLoader,帮助你创建和训练神经网络。为了充分利用这些工具的强大功能并根据你的问题进…...
ADAM优化算法与学习率调度器:深度学习中的关键工具
深度学习模型的训练效果离不开优化算法和学习率的选择。ADAM(Adaptive Moment Estimation)作为深度学习领域中广泛应用的优化算法之一,以其高效性和鲁棒性成为许多任务的默认选择。而学习率调度器则是优化算法的“助推器”,帮助训…...
黑马2024AI+JavaWeb开发入门Day03-Maven-单元测试飞书作业
视频地址:哔哩哔哩 讲义作业飞书地址:飞书 作业比较简单,随便写了写 package org.example;import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.ju…...
TouchGFX设计模式代码实例说明
一)Model - View - Presenter (MVP) 模式在 TouchGFX 中的应用 1)Model(模型): 模型代表应用程序的数据和业务逻辑。例如,在一个简单的计数器应用中,模型可以是一个包含计数器当前值的类。 class CounterModel { pri…...
Docker扩容操作(docker总是空间不足)
Docker扩容操作(docker总是空间不足) 1、df二连,一共也就70g,总是占满93%以上。所以需要移动到其他目录上 查看docker镜像和容器存储目录的空间大小 du -sh /var/lib/docker/2、停止docker服务 systemctl stop docker3、首先创建目录并迁移 # 首先创…...
ELK Fleet JAVA LOG收集与展示教程
目录 elastic-fleet架构 Elastic Agent的优势 Fleet JAVA日志收集流程 1.注册Fleet Sever 2.创建JAVA代理收集策略Agent Policy 3.添加集成 integration 4.调整Java log输出格式 5.添加Elastic Agent 6.添加Ingest 7.创建数据视图 8.其他Policy nginx-policy syst…...
【Python网络爬虫笔记】2-HTTP协议中网络爬虫需要的请求头和响应头内容
1 HTTP 协议整理 HTTP(Hyper Text Transfer Protocol)即超文本传输协议,是用于从万维网(WWW)服务器传输超文本到本地浏览器的传送协议,直白点儿,就是浏览器和服务器之间的数据交互就是通过 HTT…...
TiDB 调度概述
PD 是 TiDB 集群的管理模块,同时也负责集群数据的实时调度。本文档介绍一下 PD 的设计思想和关键概念。 场景描述 TiKV 集群是 TiDB 数据库的分布式 KV 存储引擎,数据以 Region 为单位进行复制和管理,每个 Region 会有多个 Replica…...
MySQL的Json类型数据操作方法
前言 MySQL在5.7增加了json类型数据,项目中最近有在使用。直接使用JSONObject或者JSONArray类型的时候会报以下的异常。发现需要将Java实体字段设置为String类型,然后需要的时候自己转换为JSONObject或者JSONArray。使用很不方便,经研究发现…...
论文笔记(五十九)A survey of robot manipulation in contact
A survey of robot manipulation in contact 文章概括摘要1. 引言解释柔顺性控制的概念:应用实例: 2. 需要接触操控的任务2.1 环境塑造2.2 工件对齐2.3 关节运动2.4 双臂接触操控 3. 接触操控中的控制3.1 力控制3.2 阻抗控制3.3 顺应控制 4. 接触操控中的…...
基础原型链污染
<aside> 💡 引用类型皆为对象 </aside> 原型和原型链都是来源于对象而服务于对象的概念,所以我们要先明确一点: JavaScript中一切引用类型都是对象,对象就是属性的集合。 Array类型、Function类型、Object类型、…...
nginx 升级http 到 http2
同步发布于我的网站 🚀 背景介绍准备工作配置过程遇到的问题及解决方法验证升级总结参考资料 背景介绍 HTTP/2 是 HTTP 协议的最新版本,相比 HTTP/1.1,它带来了多项重要的改进,包括多路复用、头部压缩和服务端推送。这些特性可…...
XPath表达式详解及其在Web开发中的应用
XPath(XML Path Language)是一种强大的查询语言,用于在XML文档中选择节点。由于HTML可以被视为一种特殊的XML,因此XPath同样适用于HTML文档。XPath允许开发者通过元素的层级结构和属性来选择节点或节点集合,这使得它成…...
云服务器进行安全防护的必要性
在当今这个数字化时代,云计算已成为企业运营不可或缺的一部分,而云服务器作为云计算的核心基础设施,承载着数据存储、应用部署、业务运行等多重关键任务。随着企业数字化转型的深入,云服务器上的数据量激增,业务逻辑日…...
win10系统安装docker-desktop
1、开启Hyper-v ———————————————— Hyper-V 是微软提供的一种虚拟化技术,它允许你在同一台物理计算机上运行多个独立的操作系统实例。这种技术主要用于开发、测试、以及服务器虚拟化等领域。 —————————————————————— &#…...
video.js 禁用单击暂停
video.js 默认效果是单击播放区域暂停/播放 需求:要实现单击播放区禁止暂停/播放功能 有其他的点击效果需要实现 会导致俩功能有冲突 作者给出答案:如下 .vjs-tech {pointer-events: none; } 确实管用,想了很多阻止的办法,都没这个来的快...
【二维动态规划:交错字符串】
介绍 编程语言:Java 本篇介绍一道比较经典的二维动态规划题。 交错字符串 主要说明几点: 为什么双指针解不了?为什么是二维动态规划?根据题意分析处转移方程。严格位置依赖和空间压缩优化。 题目介绍 题意有点抽象,…...
如何在CentOS 7上安全地设置Apache网站目录权限
一、概述 在CentOS 7上运行Apache Web服务器时,正确设置文件和目录的权限对于确保网站的安全性和正常运行至关重要。本文将介绍如何为Apache网站目录(例如/var/www/html/)设置合适的权限,以平衡安全性和功能性需求。 二、所有权 …...
Makefile 入门指南:构建自动化编译流程
个人主页:chian-ocean 文章专栏 前言 make 和 Makefile 是编译和构建软件项目时非常常用的工具和文件,它们通常配合使用来自动化项目的编译过程。 make 定义:make 是一个构建自动化工具,用于根据项目文件的依赖关系自动完成编译…...
TransVG 代码配置及一些小细节
TransVG代码配置 File “/home/wyq/TransVG/utils/misc.py”, line 22, in <module> from torchvision.ops import _new_empty_tensor ImportError: cannot import name ‘_new_empty_tensor’ if float(torchvision.__version__[:3]) < 0.7: # torchvision.__version…...
DIY-Tomcat part 3 实现对动态资源的请求
实现ServletRequest package connector;import javax.servlet.RequestDispatcher; import javax.servlet.ServletInputStream; import javax.servlet.ServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.i…...
在鲲鹏麒麟服务器上部署MySQL主从集群
因项目需求需要部署主从MySQL集群,继续采用上次的部署的MySQL镜像arm64v8/mysql:latest,版本信息为v8.1.0。计划部署服务器192.168.31.100和192.168.31.101 部署MySQL主节点 在192.168.31.100上先创建好/data/docker/mysql/data和/data/docker/mysql/l…...
K8S版本和istio版本的对照关系
版本对照关系 下载地址1 下载地址2...
省级新质生产力数据(蔡湘杰版本)2012-2022年
测算方式:参考《当代经济管理》蔡湘杰(2024)老师研究的做法,本文以劳动者、劳动对象和劳动资料为准则层,从新质生产力“量的积累、质的提升、新的拓展”三维目标出发,构建新质生产力综合评价指标体系&#…...
速盾:介绍一下高防cdn的缓存响应事什么功能?
高防CDN(Content Delivery Network)是一种基于分布式缓存技术的网络加速服务,能够提供强大的缓存响应功能。它的缓存响应功能主要包括缓存加速和智能缓存两个方面。 首先,高防CDN的缓存加速功能是指通过在全球范围内部署大量的缓…...
如何解决服务器扫描出的ASP木马问题
随着互联网的发展,网站安全问题日益凸显。其中,ASP(Active Server Pages)木马因其隐蔽性和危害性成为攻击者常用的手段之一。本文将详细介绍如何检测和清除服务器上的ASP木马,以保障网站的安全。 1. ASP木马概述 ASP…...
CTF之WEB(sqlmap tamper 参数)
apostropheask.py 作用:将单引号替换为UTF-8,用于过滤单引号。 base64encode.py 作用:替换为base64编码。 multiplespaces.py 作用:绕过SQL关键字添加多个空格。 space2plus.py 作用:用号替换…...
小程序-基于java+SpringBoot+Vue的乡村研学旅行平台设计与实现
项目运行 1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。 2.IDE环境:IDEA,Eclipse,Myeclipse都可以。推荐IDEA; 3.tomcat环境:Tomcat 7.x,8.x,9.x版本均可 4.硬件环境:…...
力扣81:搜索旋转排序数组II
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。 在传递给函数之前,nums 在预先未知的某个下标 k(0 < k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k1], ..., nums[n-1], n…...
力扣题库Day4(持续更新中...)
2024/11/29 回文数: 给你一个整数x,如果x是一个回文整数,返回true;否则,返回false。 回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。 class Solution {public boolean isPalindrome(int x) {if(x &l…...
MySQL数据库表的操作
1、总述 今天我跟大家分享MySQL数据库中表的创建,查看,修改,删除。 2、创建表 create table table_name ( field1 datatype, field2 datatype, field3 datatype ) character set 字符集 collate 校验规则 engine 存储引擎; 说明࿱…...
Java阶段三05
第3章-第5节 一、知识点 动态代理、jdk动态代理、cglib动态代理、AOP、SpringAOP 二、目标 理解什么是动态代理和它的作用 学会使用JAVA进行动态代理 理解什么是AOP 学会使用AOP 理解什么是AOP的切入点 三、内容分析 重点 理解什么是动态代理和它的作用 理解什么是AO…...
【论文复现】LeNet-5
📝个人主页🌹:Eternity._ 🌹🌹期待您的关注 🌹🌹 ❀ LeNet-5 概述LeNet-5网络架构介绍使用 LeNet-5 网络结构创建 MNIST 手写数字识别分类器下载并加载数据,并对数据进行预处理搭建 …...
Python-链表数据结构学习(1)
一、什么是链表数据? 链表是一种通过指针串联在一起的数据结构,每个节点由2部分组成,一个是数据域,一个是指针域(存放下一个节点的指针)。最后一个节点的指针域指向null(空指针的意思࿰…...
nginx安装和负载均衡
1. nginx安装 (1)安装依赖项: yum -y install gcc gcc-c make libtool zlib zlib-devel openssl openssl-devel pcre pcre-devel(2)下载Nginx源代码: http://nginx.org/en/download.html https://nginx.o…...
Buffered 和 BuffWrite
Buffered和BuffWrite是Java IO包中的两个类,用于提高IO操作的效率。 Buffered是一个缓冲区类,可以将一个InputStream或者一个Reader包装起来,提供了一定的缓冲区大小,可以一次读取多个字节或字符,减少了读取的次数&am…...
第三十八篇——高斯分布:大概率事件意味着什么?
目录 一、背景介绍二、思路&方案三、过程1.思维导图2.文章中经典的句子理解3.学习之后对于投资市场的理解4.通过这篇文章结合我知道的东西我能想到什么? 四、总结五、升华 一、背景介绍 通过高斯分布的公式,我们可以从科学的角度知道很多和我们的直…...
Ubuntu24.04初始化教程(包含基础优化、ros2)
目录 构建系统建立系统备份**Timeshift: 系统快照和备份工具****安装 Timeshift****使用 Timeshift 创建快照****还原快照****自动创建快照** 最基本配置换源 软件配置打开新世界大门 谷歌浏览器星火应用商城更换输入法安装vscode 完全删除snap删除**删除软件****彻底删除**禁止…...
C/C++ 数据结构与算法 【时间复杂度和空间复杂度】【日常学习,考研必备】
一、时间复杂度 定义:时间复杂度描述了算法运行时间随输入大小增长而增长的趋势。它主要关注的是算法中最耗时的部分,并忽略常数因子、低阶项等细节。表示方法:通常使用大O符号(Big O notation)来表示时间复杂度。例如…...
brew安装mongodb和php-mongodb扩展新手教程
1、首先保证macos下成功安装了Homebrew, 在终端输入如下命令: brew search mongodb 搜索是不是有mongodb资源, 演示效果如下: 2、下面来介绍Brew 安装 MongoDB,代码如下: brew tap mongodb/brew brew in…...
使用zabbix监控k8s
一、 参考文献 小阿轩yx-案例:Zabbix监控kubernetes云原生环境 手把手教你实现zabbix对Kubernetes的监控 二、部署经验 关于zabbix监控k8s,总体来说是分为两块内容,一是在k8s集群部署zabbix-agent和zabbix- proxy。二是在zabbix进行配置。…...
Brain.js(二):项目集成方式详解——npm、cdn、下载、源码构建
Brain.js 是一个强大且易用的 JavaScript 神经网络库,适用于前端和 Node.js 环境,帮助开发者轻松实现机器学习功能。 在前文Brain.js(一):可以在浏览器运行的、默认GPU加速的神经网络库概要介绍-发展历程和使用场景中&…...