Linux与深入HTTP序列化和反序列化
深入HTTP序列化和反序列化
本篇介绍
在上一节已经完成了客户端和服务端基本的HTTP通信,但是前面的传递并没有完全体现出HTTP的序列化和反序列化,为了更好得理解其工作流程,在本节会以更加具体的方式分析到HTTP序列化和反序列化
本节会在介绍HTTP协议基本结构与基本实现HTTPServer的基础之上继续完善HTTP服务器,所以需要有对应的知识作为铺垫才可以开始本节
基本实现思路
本次实现的HttpServer
类主要完成接收客户端发送的HTTP请求,也就是说,服务器需要根据客户端的HTTP请求回复一个HTTP响应,所以必须要有的方法就是处理请求方法,但是前面已经提到过,HTTP的请求属于结构化的数据,并且这个数据在传递给服务器时就已经做了序列化,服务器需要处理的就是将结构化的数据进行反序列化;同样,服务器处理完毕后还需要发给客户端,所以此处就需要服务器对处理的结果填充到HTTP响应结构对象中再返回给客户端,此处就需要进行序列化
基于上面的原因,与前面序列化和反序列化与网络计算器一样,需要实现一个协议,包含HttpRequest
和HttpResponse
类,用于处理序列化和反序列化
本次为了更好得理解序列化和反序列化,以HTTP请求为例,首先以请求行、请求报头和请求体三个整体做序列化和反序列化,接着再深入请求行、请求报头和请求体中的字段
根据这个两个阶段,需要实现的目标如下:
- 第一阶段:打印出反序列化和序列化的结果
- 第二阶段:向客户端返回具体的静态HTML文件
第一阶段
创建HttpRequest
类
根据前面的基本思路,实现HttpRequest
类就需要实现对应的反序列化。因为HTTP请求中带有三种数据:
- 请求行
- 请求报头
- 请求体
所以需要定义三个成员分别存放从请求获取到的内容,所以基本结构如下:
class HttpRequest
{
public:HttpRequest(){}// 反序列化bool deserialize(std::string& in_str){}~HttpRequest(){}
private:std::string _req_line; // 请求行std::vector<std::string> _req_head; // 请求报头std::string _req_body; // 请求体
};
实现HttpRequest
反序列化接口
在HTTP中的反序列化本质就是根据基本的格式去除掉多余的部分,从而提取出有效的数据放在相应的字段中,所以根据这个思路依次进行提取
注意,前面提到过,HTTP是基于TCP的,而TCP是面向字节流的,这就导致可能服务器接收到的HTTP请求不完整,对此还需要对接收到的HTTP请求进行完整性判断,但是本次不考虑这一步
截取请求行
首先提取请求行中的数据,根据前面对HTTP请求结构的描述可以知道HTTP请求的请求行以\r\n
结尾,所以只需要找到第一个\r\n
,就说明找到了请求行,这里定义一个成员函数用来处理这个逻辑:
// 获取请求行
bool parseReqLineFromTotal(std::string& in_str)
{auto pos = in_str.find(default_sep);if(pos == std::string::npos){LOG(LogLevel::WARNING) << "未找到请求行";return false;}// 获取到请求行_req_line = in_str.substr(0, pos);// 从原始字符串中移除请求头和第一个分隔符in_str.erase(0, pos + default_sep.size());LOG(LogLevel::DEBUG) << "请求行处理后:" << in_str;return true;
}
这里考虑到后面的请求体也是以\r\n
结尾,所以考虑将该函数更改为更通用的版本:
// 截取以\r\n结尾的数据
bool parseOneLineFromTotal(std::string& in_str, std::string& out_str)
{auto pos = in_str.find(default_sep);if(pos == std::string::npos)return false;// 获取到截取数据out_str = in_str.substr(0, pos);// 从原始字符串中移除截取的字符串和对应的分隔符in_str.erase(0, pos + default_sep.size());return true;
}
接着完善反序列化接口:
// 反序列化
bool deserialize(std::string& in_str)
{bool getReqLineFlag = parseOneLineFromTotal(in_str, _req_line);if(!getReqLineFlag){LOG(LogLevel::WARNING) << "反序列化获取请求行失败";return false;}LOG(LogLevel::INFO) << "截取的请求行为:" << _req_line;// 未完...
}
截取请求报头
截取请求报头的方式与请求行非常类似,无非就是需要多次截取,但是需要考虑截取何时结束。根据HTTP请求体的特点,最后一行就是一个空行,即\r\n
,所以可以考虑利用这个空行进行处理,具体思路如下:
因为每一次截取都会截取到分隔符之前的内容,所以可以考虑定义一个变量用于接收请求报头的结果,那么根据截取一行的函数的逻辑,只有成功找到了\r\n
时才会进行截取,而截取的结果不会包含\r\n
,那么一旦截取的结果是空且找到了\r\n
,就说明找到了最后一行
根据这个思路,将获取请求报头数据的逻辑放在一个单独的函数中,如下:
// 获取请求报头
bool getReqHeadFromTotal(std::string &in_str)
{// 保存以\r\n结尾的一行数据std::string oneLine;while (true){bool getOneLineFlag = parseOneLineFromTotal(in_str, oneLine);// 如果getOneLineFlag为true并且oneLine不为空,说明当前行有效,否则代表已经找到了结尾if(getOneLineFlag && !oneLine.empty()){_req_head.push_back(oneLine);}else if(getOneLineFlag && oneLine.empty()){break;}else{return false;}}return true;
}
继续完善反序列化接口:
// 反序列化
bool deserialize(std::string &in_str)
{// ...bool getReqHeadLine = getReqHeadFromTotal(in_str);if (!getReqHeadLine){LOG(LogLevel::WARNING) << "反序列化获取请求报头失败";return false;}LOG(LogLevel::INFO) << "获取到的请求行为:";std::for_each(_req_head.begin(), _req_head.end(), [&](int i){std::cout << _req_head[i] << std::endl;});// 未完...
}
截取请求体
因为在前面截取请求行和截取请求报头时已经修改了HTTP请求字符串,所以剩下的就是请求体,直接赋值即可:
// 反序列化
bool deserialize(std::string &in_str)
{// ...// 获取请求体_req_body = in_str;return true;
}
创建HttpResponse
类
服务器需要给客户端返回内容,所以在这之前必须对整个HttpResponse
结构进行序列化。同样,HTTP响应也有对应的三种数据:
- 请求行
- 请求报头
- 请求体
所以需要定义三个成员分别存放从请求获取到的内容,所以基本结构如下:
// HTTP响应
class HttpResponse
{
public: HttpResponse(std::string &rl, std::vector<std::string> &rq, std::string &rb): _resp_line(rl), _resp_head(rq), _resp_body(rb){}HttpResponse(std::string &rl, std::string &rb): _resp_line(rl), _resp_body(rb){}// 序列化bool serialize(std::string &out_str){}~HttpResponse(){}
private:std::string _resp_line; // 响应头std::vector<std::string> _resp_head; // 响应报头std::string _resp_body; // 响应体
};
实现HttpResponse
序列化接口
实现serialize
就会比实现deserialize
接口简单,只需要根据对应的字段加上\r\n
即可,所以基本代码如下:
// 序列化
bool serialize(std::string &out_str)
{// 给请求行添加\r\n_resp_line += default_sep;out_str += _resp_line;// 给请求报头的每一个字段加上\r\nstd::for_each(_resp_head.begin(), _resp_head.end(), [&](std::string &str){str += default_sep;out_str += str; });// 添加空行out_str += default_sep;out_str += _resp_body;return true;
}
修改HttpServer
类
只需要改变HttpServer
类中的请求处理函数,但是如果要打印反序列的结果就必须提供对应的接口或者在HttpRequest
类内提供打印函数,本次考虑后者:
void print()
{// 请求行LOG(LogLevel::INFO) << "请求行:" << _req_line;// 请求报头std::for_each(_req_head.begin(), _req_head.end(), [&](std::string& str){ LOG(LogLevel::INFO) << "请求报头:" << str; });// 请求体LOG(LogLevel::INFO) << "请求体:" << _req_body;
}
接着修改HttpServer
的handleHttpRequest
函数:
void handleHttpRequest(SockAddrIn sock_addr_in, int ac_socketfd)
{LOG(LogLevel::INFO) << "收到来自:" << sock_addr_in.getIp() << ":" << sock_addr_in.getPort() << "的连接";// 获取客户端传来的HTTP请求base_socket_ptr bs = _tp->getSocketPtr();std::string in_str;bs->recvData(in_str, ac_socketfd);// 反序列化HttpRequest req;req.deserialize(in_str);// 打印反序列结果req.print();// 构建HttpResponse返回std::string line = "HTTP 1.1 200 OK";std::string body = "<h1>Build HttpResponse success</h1>";HttpResponse resp(line, body);std::string out_str;// 序列化resp.serialize(out_str);LOG(LogLevel::INFO) << out_str;bs->sendData(out_str, ac_socketfd);
}
测试
主函数与上一节一样,测试结果如下:
从上图可以看到可以成功获取到HTTP请求结果并且正常回复HTTP响应,第一阶段目标完成
第二阶段
在第一阶段的基础之上,现在需要对HTTP请求和HTTP响应的每一个字段进行细化,本次不考虑某个字段或者属性是什么含义,只需要将其进行提取即可
修改HttpRequest
类
提取HTTP请求行中的字段
因为HTTP请求行中的每个字段是根据空格进行分隔的,回忆C/C++的输入和输出,默认也是以空白字符进行分隔,所以就可以利用这一点,可以使用C语言的sscanf()
进行读取,也可以考虑使用C++的stringstream进行
因为需要读取到每个字段,所以需要对应的成员进行接收,这里就使用三个成员_req_method
、_req_uri
和_req_ver
作为补充成员:
class HttpRequest
{
public:// ...private:std::string _req_method; // 请求方法std::string _req_uri; // 请求资源路径std::string _req_ver; // HTTP请求版本// ...
};
接着就是实现一个函数用于从req_line
中提取对应的字段填充_req_method
、_req_uri
和_req_ver
三个成员:
=== “sscanf
版本”
// sscanf版本
bool getContentFromReqLine()
{char method[1024] = {0};char uri[1024] = {0};char ver[1024] = {0};sscanf(_req_line.c_str(), "%s%s%s", method, uri, ver);LOG(LogLevel::INFO) << "请求行:" << method << "-" << uri << "-" << ver;_req_method = method;_req_uri = uri;_req_ver = ver;return true;
}
=== “stringstream
版本”
// stringstream版本bool getContentFromReqLine(){std::stringstream ss;// 读取到stringstream中ss << _req_line;// 输出到成员中ss >> _req_method >> _req_uri >> _req_ver;LOG(LogLevel::INFO) << "请求行:" << _req_method << "-" << _req_uri << "-" << _req_ver;return true;}
接下来修改deserialize
的逻辑:
bool deserialize(std::string &in_str)
{// 截取请求行bool getReqLineFlag = parseOneLineFromTotal(in_str, _req_line);if (!getReqLineFlag){LOG(LogLevel::WARNING) << "反序列化获取请求行失败";return false;}LOG(LogLevel::INFO) << "截取的请求头为:" << _req_line;// 填充请求行的字段getContentFromReqLine();// ...return true;
}
提取HTTP请求报头中的字段
前面完成了获取到HTTP请求报头中的每一条数据,但是请求报头实际上是key-value
结构的数据,服务器需要拿到其中的key
以及value
进行后续的处理,所以这里就需要分别取出key
和对应的value
为了存储对应的key
和value
,可以考虑使用一个哈希表。这里,因为处理每一个每一条报头数据不需要经过_req_head
过渡,所以可以考虑直接将分割出的字符串传递给处理分隔的函数,在该函数中直接将对应的键值对添加到哈希表即可
每一个键值对字符串以
:
分隔,而不是:
首先完成分割逻辑:
bool getPairFromReqHead(std::string& oneLine)
{// 找到分隔符auto pos = oneLine.find(default_head_sep);// 左侧即为keystd::string key = oneLine.substr(0, pos);// 右侧即为valuestd::string value = oneLine.substr(pos + default_head_sep.size());// 插入到哈希表中_kv.insert({key, value});return true;
}
接下来修改deserialize
和getReqHeadFromTotal
的逻辑:
=== “getReqHeadFromTotal
”
// 获取请求报头
bool getReqHeadFromTotal(std::string &in_str)
{// 保存以\r\n结尾的一行数据std::string oneLine;while (true){bool getOneLineFlag = parseOneLineFromTotal(in_str, oneLine);// 如果getOneLineFlag为true并且oneLine不为空,说明当前行有效,否则代表已经找到了结尾if (getOneLineFlag && !oneLine.empty()){getPairFromReqHead(oneLine);}// ...}return true;
}
=== “deserialize
”
// 反序列化
bool deserialize(std::string &in_str)
{// ...// 截取请求报头bool getReqHeadLine = getReqHeadFromTotal(in_str);if (!getReqHeadLine){LOG(LogLevel::WARNING) << "反序列化获取请求报头失败";return false;}LOG(LogLevel::INFO) << "获取到的请求报头为:";std::for_each(_kv.begin(), _kv.end(), [&](std::pair<std::string, std::string> kv){LOG(LogLevel::INFO) << kv.first << "-" << kv.second;});// ...return true;
}
提取HTTP请求体中的字段
保持和第一阶段的处理方式一样
修改HttpResponse
类
第一阶段的HttpResponse
类只是使用一个固定的字符串进行序列化再发送给客户端,这个做法明显是不妥的。实际上,对于HTTP响应来说,应该处理下面几点:
- 允许根据请求内容是否合法自动构建响应状态码,并且根据状态码自动生成状态码描述
- 允许外部添加响应报头中的属性
- 根据客户端请求的内容读取服务端存在的对应文件并返回给客户端,没有时返回404页面
HTTP状态码
根据上面的思路,首先需要处理的就是状态码,在介绍HTTP协议基本结构与基本实现HTTPServer一节提到,HTTP协议规定任何客户端的请求都必须得到响应,而区分响应情况就是通过状态码
在HTTP中,状态码有以下几种:
类别 | 原因短语 |
---|---|
1XX | Informational(信息性状态码)接收的请求正在处理 |
2XX | Success(成功状态码)请求正常处理完毕 |
3XX | Redirection(重定向状态码)需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码)服务器无法处理请求 |
5XX | Server Error(服务器错误状态码)服务器处理请求出错 |
但是,仅有开头还不足以说明具体的问题,所以每一种类别下还有具体的状态码和对应描述,因为状态码太多,所以下面仅仅展示常见的状态码:
状态码 | 类别 | 描述 | 示例场景 |
---|---|---|---|
100 | 信息响应 | 请求已接收,客户端应继续发送请求 | 客户端询问服务器是否支持某些功能 |
101 | 信息响应 | 切换协议 | 客户端请求切换到WebSocket协议 |
200 | 成功响应 | 请求成功 | 页面加载成功 |
201 | 成功响应 | 资源创建成功 | 创建新用户或上传文件 |
204 | 成功响应 | 请求成功但无内容返回 | 删除操作后不返回任何内容 |
301 | 重定向 | 永久重定向 | 网站迁移至新域名 |
302 | 重定向 | 临时重定向 | 用户登录后跳转到主页 |
304 | 重定向 | 资源未修改,使用缓存 | 浏览器缓存的资源未过期 |
400 | 客户端错误 | 请求无效或无法被服务器理解 | 请求参数缺失或格式错误 |
401 | 客户端错误 | 未授权访问 | 用户未提供身份验证 |
403 | 客户端错误 | 禁止访问 | 用户权限不足 |
404 | 客户端错误 | 资源未找到 | 请求的页面或API不存在 |
405 | 客户端错误 | 方法不允许 | 使用了不支持的HTTP方法(如POST代替GET) |
429 | 客户端错误 | 请求过多 | 超过API速率限制 |
500 | 服务器错误 | 内部服务器错误 | 服务器代码逻辑出错 |
502 | 服务器错误 | 错误网关 | 服务器作为网关时收到无效响应 |
503 | 服务器错误 | 服务不可用 | 服务器过载或维护中 |
504 | 服务器错误 | 网关超时 | 服务器作为网关时未能及时从上游获取响应 |
其中,更为常见的就是200(OK)、404(Not Found)、403(Forbidden)、302(Redirect)和504(Bad Gateway)
本次优先考虑200(OK)和404(Not Found),对于重定向将在后面的章节介绍,此处不具体描述
处理响应行
首先是HTTP响应版本,这个字段可以设置为一个固定值,因为一般情况下只会在升级的时候更改,所以可以考虑使用一个字符串指定
接着是HTTP响应状态码和状态码描述,因为本次只考虑200和404,所以只有两种情况:
- 用户请求的资源存在
- 用户请求的资源不存在
根据这两种情况,需要考虑的问题就是如何判断用户请求的资源是否存在。前面提到,URI
就是资源路径,也就是说,用户想要拿到的资源就在URI
中。根据这一点得出「判断用户请求的资源是否存在」只需要「在当前服务器的资源目录中找到对应的文件是否存在」即可。现在的问题就转变为「如何判断一个文件是否存在」,这里可以使用文件流读取对应的文件,如果文件不存在就给用户返回一个空字符串,否则就将读取到的文件添加到结果字符串即可
根据上面的思路,首先就是要获取到URI
,这一点其实在HttpRequest
中已经做到了,所以在HttpResponse
中需要获取一下即可,这里可以考虑让读取内容的函数接收一个URI
字符串,所以获取文件内容函数的基本逻辑如下:
// 获取文件内容
std::string getFileContent(std::string& uri)
{// 当前uri中即为用户需要的文件,使用文件流打开文件std::fstream f(uri);// 如果文件为空,直接返回空字符串if(!f.is_open())return std::string();// 否则就读取文件内容std::string content;std::string line;while (std::getline(f, line))content += line;f.close();return content;
}
接着,在创建一个函数用于处理获取URI
以及构建文件内容,前面提到可以使用HttpResponse
对象获取到对应的URI
,所以当前函数需要接收一个HttpRequest
对象,并且在HttpRequest
类中需要提供获取URI
的函数:
=== “获取URI
函数”
// 获取URIstd::string getReqUri(){return _req_uri;}
=== “处理URI
以及构建文件内容函数”
void buildHttpResponse(HttpRequest& req)
{// 获取uristd::string req_uri = req.getReqUri();// 根据uri获取文件内容std::string content = getFileContent(req_uri);
}
现在已经解决了文件问题,也就是说现在可以根据文件是否存在决定状态码的值和描述,根据前面的思路可以得出文件存在会返回空字符串,那么此时就说明状态码应该是404,否则就是200,所以这里就可以通过是否为空设置对应的状态码,而状态码描述可以通过状态码进行匹配,例如:
// 根据状态码得到状态码描述
std::string setStatusCodeDesc(int status_code)
{switch (status_code){case 200:return "OK";case 404:return "Not Found";default:break;}return std::string();
}
接下来继续完善构建函数buildHttpResponse
,因为要设置状态码和状态码描述,所以需要两个成员接收这两个值,便于后面构建HTTP响应结构:
void buildHttpResponse(HttpRequest &req)
{// 获取uristd::string req_uri = req.getReqUri();// 根据uri获取文件内容std::string content = getFileContent(req_uri);if (content.empty()){// 如果为真,说明文件不存在// 设置状态码为404并设置状态码描述_status_code = 404;_status_code_desc = setStatusCodeDesc(_status_code);}else{// 文件存在_status_code = 200;_status_code_desc = setStatusCodeDesc(_status_code);}
}
处理完状态码和状态码描述之后,接下来就是将HTTP版本、状态码和状态码描述构建出一个HTTP响应行,首先修改原有的构造函数,删除不需要的字段:
// 默认HTTP版本
const std::string default_http_ver = "HTTP/1.1";
class HttpResponse
{
public:HttpResponse(): _http_ver(default_http_ver){}private:std::string _http_ver; // HTTP版本// ...
};
接着,在buildHttpResponse
函数中添加构建请求行的逻辑:
void buildHttpResponse(HttpRequest &req)
{// ...// 构建请求行_resp_line = _http_ver + std::to_string(_status_code) + _status_code_desc;
}
处理响应行
根据前面的要求「允许外部添加响应报头中的属性」,需要提供一个哈希表存储key
和value
,所以首先需要创建一个类成员,接着添加一个函数用于执行添加逻辑:
class HttpResponse
{
public:// ...void insertRespHead(std::string& key, std::string& value){_kv[key] = value;}// ...private:// ...std::unordered_map<std::string, std::string> _kv;// ...
};
处理响应体
根据客户端请求的内容读取服务端存在的对应文件并返回给客户端,没有时返回404页面,所以只需要在buildHttpResponse
中根据是否有文件内容给定具体文件即可,对于存在对应的文件的,因为getFileContent
返回的就是读取到的文件内容,所以直接将结果给响应头即可,但是对于404页面,并没有读取到一个实际的文件内容,这里可以考虑直接给getFileContent
写入一个固定文件,即404文件,这样该函数获取到的文件内容就是404文件
有了基本思路,现在就是缺少这类文件。在添加文件之前,先仔细了解一下不带参数的URI
,在介绍HTTP协议基本结构与基本实现HTTPServer中提到了/
开始不一定是系统根目录,而是Web应用根目录,这个Web应用根目录实际上就是当前服务器程序所在目录下的一个文件夹,这个文件夹下放着一些静态资源,例如HTML、图片、视频、CSS、JavaScript等,所以客户端想访问资源本质就是让服务器在这个文件夹中找到对应的文件并将其中的内容返回给客户端
有了上面的概念,下面就是在当前服务器程序所在的目录创建一个Web应用根目录,基本目录结构如下:
主程序目录- Web应用根目录- src- HTML文件- assets- stylesheets- CSS文件- JavaScripts- JavaScript文件- public- images- 图片文件- videos- 视频文件- HttpServer程序
上面的目录只是一个参考目录,并不是固定的,可以根据自己或者其他地方的规定进行调整,下面将以这个目录结构为例演示,当前创建的目录结构如下:
HttpServer- wwwroot- src- assets- js- style- public- images- videos- HttpServer_main
接着,为了演示出客户端正常接收到服务器存在的文件以及404文件,需要在src
目录下创建两个HTML文件,此处不给出具体的HTML文件代码
接着,修改buildHttpResponse
逻辑,确保可以读取到文件并将文件内容存储到响应体中:
// 404页面固定路径
std::string default_404_page = "wwwroot/src/404.html";void buildHttpResponse(HttpRequest &req)
{// 获取uristd::string req_uri = req.getReqUri();// 根据uri获取文件内容std::string content = getFileContent(req_uri);if (content.empty()){// 如果为真,说明文件不存在// 设置状态码为404并设置状态码描述_status_code = 404;_status_code_desc = setStatusCodeDesc(_status_code);// 读取404页面并设置响应体_resp_body = getFileContent(default_404_page);}else{// 文件存在_status_code = 200;_status_code_desc = setStatusCodeDesc(_status_code);// 设置响应体_resp_body = content;}// 构建请求行_resp_line = _http_ver + std::to_string(_status_code) + _status_code_desc;
}
修改HttpServer
类
因为HttpServer
需要调用sendData
函数,该函数需要传入一个字符串作为发送数据,而HTTP响应中存在一个序列化函数,可以调用这个函数可以传入一个字符串,并将序列化后的字符串存储到参数的字符串中,这样就可以实现发送。所以整体handleHttpRequest
逻辑修改如下:
void handleHttpRequest(SockAddrIn sock_addr_in, int ac_socketfd)
{// ...// 构建HTTP响应HttpResponse resq;resq.buildHttpResponse(req);// 序列化std::string out_str;resq.serialize(out_str);// 发送给客户端bs->sendData(out_str, ac_socketfd);
}
测试
主函数与上一节一样,测试结果如下:
从上面的测试结果可以发现,的确可以接收到数据,如果将地址栏的内容修改为localhost:8080/index.html
,结果如下:
这个测试结果并不是像预期的那样显示主页的内容,而是显示404页面。那么明明存在index.html
文件,为什么会出现无法读取到index.html
?这是因为在解析URI
时并没有考虑到URI
起始的/
,实际上getFileContent
函数得到的uri
字符串内容是/index.html
,而已有的index.html
文件路径为wwwroot/src/index.html
,所以还需要在已有的uri
上还需要加入默认的Web应用目录wwwroot/src
,修改如下:
// Web应用路径
std::string default_webapp_dir = "wwwroot/";// HTML文件路径
std::string default_html_dir = "src";// 404页面固定路径
std::string default_404_page = default_webapp_dir + default_html_dir + "/404.html";void buildHttpResponse(HttpRequest &req)
{// 获取uri// ...// 补充uristd::string real_uri = default_webapp_dir + default_html_dir + req_uri;// 根据uri获取文件内容std::string content = getFileContent(real_uri);// ...
}
再次测试,结果如下:
优化
从上面的测试可以发现,如果想要访问index.html
文件还需要手动加上/index.html
,但是访问一个实际的网站时,尽管没有携带/index.html
,依旧可以访问到网站的index.html
文件,例如访问百度的首页:
默认访问:
在网址后添加/index.html
:
因为直接输入网址,浏览器请求的默认就是Web应用根目录下的某一个文件,只是默认情况下隐藏了IP地址+端口号后的/
,实现这个效果的方式很简单,只需要在getFileContent
函数的开始判断是否是/
,如果是就直接返回index.html
的内容即可。思路的确如此,但是在上面先处理了传递给getFileContent
的参数为添加了wwwroot/src
的字符串,所以实际上如果只有一个/
,那么传递给getFileContent
函数的参数为wwwroot/src/
,所以修改如下:
// 获取文件内容
std::string getFileContent(std::string &uri)
{// 默认访问index.html文件if(uri == "wwwroot/src/")uri = "wwwroot/src/index.html";// ...
}
再次测试,观察结果:
可以发现已经默认到了index.html
的内容
虽然上面的思路的确可以实现问题,但是如果默认以/
结尾,那么只要判断最后字符串是否是/
即可,这样不论前面的内容是什么,只要是以/
结尾,都可以访问到主页,所以还可以修改为:
// 获取文件内容
std::string getFileContent(std::string &uri)
{// 默认访问index.html文件if(uri.back() == '/')uri = "wwwroot/src/index.html";// ...
}
现在,不论前面是什么都可以访问到index.html
,哪怕是不存在的目录:
相关文章:
Linux与深入HTTP序列化和反序列化
深入HTTP序列化和反序列化 本篇介绍 在上一节已经完成了客户端和服务端基本的HTTP通信,但是前面的传递并没有完全体现出HTTP的序列化和反序列化,为了更好得理解其工作流程,在本节会以更加具体的方式分析到HTTP序列化和反序列化 本节会在介绍…...
音视频入门基础:RTP专题(20)——通过FFprobe显示RTP流每个packet的信息
通过FFprobe命令: ffprobe -protocol_whitelist "file,rtp,udp" -of json -show_packets XXX.sdp 可以显示SDP描述的RTP流每个packet(数据包)的信息: 对于RTP流,上述的“packet”(数据包&#…...
Java Web 大文件上传优化:从困境到高效
文章目录 Java Web 大文件上传优化:从困境到高效一、优化前的困境(一)内存占用问题(二)上传速度缓慢(三)稳定性欠佳 二、优化后的实现方案(一)客户端(Vue&…...
C++——STL 常用的查找算法
算法简介: find //查找元素find_if //按条件查找元素adjacent_find //查找相邻重复元素binary_search //二分查找法count //统计元素个数count_if //按条件统计元素个数 1. find 功能描述: 查找指定元素,找到返回指定元素的迭…...
【一次成功】Win10本地化单机部署k8s v1.31.2版本及可视化看板
【一次成功】Win10本地化单机部署k8s v1.31.2版本及可视化看板 零、安装清单一、安装Docker Desktop软件1.1 安装前<启用或关闭Windows功能> 中的描红的三项1.2 查看软件版本1.3 配置Docker镜像 二、更新装Docker Desktop三、安装 k8s3.1 点击启动安装3.2 查看状态3.3 查…...
Vulkan视频解码decode显示display之同步
在ReleaseDisplayedPicture函数中消耗图片资源并且显示display完成,设置两个标志m_hasConsummerSignalFence true 和m_hasConsummerSignalSemaphore true virtual int32_t ReleaseDisplayedPicture(DecodedFrameRelease** pDecodedFramesRelease, uint32_t nu…...
专题|Python梯度提升实例合集:GBM、XGBoost、SMOTE重采样、贝叶斯、逻辑回归、随机森林分析信贷、破产数据...
全文链接:https://tecdat.cn/?p41051 分析师:Jiajie Shi,Yimeng Li 在当今数据驱动的时代,数据分析师和数据建模师面临着各式各样复杂且极具挑战性的任务。本专题合集便是围绕这些挑战展开的宝贵知识盛宴(点击文末“阅…...
4.0 相机引导XY轴控制螺丝枪打螺丝
假如一个产品的同一水平上要打6个螺钉,是通过伺服XY轴移动带动相机以及螺丝枪,由相机拍照,根据拍照后螺丝孔位置来引导伺服进行移动以对准螺丝孔位置的。步骤如下: 一、9点标定,即把相机与伺服的实际位置关联起来。步骤…...
【ElasticSearch】学习笔记
一、lucene的组成 segment是一个具备完整搜索功能的最小单元。 多个segment组成了一个单机文本检索库lucene。 inverted index:倒排索引,用于快速根据关键词找到对应的文章term index: 构建出关键词的目录树,解决了term dictionary数据量过大ÿ…...
Spring Boot整合RabbitMQ极简教程
一、消息队列能解决什么问题? 异步处理:解耦耗时操作(如发短信、日志记录)流量削峰:应对突发请求,避免系统过载应用解耦:服务间通过消息通信,降低依赖 二、快速整合RabbitMQ 1. 环…...
代码随想录-04-字符串-03.替换数字
替换数字 题目 给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为number。 例如,对于输入字符串 “a1b2c3”,函数应该将其转换为 “anum…...
Tailwindcss开启黑夜模式
本篇讲述如何使用tailwindcss切换白天黑夜主题 tailwindcss自带的暗夜切换会比css自带的theme主体切换来得方便很多,学习成本也很低,只要求会用tailiwndcss 1,tailwindcss.config有两种暗夜模式切换,媒体查询和手动类切换。手动控…...
AI与人的智能,改变一生的思维模型【7】易得性偏差
目录 **易得性偏差思维模型:大脑的「热搜算法」与反操纵指南****病毒式定义:你的大脑正在被「热搜」劫持****四大核心攻击路径与史诗级案例****1. 信息过载时代的「认知短路」****2. 媒体放大器的「恐怖滤镜」****3. 个人经验的「数据暴政」****4. 社交茧…...
有序表--跳表
实现一种结构,支持如下操作,要求单次调用的时间复杂度O(log n) 1,增加x,重复加入算多个词频 2,删除x,如果有多个,只删掉一个 3,查询x的排名,x的排名为,比x小的…...
双指针---字符串替换数字(数字替换为“number“)
题目链接:替换数字 要求:时间复杂度为O(n) 思路: 1、先将字符串扩容到要输出串的长度。 2、从后向前替换数字字符,也就是双指针法,newIndex指向新长度的末尾,i指向旧长度的末尾。 #include<iostream&g…...
外星人入侵-Python-三
武装飞船 开发一个名为《外星人入侵》的游戏吧!为此将使用 Pygame,这是一组功能强大而有趣的模块,可用于管理图形、动画乃至声音, 让你能够更轻松地开发复杂的游戏。通过使用Pygame来处理在屏幕上绘制图像 等任务,可将…...
JavaScript相关面试题
以下是150道JavaScript相关面试题及详细答案: JavaScript基础 1.JavaScript是什么? JavaScript是一种直译式脚本语言,主要用于网页开发,也可用于服务器端开发(如Node.js)。它是一种动态类型、弱类型、基于原…...
常见的数学模型
数学模型的基本原理 简单来说,数学模型就是用数学语言来描述现实世界中的现象或规律。它就像一个“翻译器”,把复杂的现实问题转化成我们可以用数学方法解决的问题。 核心思想: 简化现实:现实世界太复杂,模型会抓住最…...
计算机四级 - 数据库原理 - 第3章 「关系数据库系统概述」
3.1 关系数据库系统概述 关系数据模型的三大要素:关系数据结构、关系操作集合(一次一个集合)和关系完整性约束 1. 关系语言的特点是高度非过程化的, DBMS会自动帮用户选择存取路径,用户不需要依靠循环和递归完成数据的重复操作。…...
使用PHP进行自动化测试:工具与策略的全面分析
使用PHP进行自动化测试:工具与策略的全面分析 引言 随着软件开发的复杂性不断增加,自动化测试已成为确保软件质量的关键环节。PHP作为一种广泛使用的服务器端脚本语言,拥有丰富的生态系统和工具支持,使其成为自动化测试的理想选…...
discuz门户文章允许游客评论
discuz开启游客评论 1、进入后台,用户--用户组--系统用户组--游客--编辑 2、论坛相关 设置未允许发表回复 3、门户相关--文章评论字数(设置此用户组发表文章评论字数限制,设置为0将禁止此用户组发表评论) 4、验证游客回复 测试站 http://jinzhu.zhaowo.…...
AtCoder Beginner Contest 003(A - 社の給料、B -トランプ、C -プログラミング講座、D - 社の冬 )题目讲解
前言 又更新AtCoder Beginner Contes 的题目讲解啦!! 希望能给诸位带来帮助。 话不多说,开始讲解: A - 社の給料←题目翻译 为了解决这个问题,我们需要计算青木每月完成正好N个任务时的平均工资。通过分析,我们可以发现这个问题可以通过数学公式直接求解,而不需要复…...
代码随想录二刷|图论11
图论 一、基础知识 1 无向图 (1)度:一个顶点连n条边就度为n (2)权 加权无向图:有边长的无向图 (3)通道:两个顶点之间有一些边和点,并且没有重复的边 路…...
农资出入库登记本,农药化肥库存出入库软件,佳易王农资管理庄稼医院开单管理系统操作教程
一、概述 本实例以佳易王农资管理庄稼医院开单管理系统为例说明,其他版本可参考本实例。试用版软件资源可到文章最后了解,下载的文件为压缩包文件,请使用免费版的解压工具解压即可试用。 软件特点: 1、功能实用,操作简…...
串的KMP算法详解
KMP算法深度解析 一、从暴力匹配到智能跳转: 在文本编辑器的搜索功能中,当我们在百万字的文档中查找特定关键词时,传统暴力匹配算法的时间复杂度高达O(mn)。KMP算法通过独创的部分匹配表(Partial Match Table)&#x…...
软件测试之测试分类
1. 为什么要对软件测试进行分类 软件测试是软件⽣命周期中的⼀个重要环节,具有较⾼的复杂性,对于软件测试,可以从不同的⻆度 加以分类,使开发者在软件开发过程中的不同层次、不同阶段对测试⼯作进⾏更好的执⾏和管理测试 的分类⽅…...
机器学习 : 训练过程
文章目录 概要流程1 . 前向传播2 . 计算损失3 . 后向传播4 . 梯度下降 技术名词解释小结 【全文大纲】 : https://blog.csdn.net/Engineer_LU/article/details/135149485 概要 主要思想拟合数据 流程 1 . 前向传播 y func * (wxb) 2 . 计算损失 y - Y 3 . 后向传播 根据链式法…...
六十天前端强化训练之第二十天React Router 基础详解
欢迎来到编程星辰海的博客讲解 看完可以给一个免费的三连吗,谢谢大佬! 目录 一、核心概念 1.1 核心组件 1.2 路由模式对比 二、核心代码示例 2.1 基础路由配置 2.2 动态路由示例 2.3 嵌套路由实现 2.4 完整示例代码 三、关键功能实现效果 四、…...
如何在AVL树中高效插入并保持平衡:一步步掌握旋转与平衡因子 —— 旋转篇
文章目录 AVL树种旋转的规则右单旋右单旋代码左单旋左单旋代码左右双旋左右单旋的代码右左单旋右左单旋的代码 AVL树种旋转的规则 在AVL树中,旋转是为了保持树的平衡性。AVL树是一种自平衡的二叉搜索树,它要求每个节点的左右子树的高度差不能超过1。当插…...
C++Primer学习(7.1 定义抽象数据类型)
类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是种依赖于接口(interface)和实现(implementation)分离的编程(以及设计)技术。类的接口包括用户所能执行的操作:类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。 封…...
Vue 3 Diff 算法深度解析:与 Vue 2 双端比对对比
文章目录 1. 核心算法概述1.1 Vue 2 双端比对算法1.2 Vue 3 快速 Diff 算法 2. 算法复杂度分析2.1 时间复杂度对比2.2 空间复杂度对比 3. 核心实现解析3.1 Vue 2 双端比对代码3.2 Vue 3 快速 Diff 代码 4. 性能优化分析4.1 性能测试数据4.2 内存使用对比 5. 使用场景分析5.1 Vu…...
启动桌面Docker提示虚拟服务未启动
在启动 Docker Desktop 时,可能会遇到以下提示: Docker Desktop - Virtual Machine Platform not enabled Virtual Machine Platform not enabled该错误通常是由于 Windows 未启用 “Virtual Machine Platform” 功能导致的,这是运行 Docker…...
【SpringBoot】实现登录功能
在上一篇博客中,我们讲解了注册页面的实现。在此基础上会跳转到登录页面,今天给大家带来的是使用 SpringBoot,MyBatis,Html,CSS,JavaScript,前后端交互实现一个登录功能。 目录 一、效果 二、…...
DataWhale 速通AI编程开发:(进阶篇)第3章 提示词(Prompts)配置项
学习网址:Datawhale-学用 AI,从此开始 3.1 Roo Code提示词配置了什么 众所周知,提示词(Prompt)是用户向大语言模型输入的一段文本,用于指导大语言模型生成符合用户要求的输出。在ai编程领域更是如此,提示…...
VUE中VNode(虚拟节点)是个啥?
用 JavaScript 生成 Virtual DOM(VNode) 在 Vue 中,Virtual DOM(虚拟 DOM)是一个用 JavaScript 对象表示真实 DOM 结构的抽象层。通过这种方式,Vue 可以通过比较 Virtual DOM 与真实 DOM 的差异来最小化更…...
力扣:3. 无重复字符的最长子串(滑动窗口)
3. 无重复字符的最长子串 - 力扣(LeetCode)3. 无重复字符的最长子串 - 给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。 示例 1:输入: s "abcabcbb"输出: 3 解释: 因为无重复字符的最长子串是 "abc"…...
注解+AOP实现权限控制
注解与AOP实战:实现权限控制 在现代Java开发中,注解(Annotation)和面向切面编程(AOP)是两种强大的技术,它们能够帮助我们实现代码的解耦,提高代码的可读性和可维护性。本文将通过一…...
2.5 python接口编程
在现代软件开发的复杂生态系统中,不同系统、模块之间的交互协作至关重要。接口编程作为一种关键机制,定义了组件之间的通信规范与交互方式。Python 凭借其卓越的灵活性、丰富的库资源以及简洁易读的语法,在接口编程领域占据了重要地位&#x…...
睡不着运动锻炼贴士
在快节奏的现代生活中,失眠似乎已成为许多人的“夜间伴侣”。夜晚辗转反侧,白天精神不振,这样的恶性循环让许多人苦不堪言。其实,除了调整作息和饮食习惯,适当的运动也是改善睡眠的一剂良药。今天,就让我们…...
【Python入门】一篇掌握Python中的字典(创建、访问、修改、字典方法)【详细版】
🌈 个人主页:十二月的猫-CSDN博客 🔥 系列专栏: 🏀《Python/PyTorch极简课》_十二月的猫的博客-CSDN博客 💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 目…...
深入理解 HTML 表单与输入
在网页开发的广袤领域中,HTML 表单如同搭建用户与服务器沟通桥梁的基石。它是收集用户输入信息的关键渠道,承载着交互的重任。今天,就让我们一同深入探索 HTML 表单与输入的奥秘。 HTML 表单在文档中划定出一片独特的区域,这片…...
宝塔docker切换存储目录
1、 停止 Docker 服务 sudo systemctl stop docker2、迁移 Docker 数据目录 sudo mkdir -p /newpath/docker sudo rsync -avz /var/lib/docker/ /newpath/docker/3、修改 Docker 配置文件 vi /etc/docker/daemon.json 内容 {"data-root": "/newpath/docker&q…...
IPoIB驱动中RSS与TSS技术的深度解析:多队列机制与性能优化
在高速网络通信中,IP over InfiniBand(IPoIB) 是实现低延迟、高吞吐的关键技术之一。为了充分发挥多核处理器的性能潜力,IPoIB驱动通过 接收侧扩展(RSS) 和 发送侧扩展(TSS) 技术,实现了数据包处理的多队列并行化。本文结合源码实现与性能优化策略,深入解析其核心机制…...
目前人工智能的发展,判断10年、20年后的人工智能发展的主要方向,或者带动的主要产业
根据2025年的最新行业研究和技术演进趋势,结合历史发展轨迹,未来10-20年人工智能发展的主要方向及带动的产业将呈现以下六大核心趋势: 一、算力革命与底层架构优化 核心地位:算力将成为类似“新能源电池”的基础设施,…...
DeepSeek-prompt指令-当DeepSeek答非所问,应该如何准确的表达我们的诉求?
当DeepSeek答非所问,应该如何准确的表达我们的诉求?不同使用场景如何向DeepSeek发问?是否有指令公式? 目录 1、 扮演专家型指令2、 知识蒸馏型指令3、 颗粒度调节型指令4、 时间轴推演型指令5、 极端测试型6、 逆向思维型指令7、…...
并发编程面试题二
1、java线程常见的基本状态有哪些,这些状态分别是做什么的 (1)创建(New):new Thread(),生成线程对象。 (2)就绪(Runnable):当调用线程对象的sta…...
【NLP】 8. 处理常见词(Stopwords)的不同策略
处理常见词(Stopwords)的不同策略 在自然语言处理 (NLP) 和信息检索 (IR) 任务中,常见词(Stopwords) 是指在文本中频繁出现但通常对主要任务贡献较小的词,例如 “the”、“is”、“in”、“and” 等。这些…...
【Java基础】java中的lambda表达式
Java Lambda表达式深度解析:语法、简化规则与实战 前言 Java 8的Lambda表达式通过简化匿名内部类和引入函数式编程,极大提升了代码的简洁性和可读性。 一、Lambda表达式的核心语法 Lambda表达式由参数列表、->符号和表达式主体组成,其基…...
【RS】OneRec快手-生成式推荐模型
note 本文提出了一种名为 OneRec 的统一生成式推荐框架,旨在替代传统的多阶段排序策略,通过一个端到端的生成模型直接生成推荐结果。OneRec 的主要贡献包括: 编码器-解码器结构:采用稀疏混合专家(MoE)架构…...
DQN 玩 2048 实战|第一期!搭建游戏环境(附 PyGame 可视化源码)
视频讲解: DQN 玩 2048 实战|第一期!搭建游戏环境(附 PyGame 可视化源码) 代码仓库:GitHub - LitchiCheng/DRL-learning: 深度强化学习 2048游戏介绍,引用维基百科 《2048》在44的网格上进行。…...