【项目】仿muduo库one thread one loop式并发服务器HTTP协议模块
📚 博主的专栏
🐧 Linux | 🖥️ C++ | 📊 数据结构 | 💡C++ 算法 | 🅒 C 语言 | 🌐 计算机网络 |🗃️ mysql
本项目的Server功能模块可以看我的前三篇文章,有详细讲解每个子模块的设计与实现
项目文章
仿muduo库one thread one loop式并发服务器前置知识准备
仿muduo库one thread one loop式并发服务器前置(上)
仿muduo库one thread one loop式并发服务器SERVER模块(下)
摘要:本文详细介绍了一个基于仿muduo库的HTTP服务器实现方案。该系统采用one-thread-one-loop并发模型,包含HTTP协议模块、请求/响应处理模块、上下文管理模块和服务器核心模块。文章重点解析了Util工具类(提供URL编解码、文件操作等功能)、HttpRequest/HttpResponse模块(存储请求/响应数据)、HttpContext(管理请求解析状态)以及HttpServer(路由分发和静态资源处理)的实现细节。通过7种测试案例验证了服务器在长连接、超时处理、并发访问和大文件传输等场景下的可靠性,服务端与压测工具部署于同一虚拟机,通过
localhost
发起 HTTP/1.0 短连接请求。在 5,000 并发用户持续压测 60 秒 的条件下达到5369 QPS的吞吐量。该系统提供了简洁的API接口,便于开发者快速搭建高性能HTTP服务
目录
A.各模块介绍 :
一、Util模块
1.1 Util工具类功能设计与类设计
1.2 Util工具类字符串分割函数Split()实现
1.3 Util工具类文件数据读取函数实现
1.4 Util工具类文件数据写入函数实现
1.5 Util工具类Urlencode函数实现
1.6 Util工具类Urldecode函数实现
1.7 Util工具类Mime与Statu
1.8 Util工具类文件类型判断接口实现
1.9 Util工具类文件路径有效性判断接口实现
二、HttpRequest模块
2.1 HttpRequest模块功能设计
2.2 HttpRequest模块类设计与实现
2.3 HttpRequest模块类实现
三、HttpResponse模块
3.1 HttpResponse模块功能设计
3.2 HttpResponse模块类设计与实现
四、HttpContext模块
4.1 HttpContext(上下文类)模块功能设计
4.2 HttpContext模块类设计与实现
五、HttpServer模块
5.1 HttpServer模块功能设计
5.2 HttpServer模块类设计
5.3 HttpServer模块类实现
六、基于HttpServer搭建HTTP服务器以及7种测试
6.1 长连接测试
6.2 HTTP 服务器超时连接测试
6.3 HTTP 服务器错误请求测试
6.4 HTTP 服务器业务处理超时测试
6.5 HTTP 服务器同时多条请求测试
6.6 HTTP 服务器大文件传输测试
6.7 HTTP 服务器性能压力测试说明-WebBench
A.各模块介绍 :
HTTP协议模块为高并发服务器提供协议支持,简化了特定协议服务器的搭建过程。
该模块的具体实现包括以下子模块:
Util模块:
该模块提供HTTP协议所需的常用工具函数,主要包括URL编解码、文件读写等功能。
HttpRequest模块:
该模块用于存储解析后的HTTP请求数据,保存请求的各项元素信息。
HttpResponse模块:
这是一个HTTP响应数据模块,用于在业务处理后设置和保存HTTP响应数据的各项元素信息,最终按HTTP协议规范组织成响应信息发送给客户端。
HttpContext模块:
作为HTTP请求接收的上下文管理模块,其主要作用是在接收不完整HTTP请求时维护解析状态。当单次接收的数据不足构成完整请求时,解析流程将被暂停,待后续接收到新数据后继续根据上下文完成解析,最终生成HttpRequest请求对象。该模块通过上下文控制机制,确保请求数据的接收和解析过程有序进行。
HttpServer模块:
HttpServer模块是为组件使用者提供的最终HTTP服务器实现,通过简洁的接口即可快速搭建HTTP服务。其内部架构包含以下核心组件:
TcpServer对象
- 负责底层服务器的具体搭建
双向接口
- 连接建立时的上下文设置接口
- 请求数据处理接口
请求映射表
- 采用hash-map结构存储请求与处理函数的映射关系
- 开发者预先注册请求对应的处理函数
- TcpServer接收到请求时自动调用匹配的处理函数
可以看看这几篇相关文章:
协议解析与C++编写Http实现客户端服务器端的发送请求与响应
HTTP协议重定向、请求方法与会话管理C++实现
一、Util模块
1.1 Util工具类功能设计与类设计
class Util
{
public:// 字符串分割函数static size_t Split();// 读取文件内容static bool ReadFile();// 向文件写入数据static bool Writeile();// URL编码static std::string UrlEncode(const std::string &url, bool convert_space_to_plus)// URL解码static std::string UrlDecode();// 响应状态码的描述信息获取static std::string StatuDesc();// 根据文件后缀名,获取文件mimestatic std::string ExtMime();// 判断一个文件是否是一个目录static bool IsDirectory();// 判断一个文件时否是一个普通文件static bool IsRegulerFile();// HTTP请求的资源路径有效性判断static bool ValidPath();
};
1.2 Util工具类字符串分割函数Split()实现
// 字符串分割函数:将src字符串按照sep字符进行分割,得到的各个子串放到array中,最终返回子串得数量static size_t Split(const std::string &src, const std::string &sep, std::vector<std::string> *array){size_t offset = 0; // 5while (offset < src.size()) // aa,b,{// 在src字符串偏移量offset出开始查找sep字符或子串,返回查找到的位置size_t pos = src.find(sep, offset); // 2if (pos == std::string::npos) // 代表没有找到特定的字符{// 将剩余的部分当做一个子串,放入array中if (pos == src.size() - 1)break;array->push_back(src.substr(offset));return array->size();}// aaa,bbb,ccc,if (pos == offset) // 当前子串为空{offset = pos + sep.size();continue;}array->push_back(src.substr(offset, pos - offset));offset = pos + sep.size();}return array->size();}
//写完之后可以用这个做测试
int main()
{std::string str = "aa"; //"aa,"、"aa,,,,b,"std::string sep = ",";std::vector<std::string> array;size_t ret = Split(str, sep, &array);for (auto &s : array){std::cout << "[" << s << "]" << std::endl;}std::cout << "子串数:" << ret << std::endl;return 0;
}
1.3 Util工具类文件数据读取函数实现
cplusplus.com/reference/fstream/ifstream/
// 读取文件内容:将读取的内容放到一个buffer中
static bool ReadFile(const std::string &filename, std::string *buf)
{std::ifstream ifs(filename, std::ios::binary);if (ifs.is_open() == false){ERR_LOG("OPEN %s FILE FAILED...", filename.c_str());return false;}int fsize = 0;ifs.seekg(0, ifs.end); // seekg跳转到末尾fsize = ifs.tellg(); // 获取到文件大小ifs.seekg(0, ifs.beg); // 跳转到起始位置std::string str;buf->resize(fsize);ifs.read(&(*buf)[0], fsize);if (ifs.good() == false){ERR_LOG("READ %s FILE FAILED...", filename.c_str());ifs.close();return false;}ifs.close();return true;
}int main()
{std::string buf;bool ret = ReadFile("eventfd.cpp", &buf);if (ret == false){return 1;}std::cout << buf << std::endl;return 0;
}
1.4 Util工具类文件数据写入函数实现
cplusplus.com/reference/fstream/ofstream/
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){ERR_LOG("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;
}
int main()
{std::string buf;bool ret = ReadFile("eventfd.cpp", &buf); if (ret == false){return 1;}ret = WriteFile("./HAHA.txt", buf);if (ret == false){return 1;}return 0;
}
现在生成了一个HAHA.txt的文件在当前目录下,内容和eventfd.cpp相同,验证是否相同。使用命令:md5sum,这个字符串完全相同,因此内容相同
1.5 Util工具类Urlencode函数实现
在RFC3986文档中规定的URL绝对不编码字符:. - _ ~ 以及字母和数字
在RFC3986文档中规定,编码格式 %HH
还有一个就是在不同的一些标准中的特殊处理:
W3C标准规定中规定param(查询字符串)中的空格必须被编码为+,解码则是:+转空格。
RFC 2396中规定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 == '~' || isalnum(c)){res += c;continue;}if (c == ' ' && convert_space_to_plus){res += '+';continue;}// 剩下的字符都是需要编码成为 %HH 格式char tmp[4] = {0};// snprintf 与 printf比较类似,只不过,都是格式化字符,只不过一个是打印,一个是放到一块空间中snprintf(tmp, 4, "%%%02X", c);res += tmp;}return res;
}
int main()
{std::string str = "C+ +";std::string res = UrlEncode(str, true);std::cout << res << std::endl;return 0;
}
1.6 Util工具类Urldecode函数实现
static int HEXTOI(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 convert_space_to_plus)
{std::string res;for (int i = 0; i < url.size(); i++){if (url[i] == '+' && convert_space_to_plus){res += ' ';continue;}if (url[i] == '%' && (i + 2) < url.size()){char v1 = HEXTOI(url[i + 1]);char v2 = HEXTOI(url[i + 2]);char v = v1 * 16 + v2;res += v;i += 2;continue;}res += url[i];}return res;
}
int main()
{std::string str = "C+ ";std::string res = UrlEncode(str, true);std::cout << res << std::endl;std::string res1 = UrlDecode(res, true);std::cout << res1 << std::endl;
}
1.7 Util工具类Mime与Statu
#include <sys/stat.h>static std::string StatuDesc(int statu)
{std::unordered_map<int, std::string> _status_msg = {{100, "Continue"},{101, "Switching Protocols"},{102, "Processing"},{200, "OK"},{201, "Created"},{202, "Accepted"},{203, "Non-Authoritative Information"},{204, "No Content"},{205, "Reset Content"},{206, "Partial Content"},{207, "Multi-Status"},{300, "Multiple Choices"},{301, "Moved Permanently"},{302, "Move Temporarily"},{303, "See Other"},{304, "Not Modified"},{305, "Use Proxy"},{306, "Switch Proxy"},{307, "Temporary 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 Time-out"},{409, "Conflict"},{410, "Gone"},{411, "Length Required"},{412, "Precondition Failed"},{413, "Request Entity Too Large"},{414, "Request-URI Too Large"},{415, "Unsupported Media Type"},{416, "Requested 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"},{449, "Retry With"},{451, "Unavailable For Legal Reasons"},{500, "Internal Server Error"},{501, "Not Implemented"},{502, "Bad Gateway"},{503, "Service Unavailable"},{504, "Gateway Timeout"},{505, "HTTP Version Not Supported"},{506, "Variant Also Negotiates"},{507, "Insufficient Storage"},{509, "Bandwidth Limit Exceeded"},{510, "Not Extended"},{600, "Unparseable Response Headers"}};auto it = _status_msg.find(statu);if (it != _status_msg.end()){return it->second;}return "unknown";
}static 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"},{".mp4", "video/mp4"},{".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"},{".opus", "audio/opus"},{".otf", "font/otf"},{".png", "image/png"},{".pdf", "application/pdf"},{".php", "application/x-httpd-php"},{".ppt", "application/vnd.ms-powerpoint"},{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},{".rar", "application/vnd.rar"},{".rtf", "application/rtf"},{".sh", "application/x-sh"},{".svg", "image/svg+xml"},{".tar", "application/x-tar"},{".tif", "image/tiff"},{".tiff", "image/tiff"},{".ts", "video/mp2t"},{".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.txtsize_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 (pos == std::string::npos){return "application/octet-stream";}return it->second;
}
int main()
{std::string res = StatuDesc(400);std::cout << res << std::endl;std::string res1 = ExtMime("regex.txt");std::cout << res1 << std::endl;return 0;
}
1.8 Util工具类文件类型判断接口实现
// 判断一个文件是否是一个目录
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 IsRegulerFile(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);
}
int main()
{std::cout << IsDirectory("http.txt") << std::endl;std::cout << IsRegulerFile("http/") << std::endl;return 0;
}
1.9 Util工具类文件路径有效性判断接口实现
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模块
2.1 HttpRequest模块功能设计
可以看我博客项目:前置知识准备,了解一下正则库的使用
http请求信息模块:存储HTTP请求信息要素,提供简单的功能性接口
请求信息要素:
- 请求行:请求方法,URL,协议版本
- URL:资源路径,查询字符串
- GET /search?word=C++&en=utf8 HTTP/1.1
- 请求头部:key: val\r\nkey: val\r\n.....
- Content-Length: 0\r\n
- 正文
要素:请求方法,资源路径,查询字符串,头部字段,正文,协议版本
std::smatch:保存首行使用regex正则进行解析后,所提取的数据,比如提取资源路径中的数字
功能性接口:
1. 将成员变量设置为公有成员,便于直接访问
2. 提供查询字符串,以及头部字段的单个查询和获取,插入功能
3. 获取正文长度
4. 判断长连接&短链接 Connection: close / keep-alive
2.2 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:// 设置头部字段void SetHeader(std::string& key, std::string& val);// 是否存在指定头部字段bool HasHeader(std::string& key);// 获取指定头部字段的值std::string GetHeader(std::string& key);// 插入查询字符串void SetParams(std::string& key, std::string& val);// 判断是否有某个指定的查询字符串bool HasParams(std::string& key);// 获取指定的查询字符串std::string GetParams(std::string& key);// 获取正文长度size_t ContentLength();// 判断是否是短连接:这次连接之后是否要关闭连接,长连接:false,短连接:truebool Close();
};
2.3 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;_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 SetParams(const std::string& key, const std::string& val) {_params.insert(std::make_pair(key, val));// _params[key] = val;}// 判断是否有某个指定的查询字符串bool HasParams(const std::string& key) const {auto it = _params.find(key);if (it == _params.end()) {return false;}return true;}// 获取指定的查询字符串std::string GetParams(const std::string& key) const {auto it = _params.find(key);if (it == _params.end()) {return "";}return it->second;}// 获取正文长度size_t ContentLength() const {// Content-Length: 1234\r\nbool ret = HasHeader("Content-Length");if (ret == false) {return 0;}std::string clen = GetHeader("Content-Length");return std::stol(clen);}// 判断是否是短连接:这次连接之后是否要关闭连接,长连接:false,短连接:truebool Close() const {// 没有Connection字段,或者有Connection但是值为close,都是短连接DBG_LOG("Request HasHeader: %d", HasHeader("Connection") == true);DBG_LOG("Request GetHeader: %d",GetHeader("Connection") == "keep-alive");DBG_LOG("Request:%s", GetHeader("Connection").c_str());if (HasHeader("Connection") == true &&GetHeader("Connection") == "keep-alive") {return false; // 长连接返回错误}return true;}
};
三、HttpResponse模块
3.1 HttpResponse模块功能设计
功能:存储HTTP响应信息要素,提供简单的功能性接口
响应信息要素:
1. 响应状态码
2. 头部字段
3. 响应正文
4. 重定向信息(是否进行了重定向的标志,重定向的路径)
功能性接口:
0. 为了便于成员的访问,因此将成员设置为公有成员
1. 头部字段的新增,查询,获取
2. 正文的设置
3. 重定向的设置
4. 长短连接的判断
3.2 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));// _headers[key] = val;}// 是否存在指定头部字段bool HasHeader(const std::string& key) {auto it = _headers.find(key);if (it == _headers.end()) {return false;}return true;}// 获取指定头部字段的值std::string GetHeader(const std::string& key) {auto it = _headers.find(key);if (it == _headers.end()) {return "";}return it->second;}void SetContent(const std::string& body,const std::string& type = "text/html") {_body = body;SetHeader("Content-Type", type);}void SetRedirect(const std::string& url, int statu = 302) // 302永久重定向{_statu = statu;_redirect_flag = true;_redirect_url = url;}// 判断是否是短连接:这次连接之后是否要关闭连接,长连接:false,短连接:truebool Close() {// 没有Connection字段,或者有Connection但是值为close,都是短连接if (HasHeader("Connection") == true &&GetHeader("Connection") == "keep-alive") // 是长连接{return false; // 返回错误}return true;}
};
四、HttpContext模块
4.1 HttpContext(上下文类)模块功能设计
4.2 HttpContext模块类设计与实现
typedef enum {RECV_HTTP_ERROR,RECV_HTTP_LINE,RECV_HTTP_HEAD,RECV_HTTP_BODY,RECV_HTTP_OVER
} HttpRecvStatu;#define MAX_LINE 8192 /*8kb*/
class HttpContext {
private:int _resp_statu; // 响应状态码HttpRecvStatu _recv_statu; // 当前接收及解析的阶段状态HttpRequest _request; // 已经解析得到的请求信息
private:// 解析请求行bool ParseHttpLine(const std::string& line) {std::smatch smatches;std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? ""(HTTP/1\\.[01])(?:\n|\r\n)?",std::regex::icase);bool ret = std::regex_match(line, smatches, e);if (ret == false) {_recv_statu = RECV_HTTP_ERROR;_resp_statu = 400; // BAD REQUEST// DBG_LOG("HTTP LINE PARSE ERROR: %s", line.c_str()); // 添加日志return false;}/*0 : GET /pupu's_blog/login?user=pupu&passwd=12312 HTTP/1.11 : GET2 : /pupu's_blog/login3 : user=pupu&passwd=123124 : HTTP/1.1*/// 开始解析// 请求方法的获取_request._method = smatches[1];std::transform(_request._method.begin(), _request._method.end(),_request._method.begin(), ::toupper);// std::cout << method << std::endl;// 资源路径的获取,需要进行URL解码操作,但不需要+ ——> " "_request._path = Util::UrlDecode(smatches[2], false);// 版本获取_request._version = smatches[4];// 查询字符串的解析和获取std::vector<std::string> quary_string_array;std::string quary_string = smatches[3];// 查询字符串的格式 key = val&key = val...Util::Split(quary_string, "&", &quary_string_array);for (auto& str : quary_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 val =Util::UrlDecode(str.substr(pos + 1), true); //+ ——> " "_request.SetParams(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) {// 缓冲区中的数据不足一行,则需判断buf的可读数据长度,若很长都不是一行,有问题if (buf->ReadableSize() > MAX_LINE) {_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; //"URI TOO LONG"return false;}// 缓冲区数据不足一行,但是不多,就等新数据的到来return true;}if (line.size() > MAX_LINE) {_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; //"URI TOO LONG"return false;}bool ret = ParseHttpLine(line);if (ret == false) {return false;}// buf->MoveReadOffset(line.size());_recv_statu = RECV_HTTP_HEAD;return true;}// 接收请求头bool RecvHttpHeader(Buffer* buf) {if (_recv_statu != RECV_HTTP_HEAD)return false;while (1) {// 一行一行取出数据,直到遇到空行为止,头部的格式:key: val\r\nkey:// val\r\nstd::string line = buf->GetLineAndPop();DBG_LOG("HEADER LINE: [%s]", line.c_str());// 2.需要考虑的一些要素:缓冲区中数据不足一行,或者一行数据超大if (line.size() == 0) {// 缓冲区中的数据不足一行,则需判断buf的可读数据长度,若很长都不是一行,有问题if (buf->ReadableSize() > MAX_LINE) {_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; //"URI TOO LONG"return false;}// 缓冲区数据不足一行,但是不多,就等新数据的到来return true;}if (line.size() > MAX_LINE) {_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414; //"URI TOO LONG"return false;}// 解析// 头部接收完毕if (line == "\n" || line == "\r\n") {break;}bool ret = ParseHttpHeader(line);if (ret == false) {return false;}}// 头部处理完毕,进入正文获取阶段_recv_statu = RECV_HTTP_BODY;return true;}// 解析请求头bool ParseHttpHeader(std::string& line) {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;return false;}std::string key = line.substr(0, pos);std::string val = line.substr(pos + 2); //+ ——> " "_request.SetHeader(key, val);return true;}// 接收正文部分bool RecvHttpBody(Buffer* buf) {if (_recv_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中放了多少数据了,取决于_request._bodysize_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 RecvStatus() { return _recv_statu; }HttpRequest& Request() { return _request; }// 接收并解析Http请求void RecvHttpRequest(Buffer* buf) {// 不同的状态,做不同的事情,但是这里不要break,// 因为处理完请求行后,应该立即处理头部,而不是退出等新数据switch (_recv_statu) {case RECV_HTTP_LINE:RecvHttpLine(buf);case RECV_HTTP_HEAD:RecvHttpHeader(buf);case RECV_HTTP_BODY:RecvHttpBody(buf);}return;}
};
五、HttpServer模块
用于实现HTTP服务器的搭建
5.1 HttpServer模块功能设计
设计一张请求路由表:
表中记录了针对哪个请求,应该使用哪个函数来进行业务处理的映射关系
服务器接收到请求后,会先在路由表中查找对应的处理函数。如果找到匹配项,便执行该处理函数。这种机制允许用户自定义请求的处理方式,服务器只需负责执行预设的处理逻辑即可。
这样做的好处:用户只需要实现业务处理函数,然后将请求与处理函数的映射关系,添加到服务器中。服务器只需要接收数据,解析数据,查找路由器表映射关系,执行业务处理函数。
要实现简便的搭建HTTP服务器,所需要的要素和提供的功能
要素:
GET请求的路由映射表
POST请求的路由映射表
PUT请求的路由映射表
DELETE请求的路由映射表 --- 路由映射表记录对应请求方法的请求的处理函数映射关系 --- 更多是功能性请求的处理
静态资源相对根目录 --- 实现静态资源请求的处理
高性能TCP服务器 --- 进行连接的IO操作
接口: 服务器处理流程:
从socket接收数据,放到接收缓冲区
调用OnMessage回调函数进行业务处理
对请求进行解析,得到了一个HttpRequest结构,包含了所有的请求要素
进行请求的路由查找 -- 找到对应请求的处理方法
静态资源请求 --- 一些实体文件资源的请求,html,image...... 将静态资源文件的数据读取出来,填充到HttpResponse结构中
功能性请求 --- 在请求路由映射表中查找处理函数,找到了则执行函数 具体的业务处理,并进行HttpResponse结构的数据填充
对静态资源请求/功能性请求进行处理完毕后,得到了一个填充了响应信息的HttpResponse对象,组织http格式响应,进行发送
接口:
添加请求-处理函数映射信息(GET/POST/PUT/DELETE)
设置静态资源根目录
设置是否启动超时连接关闭
设置线程池中线程数量
启动服务器
OnConnected ----用于给TcpServer设置协议上下文
OnMessage ----- 用于进行缓冲区数据解析处理
请求的路由查找
静态资源请求查找和处理
功能性请求的查找和处理
组织响应进行回复
5.2 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:// 将HttpResponse中的要素按照http协议格式进行组织,发送void WriteResponse();// 静态资源的请求处理boolFileHandler();// 根据操作功能性请求的分类处理void DispatcherRequest();// 针对请求进行路由匹配void Route();// 设置上下文void OnConnected();// 缓冲区数据解析+处理void OnMessage();public:HttpServer();void SetBaseDir(const std::string &path);// pattern:正则表达式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();
};
5.3 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) + " " + Util::StatuDesc(rsp->_statu);body += "</h1>";body += "</body>";body += "</html>";// 2.将其作为正文,放入rsprsp->SetContent(body, "text/html");}// 将HttpResponse中的要素按照http协议格式进行组织,发送void WriteResponse(const PtrConnection& conn, const HttpRequest& req,HttpResponse& rsp) {// 1.组织:先完善头部字段if (req.Close() == true) // 是短连接{DBG_LOG("--------------CONNECTION IS CLOSED------------------");rsp.SetHeader("Connection", "close");} else {DBG_LOG("--------------CONNECTION IS KEEPALIVE------------------");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/HEADif (req._method != "GET" && req._method != "HEAD") {return false;}// 3.请求的资源路径必须是合法路径if (Util::ValidPath(req._path) == false) {return false;}// 4.请求的资源必须存在,且是一个普通文件// 特殊请求情况:请求的就是/,返回默认首页index.html(追加// 实际路径:_basedir + req._path :/image/a.png ->// ./wwwroot/image/a.pngstd::string req_path =_basedir +req._path; // 为了避免直接修改资源路径,因此定义一个临时对象if (req._path.back() == '/') {req_path += "index.html";}if (Util::IsReguler(req_path) == false) {return false;}// req._path = req_path; //// 如果请求就是静态资源请求,实际的存储路径,以便于后续静态文件的处理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;}// 根据操作功能性请求的分类处理:在对应请求方法的路由表中,查找是否有对应资源请求的处理函数,有则调用,没有则发送404void DispatcherRequest(HttpRequest& req, HttpResponse* rsp,Handlers& handlers) {// 思想:在路由表存储的是键值对 -- 正则表达式 & 处理函数// 使用正则表达式,对请求的资源路径进行正则匹配,匹配成功就是用对应的函数进行处理for (auto& handler : handlers) {const std::regex& re = handler.first;const Handler& functor = handler.second;bool ret = std::regex_match(req._path, req._matches, re);if (ret == false) {continue;}return functor(req, rsp); // 传入请求信息,和空的rsp,执行处理函数}rsp->_statu = 404;}// 针对请求进行路由匹配void Route(HttpRequest& req, HttpResponse* rsp) {// 1.对请求进行分辨,是静态资源请求,还是功能性请求// GET,HEAD都默认先认为是静态资源请求,则进行静态资源的处理// 功能性请求:则需要通过几个请求路由表来确定是否有处理函数// 既不是静态资源请求,也未设置功能性请求处理函数,就返回404,所请求的资源不存在if (isFileHandler(req) == true) {// 是一个静态资源请求DBG_LOG("HANDLING STATIC FILE: %s", req._path.c_str());return FileHandler(req, rsp);}// 不是静态资源请求,则是功能性请求if (req._method == "GET" || req._method == "HEAD") {DispatcherRequest(req, rsp, _get_route);} else if (req._method == "POST") {DispatcherRequest(req, rsp, _post_route);} else if (req._method == "PUT") {DispatcherRequest(req, rsp, _put_route);} else if (req._method == "DELETE") {DispatcherRequest(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) {// 只要buffer内部有数据,就循环处理while (buffer->ReadableSize() > 0) {DBG_LOG("BUFFER DATA [%zd bytes]: %.*s", buffer->ReadableSize(),(int)std::min(buffer->ReadableSize(), 200UL),buffer->ReadPosition()); // 打印前200字节// 1.获取上下文HttpContext* context = conn->GetContext()->get<HttpContext>();// 2.通过上下文对缓冲区数据进行解析,得到HttpServer对象// 1.如果缓冲区的数量解析出错,就直接恢复出错响应// 2.如果解析正常,且请求已经获取完毕,才开始去进行处理context->RecvHttpRequest(buffer);HttpRequest& req = context->Request();HttpResponse rsp(context->RecvStatus()); // 响应的结构if (context->RespStatu() >= 400) //>400:出错了{// 进行出错响应,关闭连接ErrorHandler(req, &rsp); // 填充错误显示页面数据到rsp中WriteResponse(conn, req, rsp); // 组织响应发送给客户端buffer->MoveReadOffset(buffer->ReadableSize());conn->Shutdown();return;}if (context->RecvStatus() != RECV_HTTP_OVER) {DBG_LOG("REQUEST INCOMPLETE, WAITING MORE DATA...");// 当前请求未接收完整,则退出,等有新数据到来再重新处理return;}DBG_LOG("REQUEST RECEIVED: %s %s", req._method.c_str(),req._path.c_str());// 3.请求路由 + 业务处理Route(req, &rsp);DBG_LOG("RESPONSE STATUS: %d", rsp._statu);// 4.对HttpResponse进行组织发送WriteResponse(conn, req, rsp); // 添加完后,进行内容的回复// 5.重置上下文context->ReSet();// 6.根据长短连接判断是否关闭连接或者继续处理if (rsp.Close() == true)conn->Shutdown();}}public:HttpServer(int port, int timeout = DEFAULT_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;}// 设置或添加请求(正则表达式)与处理函数的映射关系,pattern:正则表达式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(); }
};
六、基于HttpServer搭建HTTP服务器以及7种测试
#include "http.hpp"#define WWWROOT "./wwwroot/"
std::string RequestStr(const HttpRequest& req) {std::stringstream ss;ss << req._method << " " << req._path << " " << req._version << "\r\n";for (auto& it : req._params) {ss << it.first << ": " << it.second << "\r\n";}for (auto& it : req._headers) {ss << it.first << ": " << it.second << "\r\n";}ss << "\r\n";ss << req._body;return ss.str();
}
void Hello(const HttpRequest& req, HttpResponse* rsp) {rsp->SetContent(RequestStr(req), "text/plain");
}
void Login(const HttpRequest& req, HttpResponse* rsp) {rsp->SetContent(RequestStr(req), "text/plain");
}
void PutFile(const HttpRequest& req, HttpResponse* rsp) {// rsp->SetContent(RequestStr(req), "text/plain");std::string pathname = WWWROOT + req._path;Util::WriteFile(pathname, req._body);
}
void DelFile(const HttpRequest& req, HttpResponse* rsp) {rsp->SetContent(RequestStr(req), "text/plain");
}
int main() {HttpServer server(8891);server.SetThreadCount(5);server.SetBaseDir(WWWROOT); // 设置静态资源根目录,告诉服务器有静态资源请求到来,需要到哪里去找资源文件server.Get("/hello", Hello);server.Post("/login", Login);server.Put("/1234.txt", PutFile);server.Delete("/1234.txt", DelFile);server.Listen();return 0;
}
6.1 长连接测试
client.cpp
// 长连接测试1:创建一个客户端,持续给服务器发送数据,直到超过超时时间看看是否正常
// 长连接测试1:创建一个客户端,持续给服务器发送数据,直到超过超时时间看看是否正常#include "../source/server.hpp"int main() {Socket cli_sock;cli_sock.CreateClient(8891, "127.0.0.1");std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";while(1){assert(cli_sock.Send(req.c_str(), req.size())!=-1);char buf[1024] = {0};assert(cli_sock.Recv(buf, 1023));DBG_LOG("[%s]", buf);sleep(3);}cli_sock.Close();return 0; }
因为我们需要保证Connection= "keep-alive"
1.Close返回值控制,出错注意检查Close
2.去掉换行字符
6.2 HTTP 服务器超时连接测试
创建一个客户端,给服务器发送一次数据之后,不动了,查看服务器是否会正常的超时关闭连接
// 超时连接测试1:创建一个客户端,给服务器发送一次数据之后,不动了,查看服务器是否会正常的超时关闭连接#include "../source/server.hpp"int main() {Socket cli_sock;cli_sock.CreateClient(8891, "127.0.0.1");std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";while(1){assert(cli_sock.Send(req.c_str(), req.size())!=-1);char buf[1024] = {0};assert(cli_sock.Recv(buf, 1023));DBG_LOG("[%s]", buf);sleep(15);}cli_sock.Close();return 0; }
6.3 HTTP 服务器错误请求测试
给服务器发送一个数据,告诉服务器要发送1024字节的数据,但是实际发送的数据不足1024,查看服务器处理结果
1.如果数据只发送一次,服务器得不到完整请求,就不会进行业务处理,客户端也就得不到响应,最终超时关闭连接
#include "../source/server.hpp"int main() {Socket cli_sock;cli_sock.CreateClient(8891, "127.0.0.1");std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\npupuheiheihei";while(1){assert(cli_sock.Send(req.c_str(), req.size())!=-1);char buf[1024] = {0};assert(cli_sock.Recv(buf, 1023));DBG_LOG("[%s]", buf);sleep(3);}cli_sock.Close();return 0; }
1. 10s超时关闭连接
2.连着给服务器发送了多次小的请求,服务器会将后边的请求当做前边的请求正文进行处理,而后边处理的时候有可能就会因为处理错误而关闭连接
正常情况应该是这样的运行结果,而实际上,会出现段错误,导致服务器崩溃
这里修改错误时进行了一些调试:
比如说想要判断到底是因为不断在调用这个函数所出的错误,还是不断执行该函数内部的循环的错误,那就需要在函数内部打断点:break http.hpp:786,在http.hpp的786行打断点,再用list(进行查看到哪行了),p(查看值大小),n(next下一步),c(continue下一个断点)等再来排除到底在哪里那个函数出错:仿muduo库one thread one loop式并发服务器SERVER模块(下)
原来在OnMessage这里:
这里出现的问题是:就算他是一个错误处理,也会取数据,但这里出现的现象是,数据一直取不完,因为每次至少接收一个首行,状态肯定是会变化的,状态会变成RECV_HTTP_LINE跳出去,而不是持续的在这里接收数据处于RECV_HTTP_ERROR状态。这是因为这里边是直接关闭了连接,并没有对context初始化,context响应状态一直是400,HTTP状态一直是ERROR状态,在第一次出错后,并没有重置状态,仍然处于RECV_HTTP_ERROR状态,因此在这里时,直接退出了
并没有在缓冲区里去取出数据,导致缓冲区一直有数据处理不完。因此需要在发送完数据之后,关闭连接之前,重置context:
不断地将缓冲区数据取至完毕
6.4 HTTP 服务器业务处理超时测试
业务处理出现超时,需检查服务器运行状态。当服务器性能达到瓶颈时,单个业务处理耗时过长,超过了设定的非活跃超时时间限制。
主要问题体现在:
- 单个业务处理耗时过长,可能导致其他连接因超时被释放
- 例:描述符1-5就绪,处理1耗时30秒导致超时
- 2-5描述符因长时间未刷新活跃度被释放
潜在风险:
- 若2-5为通信连接描述符:就绪后可立即处理并刷新活跃度,影响较小
- 若2号为定时器描述符:触发超时会释放3-5描述符
- 此时处理3-5事件将导致程序崩溃(内存访问错误)
解决方案: 在当前事件处理过程中,不应直接释放连接。应将释放操作加入任务队列,待事件处理完成后,再执行队列中的释放任务。
因此我们还需要在server.hpp添加:在处理时,都现将释放操作加入任务队列,防止内存访问错误
想要服务器业务处理超时就在业务处理这里sleep(15):
创建10子进程,10个客户端:访问我们的服务器,给服务器发送请求,服务器收到请求进行业务处理,一个线程处理一个业务。
#include "../source/server.hpp"int main() {signal(SIGCHLD, SIG_IGN); // 处理僵尸进程for (int i = 0; i < 10; i++) {pid_t pid = fork();if (pid < 0) {DBG_LOG("FORK ERROR...");return 1;} else if (pid == 0) {Socket cli_sock;cli_sock.CreateClient(8891, "127.0.0.1");std::string req = "GET /hello HTTP/1.1\r\nConnection: ""keep-alive\r\nContent-Length: 0\r\n\r\n";while (1) {assert(cli_sock.Send(req.c_str(), req.size()) != -1);char buf[1024] = {0};assert(cli_sock.Recv(buf, 1023));DBG_LOG("[%s]", buf);// sleep(3);}cli_sock.Close();exit(0);}}while (1)sleep(1);return 0; }
超时多少次,就处理多少次超时事件:Server.hpp中TImerWheel中修改
6.5 HTTP 服务器同时多条请求测试
#include "../source/server.hpp"int main()
{Socket cli_sock;cli_sock.CreateClient(8085, "127.0.0.1");std::string req = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";req += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";while (1){assert(cli_sock.Send(req.c_str(), req.size()) != -1);char buf[1024] = {0};assert(cli_sock.Recv(buf, 1023));DBG_LOG("[%s]", buf);sleep(3);}cli_sock.Close();return 0;
}
6.6 HTTP 服务器大文件传输测试
大文件传输测试,给服务器上传一个大文件,服务器将文件保存下来,观察处理结果
上传的文件,和服务器保存的文件一致
创建一个大文件:文件数据中都是0、空
dd if=/dev/zero of=./hello.txt bs=1G count=1
pupu@pupu-ubuntu:~/project/http-v1/test$ dd if=/dev/zero of=./hello.txt bs=1G count=1 记录了1+0 的读入 记录了1+0 的写出 1073741824字节(1.1 GB,1.0 GiB)已复制,1.53548 s,699 MB/s
追加不一样的:
echo "hello pupu" >> hello.txt
client.cpp
// 大文件传输测试,给服务器上传一个大文件,服务器将文件保存下来,观察处理结果// 上传的文件,和服务器保存的文件一致#include "../source/server.hpp" #include "../source/http/http.hpp" int main() {Socket cli_sock;cli_sock.CreateClient(8085, "127.0.0.1");std::string req = "PUT /1234.txt HTTP/1.1\r\nConnection: keep-alive\r\n";std::string body;Util::ReadFile("./hello.txt", &body);req += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n";assert(cli_sock.Send(req.c_str(), req.size()) != -1);assert(cli_sock.Send(body.c_str(), body.size()) != -1);char buf[1024] = {0};assert(cli_sock.Recv(buf, 1023));DBG_LOG("[%s]", buf);cli_sock.Close();return 0; }
差不多5s就传输完毕了:
传输成功:
使用: md5sum hello.txt md5sum 1234.txt来判断内容是否一致
6.7 HTTP 服务器性能压力测试说明-WebBench
并发量:可以同时处理多少客户端的请求而不会出现连接失败
QBS:每秒钟处理的包的数量
借助:webbench工具
原理:创建大量的进程,在进程中,创建客户端连接服务器,发送请求,收到响应后关闭连接,开始下一个连接的建立
抛开环境说性能测试,都是无知的
测试环境:
“本次性能测试在 Ubuntu 22.04 虚拟机(4 vCPU/4GB RAM) 中完成,该虚拟机运行于 Alienware m17 R3 物理主机(i7-10750H/16GB RAM) 的 VMware Workstation 虚拟化平台。服务端与压测工具部署于同一虚拟机,通过
localhost
发起 HTTP/1.0 短连接请求。在 5,000 并发用户持续压测 60 秒 的条件下,服务端表现如下:
总处理请求 322,131(成功率 100%)
平均吞吐量 5,369 QPS
数据传输速率 806,900 bytes/sec
结语:
随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。
在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。
你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容。