<项目> 高并发服务器的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_typestatic 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.htmlstatic 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\nbool ret = HasHeader("Content-Length");if (ret == false) return 0;std::string clen = GetHeader("Content-Length");return std::stol(clen);}//判断是否是短链接bool Close() const {// 没有Connection字段,或者有Connection但是值是close,则都是短链接,否则就是长连接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请求的资源路径、查询字符串

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

正则表达式的组成

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

举例: 

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 REQUESTreturn 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 REQUESTreturn false;}std::string key = Util::UrlDecode(str.substr(0, pos), true);  std::string val = Util::UrlDecode(str.substr(pos + 1), true);_request.SetParam(key, val);}return true;
}
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 LONGreturn false;}//缓冲区中数据不足一行,但是也不多,就等等新数据的到来return true;}if (line.size() > MAX_LINE) {_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414;//URI TOO LONGreturn false;}bool ret = ParseHttpLine(line);if (ret == false) return false;//首行处理完毕,进入头部获取阶段_recv_statu = RECV_HTTP_HEAD;return true;
}
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 LONGreturn false;}//缓冲区中数据不足一行,但是也不多,就等等新数据的到来return true;}if (line.size() > MAX_LINE) {_recv_statu = RECV_HTTP_ERROR;_resp_statu = 414;//URI TOO LONGreturn false;}if (line == "\n" || line == "\r\n") break;bool ret = ParseHttpHead(line);if (ret == false) return false;}//头部处理完毕,进入正文获取阶段_recv_statu = RECV_HTTP_BODY;return true;
}
 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中, 并设置mimevoid 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() {}
};

 大致流程

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

注意点:

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

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

  • 静态资源请求,则进行静态资源的处理,然后返回
  • 功能性请求,则需要通过几个请求路由表来确定是否有处理函数
  • 既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405
void Route(HttpRequest &req, HttpResponse *rsp) 
{//1. 对请求进行分辨,是一个静态资源请求,还是一个功能性请求//   静态资源请求,则进行静态资源的处理//   功能性请求,则需要通过几个请求路由表来确定是否有处理函数//   既不是静态资源请求,也没有设置对应的功能性请求处理函数,就返回405if (IsFileHandler(req) == true) //是一个静态资源请求, 则进行静态资源请求的处理return FileHandler(req, rsp);if (req._method == "GET" || req._method == "HEAD") {return Dispatcher(req, rsp, _get_route);}else if (req._method == "POST") {return Dispatcher(req, rsp, _post_route);}else if (req._method == "PUT") {return Dispatcher(req, rsp, _put_route);}else if (req._method == "DELETE") {return Dispatcher(req, rsp, _delete_route);}rsp->_statu = 405;// Method Not Allowedreturn ;
}
IsFileHandler

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

  1. 上层用户必须设置了静态资源根目录
  2.  客户端的请求方法必须是GET或HEAD方法
  3. 请求的资源路径必须合法
  4. 请求的资源必须存在,并且是一个普通文件
bool IsFileHandler(const HttpRequest &req) 
{// 1. 必须设置了静态资源根目录if (_basedir.empty()) return false;// 2. 请求方法,必须是GET / HEAD请求方法if (req._method != "GET" && req._method != "HEAD") return false;// 3. 请求的资源路径必须是一个合法路径if (Util::ValidPath(req._path) == false) return false;// 4. 请求的资源必须存在,且是一个普通文件//    有一种请求比较特殊 -- 目录:/, /image/, 这种情况给后边默认追加一个 index.html// index.html    /image/a.png// 不要忘了前缀的相对根目录,也就是将请求路径转换为实际存在的路径  /image/a.png  ->   ./wwwroot/image/a.pngstd::string req_path = _basedir + req._path;//为了避免直接修改请求的资源路径,因此定义一个临时对象if (req._path.back() == '/')  req_path += "index.html";if (Util::IsRegular(req_path) == false) return false;return true;
}
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/12345for (auto &handler : handlers) {const std::regex &re = handler.first;const Handler &functor = handler.second;bool ret = std::regex_match(req._path, req._matches, re);if (ret == false) continue;return functor(req, rsp);//传入请求信息,和空的rsp,执行处理函数}rsp->_statu = 404;
}
 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并发量测试

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

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

相关文章

基于Spring Boot + Vue的银行管理系统设计与实现

基于Spring Boot Vue的银行管理系统设计与实现 一、引言 随着金融数字化进程加速&#xff0c;传统银行业务向线上化转型成为必然趋势。本文设计并实现了一套基于Spring Boot Vue的银行管理系统&#xff0c;通过模块化架构满足用户、银行职员、管理员三类角色的核心业务需求…

微软提出 Logic-RL:基于规则的强化学习释放大语言模型推理能力

❝ 更多 LLM 架构文章点击查看&#xff1a; LLM 架构专栏 大模型架构专栏文章阅读指南 1. AI 智能体&#xff0c;颠覆还是赋能&#xff1f;一文读懂&#xff01; 2. 1W8000 字 解锁 AI 高效运作密码&#xff1a;工作流与智能体如何协同&#xff1f; 3. 万字深度剖析 AI 代理&am…

STM32八股【1】-----启动流程和startup文件理解

启动流程 知识点 MCU 上电复位。MSP从向量表第0个地址读取一个32位&#xff08;2字节&#xff09;的值并保存&#xff0c;该值为栈顶地址。PC计数器从第1个地址读取一个两字节的值并保存&#xff0c;该值为程序入口&#xff0c;一般是Reset_Handler。想了解FLASH地址映射可以…

详解c++20的协程,自定义可等待对象,生成器详解

协程 c20的协程三大标签&#xff1a;“性能之优秀”&#xff0c;“开发之灵活”&#xff0c;“门槛之高” 在讲解c的协程使用前&#xff0c;我们需要先明白协程是什么&#xff0c;协程可以理解为用户态的线程&#xff0c;它需要由程序来进行调度&#xff0c;如上下文切换与调…

JavaEE企业级开发 延迟双删+版本号机制(乐观锁) 事务保证redis和mysql的数据一致性 示例

提醒 要求了解或者熟练掌握以下知识点 spring 事务mysql 脏读如何保证缓存和数据库数据一致性延迟双删分布式锁并发编程 原子操作类 前言 在起草这篇博客之前 我做了点功课 这边我写的是一个示例代码 数据层都写成了 mock 的形式(来源于 JUnit5) // Dduo import java.u…

A2 最佳学习方法

记录自己想法的最好理由是发现自己的想法&#xff0c;并将其组织成可传播的形式 (The best reason for recording what one thinks is to discover what one thinks and to organize it in transmittable form.) Prof Ackoff 经验之谈&#xff1a; 做培训或者写文章&#xff…

嵌入式硬件工程师从小白到入门-PCB绘制(二)

PCB绘制从小白到入门&#xff1a;知识点速通与面试指南 一、PCB设计核心流程 需求分析 明确电路功能&#xff08;如电源、信号处理、通信&#xff09;。确定关键参数&#xff08;电压、电流、频率、接口类型&#xff09;。 原理图设计 元器件选型&#xff1a;匹配封装、电压、…

vue创建子组件步骤及注意事项

在 Vue 中创建子组件需要遵循组件化开发的核心原则&#xff0c;并注意数据流、通信机制、复用性等关键点。以下是详细步骤和注意事项&#xff0c;结合代码示例说明&#xff1a; 一、创建子组件的步骤 1. 定义子组件 创建一个 .vue 文件&#xff08;单文件组件&#xff09;&am…

Cocos Creator版本发布时间线

官网找不到&#xff0c;DeepSeek给的答案&#xff0c;这里做个记录。 Cocos Creator 1.x 系列 发布时间&#xff1a;2016 年 - 2018 年 1.0&#xff08;2016 年 3 月&#xff09;&#xff1a; 首个正式版本&#xff0c;基于 Cocos2d-x 的 2D 游戏开发工具链&#xff0c;集成可…

【Spring AI】基于专属知识库的RAG智能问答小程序开发——功能优化:用户鉴权主体功能开发

系列文章目录 【Spring AI】基于专属知识库的RAG智能问答小程序开发——完整项目&#xff08;含完整前端后端代码&#xff09;【Spring AI】基于专属知识库的RAG智能问答小程序开发——代码逐行精讲&#xff1a;核心ChatClient对象相关构造函数【Spring AI】基于专属知识库的R…

【AI神经网络】深度神经网络(DNN)技术解析:从原理到实践

引言 深度神经网络&#xff08;Deep Neural Network, DNN&#xff09;作为人工智能领域的核心技术&#xff0c;近年来在计算机视觉、自然语言处理、医疗诊断等领域取得了突破性进展。与传统机器学习模型相比&#xff0c;DNN通过多层非线性变换自动提取数据特征&#xff0c;解决…

目标跟踪——deepsort算法详细阐述

deepsort 算法详解 Unmatched Tracks(未匹配的轨迹) 本质角色: 是已存在的轨迹在当前帧中“失联”的状态,即预测位置与检测结果不匹配。 生命周期阶段: 已初始化: 轨迹已存在多帧,可能携带历史信息(如外观特征、运动模型)。 未被观测到: 当前帧中未找到对应的检测框…

Vue-admin-template安装教程

#今天配置后台管理模板发现官方文档的镜像网站好像早失效了&#xff0c;自己稍稍总结了一下方法# 该项目环境需要node17及以下&#xff0c;如果npm install这一步报错可能是这个原因 git clone https://github.com/PanJiaChen/vue-admin-template.git cd vue-admin-template n…

Rust从入门到精通之进阶篇:14.并发编程

并发编程 并发编程允许程序同时执行多个独立的任务&#xff0c;充分利用现代多核处理器的性能。Rust 提供了强大的并发原语&#xff0c;同时通过类型系统和所有权规则在编译时防止数据竞争和其他常见的并发错误。在本章中&#xff0c;我们将探索 Rust 的并发编程模型。 线程基…

算法训练营第二十三天 | 贪心算法(一)

文章目录 一、贪心算法理论基础二、Leetcode 455.分发饼干二、Leetcode 376. 摆动序列三、Leetcode 53. 最大子序和 一、贪心算法理论基础 贪心算法是一种在每一步选择中都采取当前状态下的最优决策&#xff0c;从而希望最终达到全局最优解的算法设计技术。 基本思想 贪心算…

css基础-display 常用布局

CSS display 属性详解 属性设置元素是否被视为块级或行级盒子以及用于子元素的布局&#xff0c;例如流式布局、网格布局或弹性布局。 一、基础显示模式 1. block 作用&#xff1a; 元素独占一行可设置宽高和内外边距默认宽度撑满父容器 应用场景&#xff1a; 布局容器&a…

速卖通API数据清洗实战:从原始JSON到结构化商品数据库

下面将详细介绍如何把速卖通 API 返回的原始 JSON 数据清洗并转换为结构化商品数据库。 1. 数据获取 首先要借助速卖通 API 获取商品数据&#xff0c;以 Python 为例&#xff0c;可使用requests库发送请求并得到 JSON 数据。 import requests# 替换为你的 API Key 和 Secret …

【零基础入门unity游戏开发——2D篇】2D物理系统 —— 2D刚体组件(Rigidbody2D)

考虑到每个人基础可能不一样,且并不是所有人都有同时做2D、3D开发的需求,所以我把 【零基础入门unity游戏开发】 分为成了C#篇、unity通用篇、unity3D篇、unity2D篇。 【C#篇】:主要讲解C#的基础语法,包括变量、数据类型、运算符、流程控制、面向对象等,适合没有编程基础的…

Collectors.toMap / list 转 map

前言 略 Collectors.toMap List<User> userList ...; Map<Long, User> userMap userList.stream().collect(Collectors.toMap(User::getUserId, Function.identity()));假如id存在重复值&#xff0c;则会报错Duplicate key xxx, 解决方案 两个重复id中&#…

热门面试题第13天|Leetcode 110.平衡二叉树 257. 二叉树的所有路径 404.左叶子之和 222.完全二叉树的节点个数

222.完全二叉树的节点个数&#xff08;优先掌握递归&#xff09; 需要了解&#xff0c;普通二叉树 怎么求&#xff0c;完全二叉树又怎么求 题目链接/文章讲解/视频讲解&#xff1a;https://programmercarl.com/0222.%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E8…