Linux与HTTP报头属性和请求方式
HTTP报头属性、请求方式
本篇介绍
在上一节深入HTTP序列化和反序列化已经详细讲解了HTTP是如何进行序列化和反序列化的,但是上一节对请求报头和响应报头的具体内容并没有做出具体的说明,本节就会基于这个问题继续探讨HttpServer
;另外在介绍HTTP协议基本结构与基本实现HTTPServer一节提到,HTTP请求的方式有很多种,而最常见的就是GET
和POST
,那么什么是请求方式,GET
和POST
这两者又有什么区别也是本节需要探讨的话题。所以综上本节主要就解决两个问题:
- 何为报头属性
- 何为请求方式,具体的请求方式又有什么区别
HTTP报头属性
HTTP报头一共有两种,分别是请求报头和响应报头。虽然有两种报头,但是二者的报头属性是一样的,所以接下来会以HTTP请求报头为例对报头进行介绍,再以HTTP响应报头演示HTTP报头如何进行设置
认识HTTP报头属性
在HTTP报头中有很多属性,每一个属性都是以键值对的方式表示,例如在深入HTTP序列化和反序列化第一阶段结果中就有一些报头属性,如图所示:
上面的每个字段和值的解释如下:
图片中的请求报头包含了多个字段,每个字段都有其特定的含义。以下是对每个字段和值的解释:
- Host: localhost:8080 - 指定服务器的主机名和端口号。客户端通过这个字段告诉服务器它想要访问的主机
- Connection: keep-alive - 表示客户端希望与服务器保持连接,以便在同一连接上发送多个请求
- sec-ch-ua: “Not;A=Brand”;v=“24”, “Chromium”;v=“128” - 表示客户端的用户代理品牌和版本信息。
sec-ch-ua
是一个客户端提示头,用于提供用户代理的品牌和版本信息 - sec-ch-ua-mobile: ?0 - 表示客户端是否为移动设备。
?0
表示不是移动设备 - sec-ch-ua-platform: “Linux” - 表示客户端操作系统平台。这里是
Linux
- DNT: 1 - 表示客户端不希望被追踪。
1
表示启用了“请勿追踪”功能 - Upgrade-Insecure-Requests: 1 - 表示客户端希望服务器将不安全的HTTP请求升级为 HTTPS请求
- User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 - 表示客户端的用户代理字符串,包含了浏览器和操作系统的信息。这里表示使用的是Chrome浏览器,运行在Linux x86_64平台上
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 - 表示客户端可以接受的MIME类型(鼠标悬浮/手指点击时显示更多信息)。这里表示客户端可以接受HTML、XHTML、XML、AVIF、WebP、APNG等格式的内容
- Sec-Fetch-Site: none - 表示请求的上下文。
none
表示请求不是从其他站点发起的 - Sec-Fetch-Mode: navigate - 表示请求的模式。
navigate
表示这是一个导航请求 - Sec-Fetch-User: ?1 - 表示请求是否由用户触发。
?1
表示是由用户触发的请求 - Sec-Fetch-Dest: document - 表示请求的目的。
document
表示请求的目的是获取一个文档 - Accept-Encoding: gzip, deflate, br, zstd - 表示客户端可以接受的内容编码。这里表示客户端可以接受gzip、deflate、br和zstd编码的内容
- Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,en-US;q=0.7 - 表示客户端可以接受的语言。这里表示客户端可以接受英语(en)、简体中文(zh-CN)、中文(zh)和美式英语(en-US)
上面的解释只需要了解即可,下面针对常见的报头属性进行说明:
- Host: 客户端告知服务器,所请求的资源是在哪个主机的哪个端口上
- User-Agent: 声明用户的操作系统和浏览器版本信息
- Referer: 当前页面是从哪个页面跳转过来的
- Location: 搭配
3xx
状态码使用,告诉客户端接下来要去哪里访问 - Content-Type: 数据类型
- Content-Length: Body的长度
- Cookie: 用于在客户端存少量信息。通常用于实现会话(session)的功能,关于Cookie和Session会在后面的章节讲解,此处不具体说明
在上面常见的报头属性中存在三类:
=== “只出现在请求报头中”
- Host: 这是HTTP/1.1规范中唯一必须包含在请求中的字段,用于指定服务器的主机名和端口号
- User-Agent: 只出现在请求中,表示客户端应用程序的信息
- Referer: 只出现在请求中,表示用户从哪个页面链接过来的
- Cookie: 只出现在请求中,客户端发送之前服务器存储的Cookie信息
=== “只出现在响应报头中”
- Location: 只出现在响应中,主要配合3xx重定向状态码使用
- Set-Cookie: 只出现在响应中,用于服务器指示客户端保存Cookie,这个字段需要搭配客户端的Cookie使用,这一点会在后面的Cookie中详细介绍
=== “可能同时出现在请求或者响应报头中”
- Content-Type: 在请求和响应中都可以出现,表示实体的媒体类型
- Content-Length: 在请求和响应中都可以出现,表示实体主体的大小
在HTTP响应报头中演示
上面认识到了常见的报头属性,但是也是文字上的了解,具体怎么做还并不知道,所以接下来就是在HTTP响应中使用这些报头属性
需要注意,因为是在HTTP报头中演示,所以只会演示上面可以出现在响应报头中的属性
Content-Type
和Content-Length
在前面客户端和服务端通信时,都是将内容读取然后直接发给客户端,但是这里存在一个问题,服务端知道文件有多大,也知道文件的结尾在哪里,而因为HTTP是基于TCP的,有可能客户端收到的数据并不是完整的,却被客户端误认为读到了文件结尾,这时就会出现客户端显示的内容并不一定是正确且完整的,所以为了尽可能避免这个问题,在服务端给客户端响应数据时通常响应报头需要携带Content-Length
,通过这个属性,客户端就可以知道自己是否读取到了完整的数据
但是,客户端有文件的大小还不够,因为HTTP协议不仅可以传递文本信息,还可以传递一些媒体信息,例如图片、视频等,如果客户端只知道文件大小而不知道文件类型,那么就可能出现二进制文件被当成文本文件进行解析从而导致显示的内容异常,所以服务端给客户端响应数据时除了需要响应Content-Length
外,还需要给客户端响应文件类型,即Content-Type
虽然正确设置这些HTTP头部是最佳实践,但在之前的例子中没有使用这些头部,主要是因为浏览器会自动推断文本文件的类型(通常默认为
text/plain
或根据内容判断为text/html
),且早期HTTP实现中服务器可通过直接关闭连接来表示传输结束。然而,在实际生产环境中,应该正确设置Content-Type
和Content-Length
,以确保客户端正确解析内容类型、确认完整接收数据并支持持久连接的正常运行
现在目录下有4张图片:
在HTML中引入这4张图片:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<!-- ... -->
<title>商城</title>
<!-- ... -->
</head>
<body>
<!-- ... -->
<div class="container">
<h2 class="section-title">热卖推荐</h2>
<div class="product-grid">
<div class="product-card">
<div class="product-img">
<img src="../assets/public/images/1.png" />
</div>
<!-- ... -->
</div>
<div class="product-card">
<div class="product-img">
<img src="../assets/public/images/2.png" />
</div>
<!-- ... -->
</div>
<div class="product-card">
<div class="product-img">
<img src="../assets/public/images/3.png" />
</div>
<!-- ... -->
</div>
<div class="product-card">
<div class="product-img">
<img src="../assets/public/images/4.png" />
</div>
<!-- ... -->
</div>
</div>
</div>
</body>
</html>
正常显示结果如下:
接下来,让服务器读取这个文件并发送给客户端,不过为了更好得展现出客户端正在向服务端请求的资源,可以在客户端获取资源时打印出URI
,即:
void buildHttpResponse(HttpRequest &req)
{
// 获取uri
std::string req_uri = req.getReqUri();
LOG(LogLevel::INFO) << "客户端正在请求:" << req_uri;
// ...
}
运行服务器和客户端,查看结果:
可以看到图片并没有显示,但是这里可能有两种可能:
- 客户端没有请求图片
- 图片发送失败
为了验证是第二种结果,接下来看是否存在刚才加的一行打印,结果如下:
可以发现,客户端的确请求了四张图片,说明第一种可能不存在,但是这些图片并没有正常显示,说明没有客户端没有正常获取到这些图片,如果查看一下浏览器调试就可以看到原因:
这里,Sec-Fetch-Dest
表示客户端需要一张图片,接着再看Response
一栏结果:
可以发现,服务端发送给客户端的数据被当成了文本进行解析,但是因为图片是二进制文件,导致直接解析成文本也没有结果
从上面的例子可以看出,如果没有图片,那么解析都是正常的,因为默认识别为文本,但是一旦是媒体文件就会出现错误
为了可以正常显示图片和文本,原来的文本方式读取就不再可用,所以接下来就要对getFileContent
函数进行修改,使其可以读取二进制文件(包括文本文件),修改思路如下:
前面提到客户端需要知道文件的大小以便正确解析文件,所以需要设置文件大小,这里可以考虑添加一个成员单独保存这个长度值,也可以考虑将存储文件内容的容器规定为文件的大小,这样存储文件内容的容器的有效数据大小就是文件大小,本次以后者为例
接下来就是考虑如何获取到文件大小,其中一种方式就是移动读取光标,然后获取光标的偏移量,第二种方式就是利用C++17的filesystem中提供的库函数file_size
快速获取文件的大小:
=== “移动光标读取偏移量”
// 使用光标偏移量获取文件大小
size_t getFileSize(std::string& file)
{
// 以二进制、光标在文件结尾的方式打开文件
std::fstream f(file, std::ios::binary | std::ios::ate);
if(!f.is_open())
return 0;
return static_cast<size_t>(f.tellg());
}
=== “使用file_size
库函数”
// 使用file_size函数获取文件大小
size_t getFileSize(std::filesystem::path filepath)
{
return static_cast<size_t>(std::filesystem::file_size(filepath));
}
接着,完善getFileContent
函数:
// 获取文件内容
std::string getFileContent(std::string &uri)
{
// 默认访问index.html文件
if (uri.back() == '/')
uri = "wwwroot/src/index.html";
// 当前uri中即为用户需要的文件,使用二进制方式打开文件
std::fstream f(uri, std::ios::in | std::ios::binary);
// 如果文件为空,直接返回空字符串
if (!f.is_open())
return std::string();
// 否则就读取文件内容
std::string content;
std::string line;
// 先获取文件大小
size_t filesize = getFileSize(uri);
// 调整容器容量
content.resize(filesize);
f.read(const_cast<char *>(content.c_str()), filesize);
LOG(LogLevel::INFO) << "读取到的文件大小为:" << filesize;
f.close();
return content;
}
修改了读取文件的方式后,代表文件可以被正常读取,接着就是设置Content-Type
和Content-Length
对于Content-Length
来说,只需要获取一下容器的有效数据大小即可,但是Content-Type
就没那么容易了,那么可以通过什么方式获取文件类型?
实际上,Content-Type
的值为MIME类型,在一些网站(例如MDN)中提供了可用文件的后缀对应的MIME类型
本次只需要用到三种类型:
- 后缀
.mp4
:对应的MIME类型为application/mp4
- 后缀
.png
:对应的MIME类型为image/png
- 后缀
.html
:对应的MIME类型为text/html
接下来,就需要处理两件事:
- 获取文件后缀:处理方式为反向查找
.
,从该位置开始到结尾即为文件后缀 - 根据文件后缀选择对应的MIME类型字符串:简单的字符串比较
=== “获取文件后缀”
// 获取文件后缀
std::string getFileSuffix(std::string& file)
{
// 反向查找.
auto pos = file.rfind(default_file_suffix_flag);
if(pos == std::string::npos)
return std::string();
// 截取文件后缀字符串
return file.substr(pos);
}
=== “获取MIME
类型”
// 根据文件后缀获取MIME类型字符串
std::string getFileMimeType(const std::string& suffix)
{
if(suffix == ".mp4")
return "application/mp4";
else if (suffix == ".png")
return "image/png";
else if (suffix == ".html")
return "text/html";
return std::string();
}
需要注意,不推荐正向查找文件后缀。因为操作系统的逻辑一般都是以最后一个后缀为标识,例如
script.min.js
,正向查找会找到.min
,而不是真正的扩展名.js
以及data.backup.2023.csv
,正向查找会找到.backup
,而不是.csv
接着就是完善buildHttpResponse
函数,因为媒体文件的位置与HTML文件的位置不同,所以在设置实际URI
时也需要根据后缀判断选择哪一个拼接方式,并且还需要在设置请求体之前先设置响应报头:
void buildHttpResponse(HttpRequest &req)
{
// 获取uri
std::string req_uri = req.getReqUri();
// 拼接HTML文件路径
std::string real_uri;
if(getFileSuffix(req_uri) == ".html")
real_uri = default_webapp_dir + default_html_dir + req_uri;
else if(getFileSuffix(req_uri) == ".png")
real_uri = default_webapp_dir + req_uri;
// ...
// 构建响应报头
// 设置文件大小
insertRespHead("Content-Length", std::to_string(content.size()));
// 设置文件类型
insertRespHead("Content-Type", getFileMimeType(getFileSuffix(real_uri)));
// ...
}
需要注意,在上面的代码中会发现媒体资源文件的路径和HTML文件拼接方式不同,这是因为在HTML文件中,使用的是
../assets/public/images/1.png
这种引用方式,而因为../
是返回上级目录,也就是相对HTML文件来说是Web应用根目录,所以实际上请求资源时路径为/assets/public/images/1.png
,所以只需要在前面拼接上wwwroot
即可
接着,还需要处理一种特殊情况,即客户端的默认请求/
,对于这一点,如果按照上面的逻辑,就会让real_uri
是一个空字符串传递给getFileContent(),此时就会出现问题,所以为了避免这种情况,还需要将getFileContent
中单独处理/
的逻辑移动到buildHttpResponse
中:
void buildHttpResponse(HttpRequest &req)
{
// ...
// 默认访问index.html文件
std::string real_uri;
if (req_uri.back() == '/')
real_uri = "wwwroot/src/index.html";
else if(getFileSuffix(req_uri) == ".html")
real_uri = default_webapp_dir + default_html_dir + req_uri;
else if(getFileSuffix(req_uri) == ".png")
real_uri = default_webapp_dir + req_uri;
// ...
}
最后,编译运行上面的代码,观察结果:
可以发现图片都可以正常显示了,再查看调试器可以看到在上面设置的两个报头属性:
另外,为了保证响应格式的确没有问题,可以使用Postman向服务器发起请求:
可以看到正常接收到结果且结果正常
重定向与Location
上面已经介绍了两个基本的属性,这两个属性涉及到网页能否正常显示出了文本以外的内容。接下来还存在一个属性,就是Location
,这个属性只出现在响应报头,表示服务器需要客户端重定向到一个具体的网址,重定向对应的状态码种类有下面三类:
=== “临时重定向”
- 302 Found - 最常用的临时重定向状态码
- 303 See Other - 特别用于将POST请求重定向到GET请求
- 307 Temporary Redirect - 临时重定向,但严格保持原始请求方法不变
=== “永久重定向”
- 301 Moved Permanently - 资源已永久移动到新位置
- 308 Permanent Redirect - 永久重定向,但严格保持原始请求方法不变
=== “其他重定向”
- 300 Multiple Choices - 表示请求有多个可能的响应
- 304 Not Modified - 缓存重定向,表示资源未修改
一旦服务器设置了Location
响应头,客户端(浏览器)就会自动向Location
的值对应的URL发起请求访问新的页面
下面以临时重定向为例,实现这一个功能,以一个场景为例:当前主页并不是index.html
而是index1.html
,再不改变原来默认请求index.html
代码的情况下,通过Location
自动跳转到index1.html
为了实现这个功能,首先需要一个index1.html
接着,在buildHttpResponse
函数中设置重定向属性,注意,因为是直接重定向,所以可以直接在请求/
就直接返回一个完整的HTTP响应结构,因为此处还需要设置对应的状态码和状态码描述,所以还需要对获取状态码描述的函数进行修改:
=== “buildHttpResponse
函数”
void buildHttpResponse(HttpRequest &req)
{
// ...
if (req_uri.back() == '/')
{
// ...
// 请求旧主页时重定向
// 设置重定向位置
insertRespHead("Location", "http://127.0.0.1:8080/index1.html");
// 设置响应状态码和状态码描述
// 设置重定向状态码和描述
_status_code = 302; // 临时重定向
_status_code_desc = setStatusCodeDesc(_status_code);
// 设置一个简单的响应体
_resp_body = "Redirecting to new version...";
// 设置响应头部
insertRespHead("Content-Type", "text/html");
insertRespHead("Content-Length", std::to_string(_resp_body.size()));
// 构建响应行
_resp_line = _http_ver + " " + std::to_string(_status_code) + " " + _status_code_desc;
return;
}
// ...
}
=== “setStatusCodeDesc
函数”
// 根据状态码得到状态码描述
std::string setStatusCodeDesc(int status_code)
{
switch (status_code)
{
case 200:
return "OK";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 404:
return "Not Found";
default:
break;
}
return std::string();
}
再次启动服务器并运行就可以发现原来显示的是index.html
的页面内容,现在默认显示的是index1.html
中的内容:
HTTP请求参数
在实际生活中,有的时候显示的网页不只有给用户看的内容,还需要有与用户交互的内容,例如登录、注册等,当用户向登录或者注册中一些输入框输入一些数据后点击提交时,这些数据会被携带着一起发送给服务器,这些由用户输入并发送给服务器的数据就是HTTP请求参数
在HTTP中,由下面几种请求方式:
GET
POST
PUT
DELETE
HEAD
PATCH
OPTIONS
TRACE
CONNECT
但是,虽然上面列举了9种请求方式,实际上最常用的就只有前两种,即GET
和POST
,下面针对二者做一下区分:
GET
参数通过URL
传递,POST
通过请求体传递GET
请求有长度限制,POST
没有严格限制GET
请求可被缓存,POST
通常不缓存GET
相对不安全,POST
相对更安全GET
具有幂等性,POST
通常非幂等
基于上面的概念,下面为了演示出GET
和POST
的区别,需要先准备一个登录页面
下面重点关注表单部分:
<form method="get">
<!-- ... -->
</form>
当前默认是GET
请求,运行服务器请求该页面,输入内容并点击提交观察结果:
再将请求方式修改为POST
请求,观察结果:
<form method="post">
<!-- ... -->
</form>
可以看到,如果是GET
请求,参数就是直接放在URI
的后方,而如果是POST
,参数则在请求体中,这也证明了前面GET
和POST
区别的第一点
当服务端需要客户端发送数据时,这个数据肯定是需要被服务端拿去使用的,例如比对账号是否存在于数据库,但是因为本次并不存在数据库以及访问数据库的操作,所以本次演示就只是从HTTP请求中获取到对应的值即可
因为GET
和POST
对请求参数的处理是不同的,所以需要分为两种情况处理,一般对于一个请求来说,如果是GET
请求,那么请求的参数会放在URI
之后,以?
开头,每一个属性都是key=value
的形式,多个属性之间用&
进行连接,但是如果是POST
请求,那么请求的参数就放在请求正文部分,同样每一个属性都是key=value
的形式,默认情况下,多个属性之间用&
进行连接
这里可以通过一个函数包装分情况处理的逻辑,而因为需要存储响应体,所以这里考虑使用一个哈希表分别存储每一个键值对,所以基本结构如下:
// HTTP请求
class HttpRequest
{
public:
// ...
// 获取请求参数
void getReqParams()
{
if(_req_method == "GET")
{
// 处理GET请求中的参数
}
else if(_req_method == "POST")
{
// 处理POST请求中的参数
}
}
// ...
private:
// ...
std::unordered_map<std::string, std::string> _param_kv; // 请求参数
// ...
};
首先处理GET
请求中的参数,前面提到,GET
请求的参数位于URI
的后方中,所以需要从URI中截取出参数,因为参数起始的字符是?
,所以从?
开始查找,该字符的下一个位置就是第一个参数键值对,当找到第一个&
就代表找到了一个完整的参数键值对,现在就只需要将这个参数字符串提取出来存储到哈希表中即可:
=== “获取GET
中的参数”
// 获取GET参数
void getReqParamsFromReqLine()
{
// 找到?的位置
auto pos = _req_uri.find(default_get_param_start_flag);
if (pos == std::string::npos)
return;
// ?username=123&password=123
while (true)
{
// 找到&
auto pos1 = _req_uri.find(default_param_sep, pos + 1);
if (pos1 == std::string::npos)
{
// 最后一个参数键值对
auto pos_t = _req_uri.find(default_kv_sep, pos + 1);
if (pos_t == std::string::npos)
break;
std::string key = _req_uri.substr(pos + default_get_param_start_flag.size(), pos_t - pos - 1);
std::string value = _req_uri.substr(pos_t + 1);
_param_kv.insert({key, value});
break;
}
// 找到=,pos表示查找起始位置
auto pos2 = _req_uri.find(default_kv_sep, pos + 1);
// pos2 - pos - 1表示截取的最后一个字符,不包括
std::string key = _req_uri.substr(pos + default_get_param_start_flag.size(), pos2 - pos - 1);
std::string value = _req_uri.substr(pos2 + default_kv_sep.size(), pos1 - pos2 - 1);
_param_kv.insert({key, value});
// 修改起始位置
pos = pos1;
}
// 恢复pos
pos = start_param;
// 从URI中移除参数
_req_uri.erase(pos);
for (auto kv : _param_kv)
{
std::cout << kv.first << ":" << kv.second << std::endl;
}
}
=== “获取POST
中的参数”
// 处理POST请求参数
void getReqParamsFromBody()
{
if (_req_body.empty())
return;
// username=123&password=123
auto pos = size_t(-1); // 初始位置设为-1,因为POST参数没有?前缀
while (true)
{
// 找到&
auto pos1 = _req_body.find(default_param_sep, pos + 1);
if (pos1 == std::string::npos)
{
// 最后一个参数键值对
auto pos_t = _req_body.find(default_kv_sep, pos + 1);
if (pos_t == std::string::npos)
break;
std::string key = _req_body.substr(pos + 1, pos_t - pos - 1);
std::string value = _req_body.substr(pos_t + 1);
_param_kv.insert({key, value});
break;
}
// 找到=
auto pos2 = _req_body.find(default_kv_sep, pos + 1);
if (pos2 == std::string::npos || pos2 > pos1)
break;
std::string key = _req_body.substr(pos + 1, pos2 - pos - 1);
std::string value = _req_body.substr(pos2 + 1, pos1 - pos2 - 1);
_param_kv.insert({key, value});
// 修改起始位置
pos = pos1;
}
// 打印解析结果
for (const auto &kv : _param_kv)
{
std::cout << kv.first << ":" << kv.second << std::endl;
}
}
=== “处理参数函数”
// 获取请求参数
void getReqParams()
{
if(_req_method == "GET")
{
// 处理GET请求中的参数
getReqParamsFromReqLine();
}
else if(_req_method == "POST")
{
// 处理POST请求中的参数
getReqParamsFromBody();
}
}
需要注意,在上面的
getReqParamsFromReqLine
函数中,在最后要处理一下从URI
中移除参数,否则获取完参数后URI
还携带参数影响后续其他逻辑执行
最后,在反序列化函数中调用处理参数函数:
// 反序列化
bool deserialize(std::string &in_str)
{
// ...
// 获取参数内容
getReqParams();
return true;
}
接下来,分别在GET
请求和POST
请求下测试:
=== “GET
请求”
=== “POST
请求”
静态资源和动态资源
介绍与准备
在上面以及之前两节中,服务器都是向客户端直接响应字符串或者一个静态资源,但是有的时候不只有静态资源,还有很多的动态资源,例如在表单提交时form
标签内的action
字段的值就是对应的一个动态资源
以一个实例表单为例:
<form action="/login" method="get">
<!-- ... -->
</form>
在这个表单中,action
的值即为一个动态资源,这个动态资源一般是一个函数,有HTTP服务器调用上层的函数去完成,这里依旧是以登录为例:
<!-- ... -->
<body>
<!-- ... -->
<div class="container">
<div class="login-container">
<h2 class="login-title">账号登录</h2>
<form action="/login" method="get">
<!-- ... -->
</form>
</div>
</div>
</body>
<!-- ... -->
因为动态资源一般都是为了处理请求中的参数,所以可以对前面获取到的参数进行处理,同样考虑简单的处理。这里设计一个布尔类型的成员变量_hasArgs
,标记是否有参数,如果_hasArgs
为真,那么就需要上层去执行任务,否则就返回静态资源。而因为判断是否有参数的函数在HttpRequest
中,所以这里将_hasArgs
放在HttpRequest
中:
// HTTP请求
class HttpRequest
{
public:
HttpRequest()
:_hasArgs(false)
{
}
// ...
private:
// ...
bool _hasArgs; // 是否有参数
};
接着,提供一个函数获取_hasArgs
的值方便上层调用:
// 获取_hasArgs
bool getHasArgs()
{
return _hasArgs;
}
最后,在获取到参数时更改_hasArgs
的值,但是这里不可以直接修改_hasArgs
,而应该是成功获取了GET或者POST请求中的参数才可以修改,所以还需要为getReqParamsFromReqLine
和getReqParamsFromBody
设计一个返回值,用于判断是否执行成功:
=== “getReqParamsFromReqLine
函数添加布尔返回值”
// 获取GET参数
bool getReqParamsFromReqLine()
{
LOG(LogLevel::INFO) << "进入获取GET参数" << _req_uri;
// 找到?的位置
auto pos = _req_uri.find(default_get_param_start_flag);
auto start_param = pos;
// 找不到返回false
if (pos == std::string::npos)
return false;
// ?username=123&password=123
while (true)
{
// 找到&
auto pos1 = _req_uri.find(default_param_sep, pos + 1);
if (pos1 == std::string::npos)
{
// 最后一个参数键值对
auto pos_t = _req_uri.find(default_kv_sep, pos + 1);
// 找不到返回false
if (pos_t == std::string::npos)
return false;
std::string key = _req_uri.substr(pos + default_get_param_start_flag.size(), pos_t - pos - 1);
std::string value = _req_uri.substr(pos_t + 1);
_param_kv.insert({key, value});
break;
}
// 找到=,pos表示查找起始位置
auto pos2 = _req_uri.find(default_kv_sep, pos + 1);
// pos2 - pos - 1表示截取的最后一个字符,不包括
std::string key = _req_uri.substr(pos + default_get_param_start_flag.size(), pos2 - pos - 1);
std::string value = _req_uri.substr(pos2 + default_kv_sep.size(), pos1 - pos2 - 1);
_param_kv.insert({key, value});
// 修改起始位置
pos = pos1;
}
// 恢复pos
pos = start_param;
// 从URI中移除参数
_req_uri.erase(pos);
LOG(LogLevel::INFO) << "离开获取GET参数" << _req_uri;
// 找到返回true
return true;
}
=== “getReqParamsFromBody
函数添加布尔返回值”
// 处理POST请求参数
bool getReqParamsFromBody()
{
// 找不到返回false
if (_req_body.empty())
return false;
// username=123&password=123
auto pos = size_t(-1); // 初始位置设为-1,因为POST参数没有?前缀
while (true)
{
// 找到&
auto pos1 = _req_body.find(default_param_sep, pos + 1);
if (pos1 == std::string::npos)
{
// 最后一个参数键值对
auto pos_t = _req_body.find(default_kv_sep, pos + 1);
// 找不到返回false
if (pos_t == std::string::npos)
return false;
std::string key = _req_body.substr(pos + 1, pos_t - pos - 1);
std::string value = _req_body.substr(pos_t + 1);
_param_kv.insert({key, value});
break;
}
// 找到=
auto pos2 = _req_body.find(default_kv_sep, pos + 1);
// 找不到返回false
if (pos2 == std::string::npos || pos2 > pos1)
return false;
std::string key = _req_body.substr(pos + 1, pos2 - pos - 1);
std::string value = _req_body.substr(pos2 + 1, pos1 - pos2 - 1);
_param_kv.insert({key, value});
// 修改起始位置
pos = pos1;
}
// 找到返回true
return true;
}
=== “获取请求参数函数”
void getReqParams()
{
if (_req_method == "GET")
{
_hasArgs = true;
// 处理GET请求中的参数
getReqParamsFromReqLine();
}
else if (_req_method == "POST")
{
_hasArgs = true;
// 处理POST请求中的参数
getReqParamsFromBody();
}
}
所谓的上层调用,就是让HttpServer
去调用上层传递的函数,所以接下来设计HttpServer
修改HttpServer
既然HttpServer
需要调用上层函数,那么就需要HttpServer
提供一个入口,因为执行的函数可能不止一个,所以这里考虑建立一张动态资源和函数映射的哈希表,本次简单处理,函数都是void(HttpRequest& req, HttpResponse& resp)
的函数:
using handler_t = std::function<void()>;
class HttpServer
{
public:
// ...
private:
// ...
std::unordered_map<std::string, handler_t> _dynamic_func;
};
接着,需要向上层提供一个添加处理动态资源函数的接口:
void pushDynamicTask(std::string name, handler_t handler)
{
_dynamic_func[name] = handler;
}
另外,为了保证执行正常,可以提供一个判断接口,用于判断需要执行的函数是否存在:
bool hasFunc(std::string name)
{
auto pos = _dynamic_func.find(name);
return pos != _dynamic_func.end();
}
最后就是对handleHttpRequest
进行处理,只需要根据getHasArgs
函数的返回值即可判断是否执行动态资源处理函数:
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);
HttpResponse resp;
if(req.getHasArgs())
{
// 处理动态资源
// 动态资源即在URI中
std::string service = req.getReqUri();
// 根据service查找哈希表执行对应的函数
if(hasFunc(service))
_dynamic_func[service](req, resp);
}
else
{
// 处理静态资源
// 构建HTTP响应
resp.buildHttpResponse(req);
}
// ...
}
测试
为了在调用动态资源处理函数时可以看到一些数据,这里考虑在HttpRequest
和HttpResponse
中提供获取参数的函数:
void getParamKv()
{
std::for_each(_param_kv.begin(), _param_kv.end(), [&](std::pair<std::string, std::string> kv)
{
std::cout << kv.first << ":" << kv.second << std::endl;
});
}
接着,提供一个处理/login
动态资源的函数:
void login(HttpRequest &req, HttpResponse &resp)
{
LOG(LogLevel::DEBUG) << "进入登录模块";
req.getParamKv();
}
修改主函数如下:
int main(int argc, char* argv[])
{
uint16_t port = std::stoi(argv[1]);
std::shared_ptr<HttpServer> hs = std::make_shared<HttpServer>(port);
// 注册处理动态资源的函数
hs->pushDynamicTask("/login", login);
hs->start();
return 0;
}
编译上面的代码并打开login.html
,输入内容并点击提交按钮,观察结果:
=== “GET
请求”
=== “POST
请求”
可以看到不论是GET
还是POST
都执行了动态资源