HTTP报头属性和请求方式
约 5508 个字 1189 行代码 26 张图片 预计阅读时间 33 分钟
本篇介绍
在上一节深入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会在后面的章节讲解,此处不具体说明
在上面常见的报头属性中存在三类:
在HTTP响应报头中演示
上面认识到了常见的报头属性,但是也是文字上的了解,具体怎么做还并不知道,所以接下来就是在HTTP响应中使用这些报头属性
Note
需要注意,因为是在HTTP报头中演示,所以只会演示上面可以出现在响应报头中的属性
Content-Type
和Content-Length
在前面客户端和服务端通信时,都是将内容读取然后直接发给客户端,但是这里存在一个问题,服务端知道文件有多大,也知道文件的结尾在哪里,而因为HTTP是基于TCP的,有可能客户端收到的数据并不是完整的,却被客户端误认为读到了文件结尾,这时就会出现客户端显示的内容并不一定是正确且完整的,所以为了尽可能避免这个问题,在服务端给客户端响应数据时通常响应报头需要携带Content-Length
,通过这个属性,客户端就可以知道自己是否读取到了完整的数据
但是,客户端有文件的大小还不够,因为HTTP协议不仅可以传递文本信息,还可以传递一些媒体信息,例如图片、视频等,如果客户端只知道文件大小而不知道文件类型,那么就可能出现二进制文件被当成文本文件进行解析从而导致显示的内容异常,所以服务端给客户端响应数据时除了需要响应Content-Length
外,还需要给客户端响应文件类型,即Content-Type
为什么上一节发送文本文件不需要传递Content-Type
和Content-Length
虽然正确设置这些HTTP头部是最佳实践,但在之前的例子中没有使用这些头部,主要是因为浏览器会自动推断文本文件的类型(通常默认为text/plain
或根据内容判断为text/html
),且早期HTTP实现中服务器可通过直接关闭连接来表示传输结束。然而,在实际生产环境中,应该正确设置Content-Type
和Content-Length
,以确保客户端正确解析内容类型、确认完整接收数据并支持持久连接的正常运行
现在目录下有4张图片:

在HTML中引入这4张图片:
HTML |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 | <!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
,即:
C++ |
---|
| 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
快速获取文件的大小:
接着,完善getFileContent
函数:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 | // 获取文件内容
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类型字符串:简单的字符串比较
Note
需要注意,不推荐正向查找文件后缀。因为操作系统的逻辑一般都是以最后一个后缀为标识,例如script.min.js
,正向查找会找到.min
,而不是真正的扩展名.js
以及data.backup.2023.csv
,正向查找会找到.backup
,而不是.csv
接着就是完善buildHttpResponse
函数,因为媒体文件的位置与HTML文件的位置不同,所以在设置实际URI
时也需要根据后缀判断选择哪一个拼接方式,并且还需要在设置请求体之前先设置响应报头:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | 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)));
// ...
}
|
Note
需要注意,在上面的代码中会发现媒体资源文件的路径和HTML文件拼接方式不同,这是因为在HTML文件中,使用的是../assets/public/images/1.png
这种引用方式,而因为../
是返回上级目录,也就是相对HTML文件来说是Web应用根目录,所以实际上请求资源时路径为/assets/public/images/1.png
,所以只需要在前面拼接上wwwroot
即可
接着,还需要处理一种特殊情况,即客户端的默认请求/
,对于这一点,如果按照上面的逻辑,就会让real_uri
是一个空字符串传递给getFileContent(),此时就会出现问题,所以为了避免这种情况,还需要将getFileContent
中单独处理/
的逻辑移动到buildHttpResponse
中:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | 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
,这个属性只出现在响应报头,表示服务器需要客户端重定向到一个具体的网址,重定向对应的状态码种类有下面三类:
一旦服务器设置了Location
响应头,客户端(浏览器)就会自动向Location
的值对应的URL发起请求访问新的页面
下面以临时重定向为例,实现这一个功能,以一个场景为例:当前主页并不是index.html
而是index1.html
,再不改变原来默认请求index.html
代码的情况下,通过Location
自动跳转到index1.html
为了实现这个功能,首先需要一个index1.html
:
新版主页
HTML |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398 | <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>优选商城 - 新版</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f5f5;
}
/* 头部样式升级 */
header {
background: linear-gradient(135deg, #FF4500, #ff6a00);
padding: 15px 0;
color: white;
box-shadow: 0 2px 10px rgba(255, 69, 0, 0.2);
position: sticky;
top: 0;
z-index: 1000;
}
.container {
width: 90%;
max-width: 1200px;
margin: 0 auto;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
}
.logo {
font-size: 28px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
/* 搜索框升级 */
.search-box {
flex-grow: 1;
margin: 0 40px;
position: relative;
}
.search-box input {
width: 100%;
padding: 12px 20px;
border: none;
border-radius: 25px;
outline: none;
font-size: 16px;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.search-box input:focus {
background: #fff;
box-shadow: 0 0 15px rgba(255, 255, 255, 0.3);
}
.search-box button {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
height: 80%;
padding: 0 25px;
background: #ff6a00;
border: none;
color: white;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
}
.search-box button:hover {
background: #e43c00;
box-shadow: 0 0 10px rgba(228, 60, 0, 0.5);
}
/* 用户操作升级 */
.user-actions {
display: flex;
align-items: center;
gap: 20px;
}
.user-actions a {
color: white;
text-decoration: none;
padding: 8px 15px;
border-radius: 20px;
transition: all 0.3s ease;
position: relative;
}
.user-actions a:hover {
background: rgba(255, 255, 255, 0.2);
}
/* 导航栏升级 */
.main-nav {
background: rgba(228, 60, 0, 0.9);
padding: 12px 0;
backdrop-filter: blur(10px);
}
.main-nav ul {
display: flex;
list-style-type: none;
gap: 30px;
}
.main-nav a {
color: white;
text-decoration: none;
font-weight: 500;
padding: 5px 0;
position: relative;
}
.main-nav a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: white;
transition: width 0.3s ease;
}
.main-nav a:hover::after {
width: 100%;
}
/* 轮播区域 */
.carousel {
margin: 20px 0;
border-radius: 10px;
overflow: hidden;
position: relative;
height: 400px;
}
.carousel img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 产品区域升级 */
.section-title {
margin: 40px 0 20px;
padding-bottom: 15px;
border-bottom: 2px solid #ddd;
position: relative;
font-size: 24px;
}
.section-title::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100px;
height: 2px;
background: #FF4500;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 25px;
padding: 20px 0;
}
.product-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.product-card:hover {
transform: translateY(-10px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.product-img {
height: 220px;
background: #f8f8f8;
border-radius: 8px;
margin-bottom: 15px;
overflow: hidden;
}
.product-img img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.product-card:hover .product-img img {
transform: scale(1.1);
}
.product-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.product-price {
display: flex;
align-items: center;
gap: 10px;
margin-top: 15px;
}
.current-price {
color: #ff4500;
font-size: 22px;
font-weight: bold;
}
.price-original {
color: #999;
font-size: 14px;
text-decoration: line-through;
}
/* 底部区域 */
footer {
background: #333;
color: white;
padding: 40px 0;
margin-top: 50px;
}
.footer-content {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30px;
}
.footer-section h3 {
margin-bottom: 20px;
font-size: 18px;
}
.footer-section ul {
list-style: none;
}
.footer-section ul li {
margin-bottom: 10px;
}
.footer-section a {
color: #ccc;
text-decoration: none;
transition: color 0.3s;
}
.footer-section a:hover {
color: #ff4500;
}
</style>
</head>
<body>
<header>
<div class="container">
<div class="header-top">
<div class="logo">优选商城</div>
<div class="search-box">
<input type="text" placeholder="搜索你想要的商品">
<button>搜索</button>
</div>
<div class="user-actions">
<a href="/login">登录</a>
<a href="/register">注册</a>
<a href="/cart">购物车</a>
<a href="/orders">我的订单</a>
</div>
</div>
</div>
</header>
<nav class="main-nav">
<div class="container">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/categories">全部分类</a></li>
<li><a href="/new">新品上市</a></li>
<li><a href="/hot">热卖商品</a></li>
<li><a href="/promotion">优惠活动</a></li>
</ul>
</div>
</nav>
<div class="container">
<div class="carousel">
<img src="../assets/public/images/banner.png" alt="促销活动">
</div>
<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 class="product-title">耳机1</div>
<div class="product-price">¥4999 <span class="price-original">¥5999</span></div>
</div>
<div class="product-card">
<div class="product-img">
<img src="../assets/public/images/2.png" />
</div>
<div class="product-title">耳机2</div>
<div class="product-price">¥3999 <span class="price-original">¥4599</span></div>
</div>
<div class="product-card">
<div class="product-img">
<img src="../assets/public/images/3.png" />
</div>
<div class="product-title">耳机3</div>
<div class="product-price">¥299 <span class="price-original">¥399</span></div>
</div>
<div class="product-card">
<div class="product-img">
<img src="../assets/public/images/4.png" />
</div>
<div class="product-title">耳机4</div>
<div class="product-price">¥899 <span class="price-original">¥1099</span></div>
</div>
</div>
</div>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3>关于我们</h3>
<ul>
<li><a href="#">公司简介</a></li>
<li><a href="#">招贤纳士</a></li>
<li><a href="#">联系我们</a></li>
</ul>
</div>
<div class="footer-section">
<h3>客户服务</h3>
<ul>
<li><a href="#">帮助中心</a></li>
<li><a href="#">售后服务</a></li>
<li><a href="#">配送说明</a></li>
</ul>
</div>
<div class="footer-section">
<h3>商家合作</h3>
<ul>
<li><a href="#">商家入驻</a></li>
<li><a href="#">营销中心</a></li>
<li><a href="#">物流合作</a></li>
</ul>
</div>
<div class="footer-section">
<h3>关注我们</h3>
<ul>
<li><a href="#">微信公众号</a></li>
<li><a href="#">新浪微博</a></li>
<li><a href="#">企业微信</a></li>
</ul>
</div>
</div>
</div>
</footer>
</body>
</html>
|
接着,在buildHttpResponse
函数中设置重定向属性,注意,因为是直接重定向,所以可以直接在请求/
就直接返回一个完整的HTTP响应结构,因为此处还需要设置对应的状态码和状态码描述,所以还需要对获取状态码描述的函数进行修改:
再次启动服务器并运行就可以发现原来显示的是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
通常非幂等
幂等性
幂等性(Idempotency)是指一个操作,无论执行一次还是执行多次,产生的效果(结果状态)都是相同的。GET
请求之所以设计为幂等的就是为了在网络不稳定情况下可以安全重试请求,但是POST
因为一般会随着用户的操作而得到不同的结果,所以POST
大部分情况下是非幂等的
基于上面的概念,下面为了演示出GET
和POST
的区别,下面需要先准备一个登录页面
登录页面
HTML |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197 | <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 优选商城</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f5f5;
}
/* 头部样式 */
header {
background-color: #FF4500;
padding: 10px 0;
color: white;
}
.container {
width: 90%;
max-width: 1200px;
margin: 0 auto;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
}
.logo {
font-size: 24px;
font-weight: bold;
}
/* 登录框样式 */
.login-container {
max-width: 400px;
margin: 50px auto;
padding: 30px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.login-title {
text-align: center;
font-size: 24px;
color: #333;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #666;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus {
border-color: #FF4500;
outline: none;
}
.login-btn {
width: 100%;
padding: 12px;
background-color: #FF4500;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.login-btn:hover {
background-color: #e43c00;
}
.form-footer {
margin-top: 20px;
text-align: center;
color: #666;
}
.form-footer a {
color: #FF4500;
text-decoration: none;
margin-left: 10px;
}
.form-footer a:hover {
text-decoration: underline;
}
/* 记住我复选框样式 */
.remember-me {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.remember-me input {
margin-right: 8px;
}
/* 第三方登录样式 */
.third-party-login {
margin-top: 30px;
text-align: center;
border-top: 1px solid #eee;
padding-top: 20px;
}
.third-party-login p {
color: #999;
margin-bottom: 15px;
}
.third-party-icons {
display: flex;
justify-content: center;
gap: 20px;
}
.third-party-icons img {
width: 40px;
height: 40px;
cursor: pointer;
transition: transform 0.3s;
}
.third-party-icons img:hover {
transform: scale(1.1);
}
</style>
</head>
<body>
<header>
<div class="container">
<div class="header-top">
<div class="logo">优选商城</div>
</div>
</div>
</header>
<div class="container">
<div class="login-container">
<h2 class="login-title">账号登录</h2>
<form method="get">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名/手机号/邮箱" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
</div>
<div class="remember-me">
<input type="checkbox" id="remember" name="remember">
<label for="remember">记住我</label>
</div>
<button type="submit" class="login-btn">登录</button>
<div class="form-footer">
忘记密码?<a href="#">立即找回</a>
<span>或</span>
<a href="/register">立即注册</a>
</div>
</form>
</div>
</div>
</body>
</html>
|
下面重点关注表单部分:
HTML |
---|
| <form method="get">
<!-- ... -->
</form>
|
当前默认是GET
请求,运行服务器请求该页面,输入内容并点击提交观察结果:

再将请求方式修改为POST
请求,观察结果:
HTML |
---|
| <form method="post">
<!-- ... -->
</form>
|

可以看到,如果是GET
请求,参数就是直接放在URI
的后方,而如果是POST
,参数则在请求体中,这也证明了前面GET
和POST
区别的第一点
当服务端需要客户端发送数据时,这个数据肯定是需要被服务端拿去使用的,例如比对账号是否存在于数据库,但是因为本次并不存在数据库以及访问数据库的操作,所以本次演示就只是从HTTP请求中获取到对应的值即可
因为GET
和POST
对请求参数的处理是不同的,所以需要分为两种情况处理,一般对于一个请求来说,如果是GET
请求,那么请求的参数会放在URI
之后,以?
开头,每一个属性都是key=value
的形式,多个属性之间用&
进行连接,但是如果是POST
请求,那么请求的参数就放在请求正文部分,同样每一个属性都是key=value
的形式,默认情况下,多个属性之间用&
进行连接
这里可以通过一个函数包装分情况处理的逻辑,而因为需要存储响应体,所以这里考虑使用一个哈希表分别存储每一个键值对,所以基本结构如下:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | // 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中截取出参数,因为参数起始的字符是?
,所以从?
开始查找,该字符的下一个位置就是第一个参数键值对,当找到第一个&
就代表找到了一个完整的参数键值对,现在就只需要将这个参数字符串提取出来存储到哈希表中即可:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 | // 获取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;
}
}
|
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | // 处理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;
}
}
|
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | // 获取请求参数
void getReqParams()
{
if(_req_method == "GET")
{
// 处理GET请求中的参数
getReqParamsFromReqLine();
}
else if(_req_method == "POST")
{
// 处理POST请求中的参数
getReqParamsFromBody();
}
}
|
Note
需要注意,在上面的getReqParamsFromReqLine
函数中,在最后要处理一下从URI
中移除参数,否则获取完参数后URI
还携带参数影响后续其他逻辑执行
最后,在反序列化函数中调用处理参数函数:
C++ |
---|
| // 反序列化
bool deserialize(std::string &in_str)
{
// ...
// 获取参数内容
getReqParams();
return true;
}
|
接下来,分别在GET
请求和POST
请求下测试:
静态资源和动态资源
介绍与准备
在上面以及之前两节中,服务器都是向客户端直接响应字符串或者一个静态资源,但是有的时候不只有静态资源,还有很多的动态资源,例如在表单提交时form
标签内的action
字段的值就是对应的一个动态资源
以一个实例表单为例:
HTML |
---|
| <form action="/login" method="get">
<!-- ... -->
</form>
|
在这个表单中,action
的值即为一个动态资源,这个动态资源一般是一个函数,有HTTP服务器调用上层的函数去完成,这里依旧是以登录为例:
HTML |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | <!-- ... -->
<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
中:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | // HTTP请求
class HttpRequest
{
public:
HttpRequest()
:_hasArgs(false)
{
}
// ...
private:
// ...
bool _hasArgs; // 是否有参数
};
|
接着,提供一个函数获取_hasArgs
的值方便上层调用:
C++ |
---|
| // 获取_hasArgs
bool getHasArgs()
{
return _hasArgs;
}
|
最后,在获取到参数时更改_hasArgs
的值,但是这里不可以直接修改_hasArgs
,而应该是成功获取了GET或者POST请求中的参数才可以修改,所以还需要为getReqParamsFromReqLine
和getReqParamsFromBody
设计一个返回值,用于判断是否执行成功:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 | // 获取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;
}
|
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | // 处理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;
}
|
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | 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)
的函数:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12 | using handler_t = std::function<void()>;
class HttpServer
{
public:
// ...
private:
// ...
std::unordered_map<std::string, handler_t> _dynamic_func;
};
|
接着,需要向上层提供一个添加处理动态资源函数的接口:
C++ |
---|
| void pushDynamicTask(std::string name, handler_t handler)
{
_dynamic_func[name] = handler;
}
|
另外,为了保证执行正常,可以提供一个判断接口,用于判断需要执行的函数是否存在:
C++ |
---|
| bool hasFunc(std::string name)
{
auto pos = _dynamic_func.find(name);
return pos != _dynamic_func.end();
}
|
最后就是对handleHttpRequest
进行处理,只需要根据getHasArgs
函数的返回值即可判断是否执行动态资源处理函数:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | 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
中提供获取参数的函数:
C++ |
---|
| 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
动态资源的函数:
C++ |
---|
| void login(HttpRequest &req, HttpResponse &resp)
{
LOG(LogLevel::DEBUG) << "进入登录模块";
req.getParamKv();
}
|
修改主函数如下:
C++ |
---|
1
2
3
4
5
6
7
8
9
10
11
12
13 | 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
都执行了动态资源