目录
- [HTTP 协议](#HTTP 协议)
- [认识 URL](#认识 URL)
- [HTTP 协议请求与响应格式](#HTTP 协议请求与响应格式)
- [HTTP 的方法](#HTTP 的方法)
- [HTTP 的状态码](#HTTP 的状态码)
- [HTTP 常见 Header](#HTTP 常见 Header)
- [最简单的 HTTP 服务器](#最简单的 HTTP 服务器)
- 实验
- [附录:HTTP 历史及版本核心技术与时代背景](#附录:HTTP 历史及版本核心技术与时代背景)
HTTP 协议
虽然我们说,应用层协议是我们程序猿自己定的,但实际上已经有大佬们定义了一些现成的、又非常好用的应用层协议,供我们直接参考使用。HTTP(超文本传输协议)就是其中之一。
在互联网世界中,HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如 HTML 文档)。
HTTP 协议是客户端与服务器之间通信的基础。客户端通过 HTTP 协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP 是一种无状态协议 ,服务器不会仅凭协议本身保存客户端的状态信息;底层 TCP 连接是否复用,则取决于 HTTP 版本和 Connection 等约定,HTTP/1.1 默认使用持久连接。
认识 URL
平时我们俗称的"网址",其实就是说的 URL。

urlencode 和 urldecode(了解)
像 /、?、: 等字符,已经被 URL 当作具有特殊含义的字符,因此不能随意出现。比如,某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。
转义规则如下:先将待编码内容转换为 UTF-8 字节序列,再把需要转义的每个字节写成两位十六进制数,并在前面加上 %,形成 %HH 格式。
例如:

+ 被转义成了 %2B。
urldecode 就是 urlencode 的逆过程。
UrlEncode编码/UrlDecode解码:https://tool.chinaz.com/Tools/urlencode.aspx
HTTP 协议请求与响应格式
HTTP 请求

- 首行:
[方法] + [URL] + [版本]。 - **Header:**请求的属性,是用冒号分隔的键值对;每组属性之间使用
\r\n分隔;遇到空行表示 Header 部分结束。 - **Body:**空行后面的内容都是 Body。Body 允许为空字符串;如果 Body 存在,则 Header 中会有一个
Content-Length属性来标识 Body 的长度。

编写 HTTP 请求的代码:验证 HTTP 请求
- 需要基于历史代码,先架构出一个基本的 HTTP 服务器,然后用浏览器进行验证。
- 需要验证前端内容,直接使用 AI 即可。
HTTP 响应

- 首行:
[版本号] + [状态码] + [状态码解释]。 - **Header:**响应的属性,是用冒号分隔的键值对;每组属性之间使用
\r\n分隔;遇到空行表示 Header 部分结束。 - **Body:**空行后面的内容都是 Body。Body 允许为空字符串;如果 Body 存在,则 Header 中会有一个
Content-Length属性来标识 Body 的长度。如果服务器返回了一个 HTML 页面,那么 HTML 页面内容就在 Body 中。

基本的应答格式:

HTTP 的方法

其中最常用的就是 GET 方法和 POST 方法。
HTTP 常见方法
1. GET 方法(重点)
- **用途:**用于请求 URL 指定的资源。
- 示例:
GET /index.html HTTP/1.1 - **特性:**指定资源经服务器端解析后返回响应内容。
要通过历史写的 HTTP 服务器验证 GET 方法,这里需要了解一下 FORM 表单的问题,也要引入 Web 根目录和文件读取的基本操作。
cpp
std::string GetFileContentHelper(const std::string &path)
{
// 一份简单的读取二进制文件的代码
std::ifstream in(path, std::ios::binary);
if (!in.is_open())
return "";
in.seekg(0, in.end);
int filesize = in.tellg();
in.seekg(0, in.beg);
std::string content;
content.resize(filesize);
in.read(content.data(), filesize);
// std::vector<char> content(filesize);
// in.read(content.data(), filesize);
in.close();
return content;
}
2. POST 方法(重点)
- **用途:**用于传输实体的主体,通常用于提交表单数据。
- 示例:
POST /submit.cgi HTTP/1.1 - **特性:**可以发送大量的数据给服务器,并且数据包含在请求体中。
要通过历史写的 HTTP 服务器验证 POST 方法,这里需要了解一下 FORM 表单的问题。
3. PUT 方法(不常用)
- **用途:**用于传输文件,将请求报文主体中的文件保存到请求 URL 指定的位置。
- 示例:
PUT /example.html HTTP/1.1 - **特性:**不太常用,但在某些情况下(如 RESTful API 中)用于更新资源。
4. HEAD 方法
- **用途:**与 GET 方法类似,但不返回报文主体部分,仅返回响应头。
- 示例:
HEAD /index.html HTTP/1.1 - **特性:**用于确认 URL 的有效性及资源更新的日期时间等。
bash
# curl -i 显示响应头与响应体
$ curl -i www.baidu.com
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Length: 2381
Content-Type: text/html
Date: Sun, 16 Jun 2024 08:38:04 GMT
Etag: "588604dc-94d"
Last-Modified: Mon, 23 Jan 2017 13:27:56 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
<!DOCTYPE html>
...
# 使用 HEAD 方法,只会返回响应头
$ curl --head www.baidu.com
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Length: 277
Content-Type: text/html
Date: Sun, 16 Jun 2024 08:43:38 GMT
Etag: "575e1f71-115"
Last-Modified: Mon, 13 Jun 2016 02:50:25 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
5. DELETE 方法(不常用)
- **用途:**用于删除文件,是 PUT 的相反方法。
- 示例:
DELETE /example.html HTTP/1.1 - **特性:**按请求 URL 删除指定的资源。
6. OPTIONS 方法
- **用途:**用于查询针对请求 URL 指定的资源所支持的方法。
- 示例:
OPTIONS * HTTP/1.1 - **特性:**返回允许的方法,如 GET、POST 等。
不支持的效果:
bash
# 搭建一个 nginx 用来测试
# sudo apt install nginx
# sudo nginx # 开启
# ps ajx | grep nginx # 查看
# sudo nginx -s stop # 停止服务
$ sudo nginx -s stop
$ ps ajx | grep nginx
2944845 2945390 2945389 2944845 pts/1 2945389 S+ 1002 0:00 grep --color=auto nginx
$ sudo nginx
$ ps axj | grep nginx
1 2945393 2945393 2945393 ? -1 Ss 0 0:00 nginx: master process nginx
2945393 2945394 2945393 2945393 ? -1 S 33 0:00 nginx: worker process
2945393 2945395 2945393 2945393 ? -1 S 33 0:00 nginx: worker process
2944845 2945397 2945396 2944845 pts/1 2945396 S+ 1002 0:00 grep --color=auto nginx
# -X(大写 X)指明方法
$ curl -X OPTIONS -i 127.0.0.1/
HTTP/1.1 405 Not Allowed
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 16 Jun 2024 08:48:22 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
<html>
<head><title>405 Not Allowed</title></head>
<body>
<center><h1>405 Not Allowed</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
支持的效果:
text
HTTP/1.1 200 OK
Allow: GET, HEAD, POST, OPTIONS
Content-Type: text/plain
Content-Length: 0
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 16 Jun 2024 09:04:44 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
// 注意:这里没有响应体,因为 Content-Length 为 0
HTTP 的状态码

最常见的状态码包括 200 OK、404 Not Found、403 Forbidden、302 Found(重定向)和 504 Gateway Timeout。
| 状态码 | 含义 | 应用样例 |
|---|---|---|
| 100 | Continue | 上传大文件时,服务器告诉客户端可以继续上传 |
| 200 | OK | 访问网站首页,服务器返回网页内容 |
| 201 | Created | 发布新文章,服务器返回文章创建成功的信息 |
| 204 | No Content | 删除文章后,服务器返回"无内容"表示操作成功 |
| 301 | Moved Permanently | 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用 |
| 302 | Found | 用户登录成功后,重定向到用户首页 |
| 304 | Not Modified | 浏览器缓存机制中,对未修改的资源返回 304 状态码 |
| 400 | Bad Request | 填写表单时,格式不正确导致提交失败 |
| 401 | Unauthorized | 访问需要登录的页面时,未登录或认证失败 |
| 403 | Forbidden | 尝试访问没有权限查看的页面 |
| 404 | Not Found | 访问不存在的网页链接 |
| 500 | Internal Server Error | 服务器崩溃或数据库错误导致页面无法加载 |
| 502 | Bad Gateway | 使用代理服务器时,代理服务器无法从上游服务器获取有效响应 |
| 503 | Service Unavailable | 服务器维护或过载,暂时无法处理请求 |
下面是仅包含重定向相关状态码的表格:
| 状态码 | 含义 | 是否为临时重定向 | 应用样例 |
|---|---|---|---|
| 301 | Moved Permanently | 否(永久重定向) | 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用 |
| 302 | Found | 是(临时重定向) | 用户登录成功后,重定向到用户首页 |
| 307 | Temporary Redirect | 是(临时重定向) | 临时将资源重定向到新的位置(较少使用) |
| 308 | Permanent Redirect | 否(永久重定向) | 永久将资源重定向到新的位置(较少使用) |
关于重定向的验证:以 301 为代表
HTTP 状态码 301(永久重定向)和 302(临时重定向)都依赖 Location 选项。
HTTP 状态码 301(永久重定向)
- 当服务器返回 HTTP 301 状态码时,表示请求的资源已经被永久移动到新的位置。
- 在这种情况下,服务器会在响应中添加一个
Location头部,用于指定资源的新位置。这个Location头部包含新的 URL 地址,浏览器会自动重定向到该地址。 - 例如,在 HTTP 响应中,可能会看到类似以下的头部信息:
text
HTTP/1.1 301 Moved Permanently\r\n
Location: /new-location\r\n
HTTP 状态码 302(临时重定向)
- 当服务器返回 HTTP 302 状态码时,表示请求的资源临时被移动到新的位置。
- 同样地,服务器也会在响应中添加一个
Location头部来指定资源的新位置。浏览器会暂时使用新的 URL 进行后续请求,但不会缓存这个重定向。 - 例如,在 HTTP 响应中,可能会看到类似以下的头部信息:
text
HTTP/1.1 302 Found\r\n
Location: /new-location\r\n
**总结:**无论是 HTTP 301 还是 HTTP 302 重定向,都需要依赖 Location 选项来指定资源的新位置。Location 是一个标准的 HTTP 响应头部,用于告诉浏览器应该将请求重定向到哪个新的 URL 地址。
HTTP 常见 Header
Content-Type:数据类型(如text/html)。Content-Length:Body 的长度。Host:客户端告知服务器,所请求的资源位于哪个主机的哪个端口上。User-Agent:声明用户的操作系统和浏览器版本信息。Referer:当前页面是从哪个页面跳转过来的。Location:搭配 3xx 状态码使用,告诉客户端接下来要去哪里访问。Cookie:用于在客户端存储少量信息,通常用于实现会话(session)功能。
关于 Connection 报头
HTTP 中的 Connection 字段是 HTTP 报文头的一部分,主要用于控制和管理客户端与服务器之间的连接状态。
核心作用
- 管理持久连接:
Connection字段可用于管理持久连接(也称为长连接)。持久连接允许客户端和服务器在请求/响应完成后不立即关闭 TCP 连接,以便在同一个连接上发送多个请求和接收多个响应。
持久连接(长连接)
- **HTTP/1.1:**默认使用持久连接。当客户端和服务器都不明确指定关闭连接时,连接将保持打开状态,以便后续的请求和响应可以复用同一个连接。
- **HTTP/1.0:**默认连接是非持久的。如果希望在 HTTP/1.0 上实现持久连接,需要在请求头中显式设置
Connection: keep-alive。
语法格式
Connection: keep-alive:表示希望保持连接,以复用 TCP 连接。Connection: close:表示请求/响应完成后应该关闭 TCP 连接。
补充阅读: User-Agent 里的历史故事。
常见 Header 速查表
下面附上一张关于 HTTP 常见 Header 的表格:
| 字段名 | 含义 | 样例 |
|---|---|---|
| Accept | 客户端可接受的响应内容类型 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 |
| Accept-Encoding | 客户端支持的数据压缩格式 | Accept-Encoding: gzip, deflate, br |
| Accept-Language | 客户端可接受的语言类型 | Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 |
| Host | 请求的主机名和端口号 | Host: www.example.com:8080 |
| User-Agent | 客户端的软件环境信息 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 |
| Cookie | 客户端发送给服务器的 HTTP Cookie 信息 | Cookie: session_id=abcdefg12345; user_id=123 |
| Referer | 请求的来源 URL | Referer: /previous_page.html |
| Content-Type | 实体主体的媒体类型 | Content-Type: application/x-www-form-urlencoded(表单提交)或 Content-Type: application/json(JSON 数据) |
| Content-Length | 实体主体的字节大小 | Content-Length: 150 |
| Authorization | 认证信息,如用户名和密码 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==(Base64 编码后的用户名和密码) |
| Cache-Control | 缓存控制指令 | 请求时:Cache-Control: no-cache 或 Cache-Control: max-age=3600;响应时:Cache-Control: public, max-age=3600 |
| Connection | 请求完成后关闭还是保持连接 | Connection: keep-alive 或 Connection: close |
| Date | 请求或响应的日期和时间 | Date: Wed, 21 Oct 2023 07:28:00 GMT |
| Location | 重定向的目标 URL(与 3xx 状态码配合使用) | Location: /new_location.html(与 302 状态码配合使用) |
| Server | 服务器类型 | Server: Apache/2.4.41 (Unix) |
| Last-Modified | 资源的最后修改时间 | Last-Modified: Wed, 21 Oct 2023 07:20:00 GMT |
| ETag | 资源的唯一标识符,用于缓存 | ETag: "3f80f-1b6-5f4e2512a4100" |
| Expires | 响应过期的日期和时间 | Expires: Wed, 21 Oct 2023 08:28:00 GMT |
最简单的 HTTP 服务器
实现一个最简单的 HTTP 服务器,只在网页上输出 hello world。只要按照 HTTP 协议的要求构造数据,就很容易做到。
c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void Usage()
{
printf("usage: ./server [ip] [port]\n");
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage();
return 1;
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
{
perror("socket");
return 1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return 1;
}
ret = listen(fd, 10);
if (ret < 0)
{
perror("listen");
return 1;
}
for (;;)
{
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(fd, (struct sockaddr *)&client_addr, &len);
if (client_fd < 0)
{
perror("accept");
continue;
}
// 用一个足够大的缓冲区直接把数据读完
char input_buf[1024 * 10] = {0};
ssize_t read_size = read(client_fd, input_buf, sizeof(input_buf) - 1);
if (read_size < 0)
{
return 1;
}
printf("[Request] %s", input_buf);
char buf[1024] = {0};
const char *hello = "<h1>hello world</h1>";
sprintf(buf,
"HTTP/1.0 200 OK\r\nContent-Length: %zu\r\n\r\n%s",
strlen(hello), hello);
write(client_fd, buf, strlen(buf));
close(client_fd);
}
return 0;
}
编译并启动服务。在浏览器中输入 [ip]:[port] 对应的地址,就能看到显示结果 Hello World。


备注
- 此处使用
9090端口号启动了 HTTP 服务器。虽然 HTTP 服务器一般使用80端口,但这只是一个通用习惯,并不是说 HTTP 服务器不能使用其他端口号。 - 使用 Chrome 测试服务器时,可以看到服务器打印出的请求中还有一个
GET /favicon.ico HTTP/1.1请求。 - 大家可自行查找资料,理解
favicon.ico的作用。
实验
把返回的状态码改成 404、403、504 等,观察浏览器上分别会出现什么样的效果。
附录:HTTP 历史及版本核心技术与时代背景
HTTP(Hypertext Transfer Protocol,超文本传输协议)作为互联网中浏览器和服务器间通信的基石,经历了从简单到复杂、从单一到多样的发展过程。以下按照时间顺序介绍 HTTP 的主要版本、核心技术及其对应的时代背景。
HTTP/0.9
核心技术:
- 仅支持 GET 请求方法。
- 仅支持纯文本传输,主要是 HTML 格式。
- 无请求和响应头信息。
时代背景:
- 1991 年,HTTP/0.9 作为 HTTP 协议的最初版本,用于传输基本的超文本 HTML 内容。
- 当时的互联网还处于起步阶段,网页内容相对简单,主要以文本为主。
HTTP/1.0
核心技术:
- 引入 POST 和 HEAD 请求方法。
- 引入请求和响应头信息,支持多种数据格式(MIME)。
- 支持缓存(cache)。
- 支持状态码(status code)、多字符集等。
时代背景:
- 1996 年,随着互联网的快速发展,网页内容逐渐丰富,HTTP/1.0 版本应运而生。
- 为了满足日益增长的网络应用需求,HTTP/1.0 增加了更多功能和灵活性。
- 然而,HTTP/1.0 的默认工作方式是每次 TCP 连接只能发送一个请求,性能上存在一定局限。
HTTP/1.1
核心技术:
- 引入持久连接(persistent connection),支持管道化(pipelining)。
- 允许在单个 TCP 连接上进行多个请求和响应,提高了性能。
- 引入分块传输编码(chunked transfer encoding)。
- 支持
Host头,允许在一个 IP 地址上部署多个 Web 站点。
时代背景:
- 1999 年,随着网页加载的外部资源越来越多,HTTP/1.0 的性能问题愈发突出。
- HTTP/1.1 通过引入持久连接和管道化等技术,有效提高了数据传输效率。
- 同时,互联网应用开始呈现多元化、复杂化趋势,HTTP/1.1 的出现满足了这些需求。
HTTP/2.0
核心技术:
- 多路复用(multiplexing),一个 TCP 连接可以承载多个 HTTP 请求流。
- 二进制帧格式(binary framing),优化数据传输。
- 头部压缩(header compression),减少传输开销。
- 服务器推送(server push),提前向客户端发送资源。
时代背景:
- 2015 年,随着移动互联网的兴起和云计算技术的发展,网络应用对性能的要求越来越高。
- HTTP/2.0 通过多路复用、二进制帧格式等技术,显著提高了数据传输效率和网络性能。
- HTTP/2.0 在实际部署中通常配合 HTTPS 使用,以提高数据传输的安全性。
HTTP/3.0
核心技术:
- 使用 QUIC 协议替代 TCP,形成基于 UDP 构建的多路复用传输协议。
- 减少传统 TCP 与 TLS 分步建连带来的等待时间,提高连接建立速度。
- 缓解 TCP 层面的队头阻塞问题,提高数据传输效率。
时代背景:
- 2022 年,随着 5G、物联网等技术的快速发展,网络应用对实时性、可靠性的要求越来越高。
- HTTP/3.0 通过使用 QUIC 协议,提高了连接建立速度和数据传输效率,满足了这些需求。
- HTTP/3.0 将加密能力集成到 QUIC 中,保证数据传输的安全性。