云计算百科
云计算领域专业知识百科平台

<项目> 高并发服务器的HTTP协议支持

目录

HTTP模块

模块划分与介绍

模块实现

Util模块

HTTPRequest模块

HTTPResponse模块

HTTPContext模块

ParseHttpLine

RecvHttpLine

RecvHttpHead

 ParseHttpHead

 RecvHttpBody

对外接口 

 HttpServer模块

OnConnected

OnMessage  

Route

IsFileHandler

FileHandler

Dispatcher 

 WriteReponse

ErrorHandler

对外接口

测试

功能测试

测试4

压力测试


实现了基于事件驱动的epoll的高并发服务器之后,我们需要为用户提供一些协议支持,方便用户使用高并发服务器,那么我们直接支持应用层运用最广泛的超文本传输协议HTTP

HTTP模块

模块划分与介绍

HTTP协议的支持我们分类为五个模块:

Util工具集模块 。

功能:实现一些零碎的频繁调用的功能性工具接口:

  • 向文件读取内容
  • 向文件写入内容
  • URL的编码、解码
  • 通过HTTP状态码获取对应描述信息
  • 通过文件后缀名获取content-type
  • 判断文件类型
  • 判断HTTP请求资源路径是否合法

意义:在协议支持模块中,便于使用,不需要频繁编写

 HTTPRequest模块。

功能:负责HTTP请求的报文反序列后的字段存储。

意义:HTTP请求反序列化解析后,便于请求信息的获取。

成员:

  • 请求方法
  • 请求资源路径
  • 查询字符串
  • 协议版本
  • 头部字段
  • 正文

接口:

  • 查询字符串的插入和获取
  • 头部字段的插入和获取
  • 正文长度的获取
  • 长短连接的判断

HTTPResponse模块。

功能: 负责HTTP响应报文的信息存储。

意义:便于后续构建HTTP响应报文时,获取各个报文字段信息。

成员:

  • 协议版本
  • 响应状态码
  • 状态码描述信息
  • 头部字段
  • 正文

接口:

  • 头部字段的插入、获取以及是否存在
  • 正文的设置
  • 长短连接的判断

HTTPContext模块。

功能:记录HTTP请求的接收和处理进度。

意义:服务端的一个连接读事件就绪后,会调用该连接的读回调函数,此时读上来的数据可能并不是一个完整的HTTP请求报文(因为TCP面向字节流),所以上层业务处理(对HTTP请求报文的解析)就会失败,需要收到剩余的HTTP请求报文数据才能完成HTTP请求报文的反序列化工作,因此每次在一个连接的请求报文处理的时候,需要将处理进度记录,以便于下次处理时继续从上次的进度向下处理。我们在服务器模块已经对一个连接的Connection对象添加了Any类型的context成员,这个context成员就是用来接收HTTPContext对象的,所以之后每次处理请求报文时可以根据Connection对象的context成员来获取当前报文处理进度。

成员:

  • 接收状态
    • 处理到请求行
    • 处理到请求头部
    • 处理到请求正文
    • 处理完毕
    • 处理出错
  • 响应状态码
    • 在HTTP请求报文的解析过程中可能会出现错误,例如:访问资源不存在、资源路径不合法,我们需要对这些错误设置对应的响应状态码

接口:

  • 接收并处理数据
  • 返回反序列化解析完毕的请求信息(一个HTTPrequest对象)
  • 返回响应状态码
  • 返回当前处理的状态

HTTPServer模块。 

功能:对HTTP协议模块的整合

意义:使用户对HTTP服务器的搭建更加的方便简捷。

模块实现

Util模块

为了方便在其他模块中使用Util类中的方法,我们将Util类中的方法都设置为static静态成员函数,并设置为public访问权限,从而可以在类外使用域名访问成员函数。

Split字符串分割

将原字符串src根据sep为分隔符,将分割结果放入out中,不保留分割后产生的空字符串。

// 将字符串Src按照sep字符进行分割,输出到out中
static void Split(const std::string& Src, std::vector<std::string>* Out, const std::string& sep)
{
size_t start = 0;
while (start < Src.size())
{
size_t pos = Src.find(sep, start);
if (pos == std::string::npos)
{
// "abd,,,cbd,def," —> "abd" "cbd" "def"
Out->push_back(Src.substr(start));
break;
}
if (start == pos)
{
start += sep.size();
continue;
}
Out->push_back(Src.substr(start, pos – start));
start = pos + sep.size();
}
}

读写文件 

// 打开并读取文件
static bool ReadFile(const std::string& path, std::string* content)
{
std::ifstream in(path, std::ios::binary);
if (!in.is_open())
{
ERR_LOG("OPEN READ %s FILE FAILED", path.c_str());
return false;
}
// 通过读指针偏移,获取文档大小
in.seekg(0, in.end);
size_t fsize = in.tellg();
in.seekg(0, in.beg);

// 直接扩容指定大小,并读到content中
content->resize(fsize);
in.read(&(*content)[0], fsize);

if (!in.good())
{
ERR_LOG("read %s file failed", path.c_str());
in.close();
return false;
}

in.close();
return true;
}
// 向文件写入
static bool WriteFile(const std::string& path, const std::string& content)
{
// 覆盖写
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out.is_open())
{
ERR_LOG("OPEN WRITE %s FILE FAILED", path.c_str());
return false;
}

out.write(content.c_str(), content.size());
if (!out.good())
{
ERR_LOG("write %s file failed", path.c_str());
out.close();
return false;
}
out.close();
return true;
}

 URL编码

  • URL编码格式:将特殊字符的ASCII值转为两个16进制字符,前缀为%。例如"C++",转为"C%2B%2b"
  • RFC3986文档规定 .  –  _  ~  字母 数字 均属于绝对不编码字符
  • W3C标准规定,查询字符串中的空格需要编码为+,解码则是将+转为空格

static std::string EncodeURL(const std::string& url, bool convert_space_to_plus)
{
std::string res;
for (auto& c : url)
{
if (c == '.' || c == '_' || c == '-' || c == '~' || c == isalnum(c))
{
res += c;
continue;
}
if (c == ' ' && convert_space_to_plus)
{
res += '+';
continue;
}
char tmp[4] = {0};
snprintf(tmp, 4, "%%%02X", c);
res += tmp;

return res;
}
}

 URL解码

static char HEXTOI(char c)
{
if (c >= '0' && c <= '9')
{
return c – '0';
}
if (c >= 'a' && c <= 'z')
{
return c – 'a' + 10;
}
if (c >= 'A' && c <= 'Z')
{
return c – 'A' + 10;
}
return -1;
}
// URL解码
static std::string DecodeURL(const std::string& url, bool convert_plus_to_space)
{
std::string res;
for (int i = 0; i < url.size(); ++i)
{
if (url[i] == '+' && convert_plus_to_space)
{
res += ' ';
continue;
}
if (url[i] == '%' && i + 2 < url.size())
{
char c1 = HEXTOI(url[i + 1]);
char c2 = HEXTOI(url[i + 2]);
char c = c1 * 16 + c2;
res += c;
i += 2;
continue;
}
res += url[i];
}
return res;
}

 响应状态码的获取

将maps使用static修饰,生命周期转为全局,并只在第一次被初始化 

// 响应状态码对应的解释信息
static std::string StatusDesc(int status)
{
// 只初始化一次
static std::unordered_map<int, std::string> maps {
{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 = maps.find(status);
if (it == maps.end())
return "Unkown";
return it->second;
}

获取文件类型

获取文件类型,在构建HTTP响应时需要设置头部字段表明正文类型

// 根据文件后缀名获取文件content_type
static std::string ContentType(const std::string& filename)
{
// 只初始化一次
static std::unordered_map<std::string, std::string> maps {
{".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"}
};
size_t pos = filename.rfind(".");
if (pos == std::string::npos)
{
return "application/octet-stream";
}
std::string ext = filename.substr(pos);
auto it = maps.find(ext);
if (it == maps.end())
return "application/octet-stream";
return it->second;
}

 判断文件是否是目录、普通文件

有一个宏S_ISDIR,可以判断是否是目录文件 

// 判断一个文件是否是一个目录
static bool IsDir(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);
}

 检验资源请求路径是否合法

客户只能请求相对根目录下的资源,例如不能请求相对根目录之上的文件资源 /../idex.html

// 检验HTTP请求的资源路径是否合法安全,客户只能请求相对根目录下的资源,例如不能请求/../idex.html
static bool ValidPath(const std::string& url)
{
// "/Test/../HTTP/../"

// 默认从根目录开始,层数为1,是安全的,一旦层数为0,表示到达相对根目录的上层目录,是不安全的
int deep = 1;
for (int i = 1; i < url.size(); ++i)
{
// 一般以这种形式出现 "../"
if (url[i] == '.')
{
if (i + 2 < url.size() && url[i + 1] == '.' && url[i + 2] == '/')
{
if (–deep == 0)
return false;
i += 2;
continue;
}
}
if (url[i] == '/')
deep++;
}
if (deep > 0) return true;
}

HTTPRequest模块

 HTTPRequest模块

功能:负责HTTP请求的报文反序列后的字段存储。

意义:HTTP请求反序列化解析后,便于请求信息的获取。

成员:

  • 请求方法
  • 请求资源路径
  • 查询字符串
  • 协议版本
  • 头部字段
  • 正文

接口:

  • 查询字符串的插入和获取
  • 头部字段的插入和获取
  • 正文长度的获取
  • 长短连接的判断

实现

class HttpRequest {
public:
std::string _method; //请求方法
std::string _path; //资源路径
std::string _version; //协议版本
std::string _body; //请求正文
std::smatch _matches; //资源路径的正则提取数据
std::unordered_map<std::string, std::string> _headers; //头部字段
std::unordered_map<std::string, std::string> _params; //查询字符串
public:
HttpRequest() : _version("HTTP/1.1") {}
void ReSet()
{
_method.clear();
_path.clear();
_version = "HTTP/1.1";
_body.clear();
std::smatch 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 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
{
// Content-Length: 1234\\r\\n
bool 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,则都是短链接,否则就是长连接
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")
{
return false;
}
return true;
}
};

HTTPResponse模块

HTTPResponse模块

功能: 负责HTTP响应报文的信息存储。

意义:便于后续构建HTTP响应报文时,获取各个报文字段信息。

成员:

  • 响应状态码
  • 头部字段(哈希表存储)
  • 正文
  • 重定向信息(是否进行了重定向,已经重定向的URL)

接口:

  • 头部字段的插入、获取、查询
  • 正文的设置
  • 重定向设置
  • 长短连接的判断

 实现

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("Content-Type", type);
}
void SetRedirect(const std::string &url, int statu = 302) {
_statu = statu;
_redirect_flag = true;
_redirect_url = url;
}
//判断是否是短链接
bool Close() {
// 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接
if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive") {
return false;
}
return true;
}
};

HTTPContext模块

HTTPContext模块

功能:对HTTP请求反序列化,并记录HTTP请求的接收和处理进度。

意义:服务端的一个连接读事件就绪后,会调用该连接的读回调函数,此时读上来的数据可能并不是一个完整的HTTP请求报文(因为TCP面向字节流),所以上层业务处理(对HTTP请求报文的解析)就会失败,需要收到剩余的HTTP请求报文数据才能完成HTTP请求报文的反序列化工作,因此每次在一个连接的请求报文处理的时候,需要将处理进度记录,以便于下次处理时继续从上次的进度向下处理。我们在服务器模块已经对一个连接的Connection对象添加了Any类型的context成员,这个context成员就是用来接收HTTPContext对象的,所以之后每次处理请求报文时可以根据Connection对象的context成员来获取当前报文处理进度。

成员:

  • 接收状态
    • 处理到请求行
    • 处理到请求头部
    • 处理到请求正文
    • 处理完毕
    • 处理出错
  • 响应状态码
    • 在HTTP请求报文的解析过程中可能会出现错误,例如:访问资源不存在、资源路径不合法,我们需要对这些错误设置对应的响应状态码

接口:

  • 接收并处理数据
  • 返回反序列化解析完毕的请求信息(一个HTTPrequest对象)
  • 返回响应状态码
  • 返回当前处理的状态

#define MAX_LINE 8192
class HttpContext
{
private:
int _resp_statu; //响应状态码
HttpRecvStatu _recv_statu; //当前接收及解析的阶段状态
HttpRequest _request; //已经解析得到的请求信息
private:
bool ParseHttpLine(const std::string &line)
{}
bool RecvHttpLine(Buffer *buf)
{}
bool RecvHttpHead(Buffer *buf)
{}
bool ParseHttpHead(std::string &line)
{}
bool RecvHttpBody(Buffer *buf)
{}
public:
HttpContext():_resp_statu(200), _recv_statu(RECV_HTTP_LINE) {}
void ReSet()
{}
int RespStatu() {}
HttpRecvStatu RecvStatu() {}
HttpRequest &Request() {}
//接收并解析HTTP请求
void RecvHttpRequest(Buffer *buf)
{}
};

实现 

枚举五种状态:

  • RECV_HTTP_ERROR
  • RECV_HTTP_LINE
  • RECV_HTTP_HEAD
  • RECV_HTTP_BODY
  • RECV_HTTP_OVER

typedef enum {
RECV_HTTP_ERROR,
RECV_HTTP_LINE,
RECV_HTTP_HEAD,
RECV_HTTP_BODY,
RECV_HTTP_OVER
}HttpRecvStatu;

ParseHttpLine

 正则表达式解析HTTP请求行,获取HTTP请求的资源路径、查询字符串

正则表达式是一种用于匹配字符串中字符组合的模式。通过使用一系列特殊的符号和规则,我们可以构建出复杂的搜索条件,并利用它来进行文本查找、替换等操作。

正则表达式的组成

  • 普通字符:包括字母、数字以及标点符号等直接代表它们自身的字符。
  • 特殊字符(元字符):
    • . 匹配任意单个字符(换行符除外)
    • * 表示前面元素可以出现零次或多次
    • + 前面元素必须至少出现一次以上才能成功匹配
    • ? 意味着前一项目是可选项,即能存在也可以不存在
  • 举例: 

    std::string s = "GET /helloworld/login?user=zhangsan&passwd=suici HTTP/1.1\\r\\n";
    std::smatch matches;
    std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\\\?(.*))? (HTTP/1\\\\.[01])(?:\\n|\\r\\n)?");
    // (GET|HEAD|POST|PUT|DELETE) 表示查找这些字符串的任意一个
    // ([^?]*) [^?]表示匹配非问号字符,*表示0次或者多次
    // \\\\? 表示转义的问号 (.*)表示提取?之后的任意字符0次或多次
    bool ret = std::regex_match(s, matches, e);
    if (!ret) return -1;
    for (auto& e : matches)
    {
    std::cout << e << std::endl;
    }

     

    我们的正则表达式避免了

    • 因为请求行中又 \\r\\n 而匹配失败问题
    • 没有搜索关键字? 失败问题 

     实现

    注意点:

    • ParseHttpLine目的是反序列化请求行,获取请求报文的请求方法、请求资源路径、查询字符串、HTTP版本信息,并存储在HTTPRequest对象中

    • 为了代码的健壮性,有些HTTP请求的请求方法可能是小写,所以在反序列化请求行时需要将请求方法统一大写
    • 浏览器的URL默认会进行特殊字符编码,所以我们需要将获取到的资源路径进行URL解码 
    • 对于查询字符串,可能会有多个以&作为分隔符的键值对,所以我们在对HTTPRequest对象填充请求报文的报头字段时,需要进行字符串分割,获取各个键值对,再对各个键值对以:作为分隔符,插入到HTTPRequest对象的存储报头字段的哈希表中
    • (GET|HEAD|POST|PUT|DELETE) 表示查找这些字符串的任意一个
    • ([^?]*) [^?]表示匹配非问号字符,*表示0次或者多次
    • \\\\? 表示转义的问号  (.*)表示提取?之后的任意字符0次或多次

    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 REQUEST
    return false;
    }
    //0 : GET /bitejiuyeke/login?user=xiaoming&pass=123123 HTTP/1.1
    //1 : GET
    //2 : /bitejiuyeke/login
    //3 : user=xiaoming&pass=123123
    //4 : HTTP/1.1
    //请求方法的获取
    _request._method = matches[1];
    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 REQUEST
    return 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;
    }

    RecvHttpLine

     从输入缓冲区读取一行数据,并进行HTTP请求行的解析

    注意点:

    规定一个请求行最大长度为 MAX_LINE = 8K

    • 先判定当前状态是否为RECV_HTTP_LINE,如果不是则 return false
    • 如果输入缓冲区没有一行数据(即没有 '\\n')
      • 如果输入缓冲区可读字节数大于8K,则判定该请求报文有问题,HTTP响应414状态码 — “URI Too Long”,并return false
      • 如果输入缓冲区可读字节数小于8K,则return true,等待下一次读事件就绪时被回调
    • 如果获取到了一行数据,但是数据长度大于MAX_LINE,同样HTTP响应414状态码 — “URI Too Long”,并return false
    • 调用ParseHttpLine,进行请求行的反序列化处理
    • 如果ParseHttpLine返回值为真,则表明反序列化成功,进入头部字段获取状态

    bool RecvHttpLine(Buffer *buf)
    {
    if (_recv_statu != RECV_HTTP_LINE) return false;
    //1. 获取一行数据,带有末尾的换行
    std::string line = buf->Getline();
    //2. 需要考虑的一些要素:缓冲区中的数据不足一行, 获取的一行数据超大
    if (line.size() == 0) {
    //缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的
    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;

    //首行处理完毕,进入头部获取阶段
    _recv_statu = RECV_HTTP_HEAD;
    return true;
    }

    RecvHttpHead

    HTTP请求头部字段的读取

     注意点:

    • 先判断当前状态是否为RECV_HTTP_HEAD
    • 循环读取每一个头部字段,遇到连续的 \\r\\n 或连续的 \\n 则 break
    • 将每一个头部字段调用ParseHttpHead进行解析,并将解析结果 insert 到 _request 对象中
    • 如果解析成功,则当前处理状态更改为RECV_HTTP_BODY,表明头部处理完毕,进入正文获取阶段

    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->Getline();
    //2. 需要考虑的一些要素:缓冲区中的数据不足一行, 获取的一行数据超大
    if (line.size() == 0)
    {
    //缓冲区中的数据不足一行,则需要判断缓冲区的可读数据长度,如果很长了都不足一行,这是有问题的
    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 = ParseHttpHead(line);
    if (ret == false)
    return false;

    }
    //头部处理完毕,进入正文获取阶段
    _recv_statu = RECV_HTTP_BODY;
    return true;
    }

     ParseHttpHead

     解析一行头部字段

    注意点:

    • 不要忘了把结尾的\\r\\n处理掉,否则直接substr会导致val携带\\r\\n 

    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;//
    return false;
    }
    std::string key = line.substr(0, pos);
    std::string val = line.substr(pos + 2);
    _request.SetHeader(key, val);
    return true;
    }

     RecvHttpBody

    获取请求正文

    注意点:

    • 先判断当前处理状态是否是RECV_HTTP_BODY。
    • 根据已经解析好的头部字段的Content-Length获取正文长度。
    • 判断输入缓冲区可读空间是否足够正文长度
      • Content-Length长度为0,则直接修改处理状态为RECV_HTTP_OVER,表明请求接收解析完毕。
      • 足够,则追加到_request的正文字段中(注意是追加,不是赋值,因为这可能是第二次进入RecvHttpBody函数),然后更改处理状态为RECV_HTTP_OVER,表明请求接收解析完毕。
      • 不足,则也将缓冲区数据追加到_request的正文字段中,不修改处理状态,等待下一次的读事件就绪被回调

    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->MoveReadPos(real_len);
    _recv_statu = RECV_HTTP_OVER;
    return true;
    }
    // 3.2 缓冲区中数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来
    _request._body.append(buf->ReadPosition(), buf->ReadAbleSize());
    buf->MoveReadPos(buf->ReadAbleSize());
    return true;
    }

    对外接口 

    注意点:

    • 在构造函数中,默认接收处理状态为RECV_HTTP_LINE,响应状态码为200
    • 状态重置函数,重置处理的状态
    • 获取处理状态
    • 获取响应状态码
    • 获取反序列化并填充好字段的request对象
    • 反序列化接口,每次都需要调用该接口反序列化请求报文,按顺序反序列化

    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, 因为处理完请求行后,应该立即处理头部,而不是退出等新数据
    switch(_recv_statu)
    {
    case RECV_HTTP_LINE: RecvHttpLine(buf);
    case RECV_HTTP_HEAD: RecvHttpHead(buf);
    case RECV_HTTP_BODY: RecvHttpBody(buf);
    }
    return;
    }

     HttpServer模块

    HTTPServer模块。

    功能:对HTTP协议模块的整合,实现HTTP服务器的搭建。

    意义:使用户对HTTP服务器的搭建更加的方便简捷。

    设计:我们维护多张表,表中记录了针对哪个请求,应该调用哪个回调函数来进行业务处理的映射关系。如果用户使用的是GET方法,则后端会在GET表中搜索用户请求的资源路径所需要调用的回调函数,并将用户的HTTP请求中的搜索关键字传入进去,是一种动态资源的获取。表的功能如同路由器表功能一样,故为路由表。

    什么请求,怎么处理,由用户来决定,所以这几张表是用户来填充的。这样做的好处就是用户只需要实现业务处理函数,然后将请求与处理函数的映射关系,添加到服务器中。那么服务器只需要接受数据,调用业务回调函数,解析数据,查找路由表映射关系,执行业务处理函数。

    成员

    • GET请求的路由映射表
    • POST请求的路由映射表
    • PUT请求的路由映射表
    • DELETE请求的路由 
    • 静态资源相对根目录
    • 高性能TCP服务器 — 进行连接的IO操作

    接口:

    public:

    • 添加请求与处理函数的映射信息
    • 设置静态资源根目录
    • 设置是否启动超时连接关闭
    • 设置从属Reactor线程数量
    • 启动服务器

    private:

    • OnConnected,给TcpServer设置上下文(一个HTTPContext对象)
    • OnMessage,业务处理函数,读取缓冲区数据、反序列化、路由、响应、重置上下文、关闭短连接
    • Route,请求的路由查找。判断客户端请求的是静态资源还是功能性请求
    • FileHandler,静态资源的请求处理
    • Dispatcher,功能性请求的分类处理
    • WriteReponse,组织HTTP响应报文,并调用Send发送至发送缓冲区

    总览接口 

    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)
    {}
    //将HttpResponse中的要素按照http协议格式进行组织,发送
    void WriteReponse(const std::shared_ptr<Connection> &conn, const HttpRequest &req, HttpResponse &rsp)
    {}
    bool IsFileHandler(const HttpRequest &req)
    {}
    // 静态资源的请求处理 — 将静态资源文件的数据读取出来,放到rsp的_body中, 并设置mime
    void FileHandler(const HttpRequest &req, HttpResponse *rsp)
    {}
    //功能性请求的分类处理
    void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers)
    {}
    void Route(HttpRequest &req, HttpResponse *rsp)
    {}
    //设置上下文
    void OnConnected(const std::shared_ptr<Connection> &conn)
    {}
    //缓冲区数据解析+处理
    void OnMessage(const std::shared_ptr<Connection> &conn, Buffer *buffer)
    {}
    public:
    HttpServer(int port, int timeout = DEFALT_TIMEOUT):_server(port)
    {}
    void SetBaseDir(const std::string &path)
    {}
    /*设置/添加,请求(请求的正则表达)与处理函数的映射关系*/
    void Get(const std::string &pattern, const Handler &handler)
    {}
    void Post(const std::string &pattern, const Handler &handler)
    {}
    void Put(const std::string &pattern, const Handler &handler)
    {}
    void Delete(const std::string &pattern, const Handler &handler)
    {}
    void SetThreadCount(int count)
    {}
    void Listen()
    {}
    };

     大致流程

  • 启动服务器,服务器创建EventLoop对象、Acceptor对象并调用Acceptor内部的baseloop的Start函数,开启Acceptor中对Listen读事件的事件监控。
  • 创建传入的Count数量的从属线程(例如是三个)三个线程内部实例化各自的EventLoop,并在线程的例程中调用Start函数,等待epoll监控的事件就绪,就绪后就调用就绪事件的回调函数(Channel模块中的HandlerEvent事件分配器)
  • 一旦有新连接就绪则调用Listen的读回调函数,accept新连接,为新连接new一个Connection对象,并对Connection对象设置读、写、错误、关闭、任意事件回调,以及上层设置的阶段回调OnConnected、OnMessage、OnClosed、AnyEvent、SvrClosed回调函数,然后添加该连接的读监控到相应的线程中的EventLoop对象的epoll中。
  • 连接的读事件就绪则调用Connection中设置的读回调函数,将该连接的TCP接收缓冲区数据拷贝到我们的输入缓冲区inbuffer中,然后调用上层设置的业务处理函数OnMessage,对数据进行业务处理
  • OnMessage对请求进行解析,反序列化后将信息填充到HTTPRequest的request对象中
  • 对请求的资源路径进行路由查找,找到对应的处理方法(静态资源请求或是功能性请求)
  • 对请求进行处理,并填充一个HTTPResponse的response对象,根据response的信息构建一个HTTP响应报文,写入到我们的发送缓冲区outbuffer,开启连接的写事件监控,一旦写事件就绪,则将我们的发送缓冲区数据写入到TCP的发送缓冲区,如果数据发送完毕则关闭该连接的写事件监控,如果没有写完,则继续监控写事件
  • 实现

    OnConnected

    //设置上下文
    void OnConnected(const std::shared_ptr<Connection> &conn)
    {
    conn->SetContext(HttpContext());
    DBG_LOG("NEW CONNECTION %p", conn.get());
    }

    功能:

    在Acceptor中,如果Listen的读事件就绪,则会获取新连接,并为新连接new一个Connection对象,为连接进行各种回调函数设置,最后调用连接的Established函数,启动该连接的读事件监控,并调用上层设置的连接建立时的回调函数_conn_cb,此时就会执行我们的OnConnected函数,为该连接设置一个上下文,我们这里适配的是HTTP协议上下文,如果是其他协议,则赋值其他协议上下文对象即可,在Connection类中我们使用的是Any类对象接收context,无畏类型

    OnMessage  

    //缓冲区数据解析+处理
    void OnMessage(const std::shared_ptr<Connection> &conn, Buffer *buffer)
    {
    while(buffer->ReadAbleSize() > 0)
    {
    //1. 获取上下文
    HttpContext *context = conn->GetContext()->get<HttpContext>();
    //2. 通过上下文对缓冲区数据进行解析,得到HttpRequest对象
    // 1. 如果缓冲区的数据解析出错,就直接回复出错响应
    // 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->MoveReadPos(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;
    }

    注意点:

  • 循环处理,直至缓冲区为空,因为inbuffer接收缓冲区可能不止一个HTTP请求报文。
  • 获取该连接的上下文对象,如果是第一个调用OnMessage则上下文对象的成员默认处理状态就是RECV_HTTP_LINE。如果不是第一次调用OnMessage函数,则需要根据上一次处理结果的状态,继续处理,所以需要获取上下文数据,以明确HTTP请求报文处理到哪个进度了。
  • 反序列化HTTP请求报文,填充HTTPRequest类对象request,并获取request对象。如果反序列化后的响应状态码不为200,则表明解析出错,对用户响应一个错误页面即可,并重置上下文的状态,不然下一次该连接数据再次到来时,还会获取到上一次的响应状态码,即使没有反序列化出错,但是状态码依旧是上次残留状态。最后清空缓冲区数据,因为HTTP请求已经解析出错了,后面的解析也不可能对,没必要再往后执行了,然后释放关闭连接。
  • 如果响应状态码为200,但是接收处理状态不是RECV_HTTP_OVER,表明当前请求还没有接收完整,等新数据到来再重新继续处理,所以return
  • 如果响应状态码为200,接受处理状态为RECV_HTTP_OVER,表明HTTP请求处理成功,调用Route进行资源获取,填充HTTPResponse对象rsp
  • 根据rsp构建HTTP响应报文,写入Connection的发送缓冲区
  • 如果请求报文中Connection对应键值为close,则表明本次连接为短连接,响应数据后直接释放连接,否则,如果为keep-alive则为长连接,继续处理请求
  • Route

     进行请求的分辨,根据是一个静态资源请求,还是一个功能性请求分类处理

    • 静态资源请求,则进行静态资源的处理,然后返回
    • 功能性请求,则需要通过几个请求路由表来确定是否有处理函数
    • 既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405

    void Route(HttpRequest &req, HttpResponse *rsp)
    {
    //1. 对请求进行分辨,是一个静态资源请求,还是一个功能性请求
    // 静态资源请求,则进行静态资源的处理
    // 功能性请求,则需要通过几个请求路由表来确定是否有处理函数
    // 既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405
    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);
    }
    rsp->_statu = 405;// Method Not Allowed
    return ;
    }

    IsFileHandler

    该请求是否请求的静态资源,我们有以下几点判断

  • 上层用户必须设置了静态资源根目录
  •  客户端的请求方法必须是GET或HEAD方法
  • 请求的资源路径必须合法
  • 请求的资源必须存在,并且是一个普通文件
  • 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.png
    std::string req_path = _basedir + req._path;//为了避免直接修改请求的资源路径,因此定义一个临时对象
    if (req._path.back() == '/')
    req_path += "index.html";

    if (Util::IsRegular(req_path) == false)
    return false;

    return true;
    }

    FileHandler

    判断客户端请求为静态资源后,根据路径获取文档内容,构建HTTPResponse对象即可 

    // 静态资源的请求处理 — 将静态资源文件的数据读取出来,放到rsp的_body中, 并设置mime
    void FileHandler(const HttpRequest &req, HttpResponse *rsp)
    {
    std::string req_path = _basedir + req._path;
    if (req._path.back() == '/')
    req_path += "index.html";

    bool ret = Util::ReadFile(req_path, &rsp->_body);
    if (ret == false)
    return;

    std::string mime = Util::ExtMime(req_path);
    rsp->SetHeader("Content-Type", mime);
    return;
    }

    Dispatcher 

    判断客户端请求为功能请求后,在对应请求方法的路由表中,查找是否含有对应资源请求的处理函数,有则调用,没有则返回404

    //功能性请求的分类处理
    void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers)
    {
    //在对应请求方法的路由表中,查找是否含有对应资源请求的处理函数,有则调用,没有则发挥404
    //思想:路由表存储的时键值对 — 正则表达式 & 处理函数
    //使用正则表达式,对请求的资源路径进行正则匹配,匹配成功就使用对应函数进行处理
    // /numbers/(\\d+) /numbers/12345
    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;
    }

     WriteReponse

    构建HTTP响应报文,并调用Connection的Send接口,将响应报文拷贝到发送缓冲区

    //将HttpResponse中的要素按照http协议格式进行组织,发送
    void WriteReponse(const std::shared_ptr<Connection> &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());
    }

    ErrorHandler

    组织一个错误展示页面,将页面数据,当作响应正文,放入rsp中

    void ErrorHandler(const HTTPRequest& req, HTTPResponse* rsp)
    {
    // 目标:错误页面为响应正文,填充好rsp即可
    std::string body;
    Util::ReadFile("./wwwroot/err.html", &body);
    rsp->SetContent(body, "text/html");
    }

    对外接口

    注意点:

    • 路由表的Key并不是string类型,而是regex类型,这是由于regex每次使用都需要正则表达式编译为它可以识别的一种格式,如果每一个请求都要编译,会影响效率,所以直接保存编译好的regex作为key

    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;
    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();
    }
    };

    测试

    #include "HTTP.hpp"
    #include "../Server.hpp"

    const std::string basedir = "./wwwroot/";

    std::string RequestStr(const HttpRequest& req)
    {
    std::string message;
    message += req._method + " " + req._path + " " + req._version + "\\r\\n";
    for (auto& it : req._params)
    {
    message += it.first + ": " + it.second + "\\r\\n";
    }
    for (auto& it : req._headers)
    {
    message += it.first + ": " + it.second + "\\r\\n";
    }
    message += "\\r\\n";
    message += req._body;
    return message;
    }
    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");
    }
    void DelFile(const HttpRequest& req, HttpResponse* rsp)
    {
    rsp->SetContent(RequestStr(req), "text/plain");
    }

    int main()
    {
    HttpServer svr(8080);
    svr.SetThreadCount(3);
    // 设置根目录
    svr.SetBaseDir(basedir);
    svr.Get("/hello", Hello);
    svr.Post("/login", Login);
    svr.Put("/put", PutFile);
    svr.Delete("/del", DelFile);

    svr.Listen();

    return 0;
    }

     

    提交表单后服务器返回我们的请求报文

    压力测试

    在2核2G云服务器上,即运行服务端,又运行客户端,得的测试结果:当并发量为4000时(云服务器可创建进程数量有限),QPS:88286 / 60 = 1471

    虚拟机配置为4核4G,我们在虚拟机上同时运行服务端与webbench进行5000并发量测试

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » <项目> 高并发服务器的HTTP协议支持
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!