- HTTP1.0/1.1 核心区别:核心是「长连接」+「缓存增强」+「Host 必传」,1.1 是 1.0 的超集,1.1 的所有优化都为「高性能、低开销、高并发」设计,生产级服务器仅支持 HTTP1.1(向下兼容 1.0);
- HTTP 请求结构:纯 ASCII 文本协议,固定三段式 → 请求首行 + 请求头部 + 请求正文 ,核心分隔符是
\r\n,头部与正文的分界是空行\r\n\r\n,无正文则直接结束;- HTTP 请求解析核心:TCP 是流式协议 (粘包 / 半包),HTTP 数据是分段到达的,必须用「状态机 + 缓冲区暂存」实现分段解析,分 3 个阶段依次解析首行、头部、正文,无任何例外;
- URL 编解码核心思路:URL 编码是百分号编码 (Percent-Encoding),非法字符转「%+ 两位十六进制 ASCII 值」;解码是反向操作,「% xx」转回对应字符,核心解决「URL 含非法字符导致解析失败」的问题。
一、HTTP 1.0 和 HTTP 1.1 的【核心区别】
HTTP 1.0 发布于 1996 年,HTTP 1.1 发布于 1999 年,是 HTTP 协议的里程碑式升级 ,1.1 是 1.0 的超集 (1.1 兼容 1.0 的所有特性),二者的所有区别都是「HTTP1.1 为了解决 1.0 在高并发、高性能场景下的致命缺陷而做的针对性优化」。
核心背景:HTTP 协议是基于 TCP/IP 的应用层无状态文本协议,所有 HTTP 请求 / 响应都通过 TCP 连接传输,HTTP1.0 的设计初衷是「简单的网页浏览」,而 HTTP1.1 的设计目标是「支撑大规模、高并发的互联网服务」。
区别 1:默认连接方式:HTTP1.0 默认「短连接」,HTTP1.1 默认「长连接 (持久连接)」
这是二者最核心的差异,没有之一,也是性能差距的根源,直接决定了服务器的并发能力上限!
- HTTP1.0 短连接(Connection: close 是默认值) :客户端每次发起一个 HTTP 请求 ,都会和服务器建立一条全新的 TCP 连接,请求处理完成、响应发送完毕后,服务器立即关闭该 TCP 连接;
- 致命缺陷:TCP 连接的建立(三次握手)、关闭(四次挥手)有巨大的网络开销 + 内核开销 ,如果一个网页有 10 张图片,客户端要发起 10 次 HTTP 请求,就要建立 / 关闭 10 次 TCP 连接,高并发下服务器的连接数会被瞬间打满,性能暴跌。
- HTTP1.1 长连接(Connection: keep-alive 是默认值) :客户端和服务器建立一次 TCP 连接后 ,该连接可以被复用 ,在同一个 TCP 连接上,客户端可以连续发起多个 HTTP 请求 / 响应,直到连接被主动关闭(超时 / 异常 / 显式关闭);
- 核心优势:彻底规避了 TCP 连接的建立 / 关闭开销,单条 TCP 连接可处理数十上百个 HTTP 请求 ,服务器的 TCP 连接数大幅减少,内核资源占用降低,并发能力提升数倍甚至十倍,这是 HTTP1.1 能支撑高并发的核心基石。
项目落地:服务器中必须实现长连接的超时管理 ------ 给每个 TCP 连接设置
keep-alive超时时间,如果长时间无请求,主动关闭连接,避免连接泄漏导致 fd 耗尽。
区别 2:HTTP1.1 必传Host请求头,HTTP1.0 无此要求,解决「虚拟主机」核心问题
这是 HTTP1.1 的强制规范 ,也是现代 Web 服务器(Nginx/Apache)能实现「一台服务器部署多个域名网站」的核心前提,是生产级服务器的必备特性!
- HTTP1.0 无 Host 头:服务器只能根据「IP 地址」区分不同的服务,一台服务器只能部署一个网站,因为服务器不知道客户端请求的是哪个域名;
- HTTP1.1 Host 头是必传项 :请求头中必须包含
Host: www.xxx.com,服务器可以通过Host字段的值,区分同一个 IP 下的不同域名网站,实现「虚拟主机」部署。
项目落地:服务器解析请求头时,必须校验 Host 头是否存在,不存在则返回 400 Bad Request 错误,这是 HTTP1.1 的规范要求。
区别 3:HTTP1.1 支持「管线化请求 (Pipelining)」,HTTP1.0 不支持
该特性是长连接的「锦上添花」,进一步提升了 TCP 连接的利用率,是高并发的重要优化:
- HTTP1.0 :请求必须「串行执行」------ 客户端发送一个请求后,必须等待服务器返回完整响应,才能发送下一个请求,TCP 连接在等待响应的过程中处于空闲状态,利用率低;
- HTTP1.1 :支持「管线化并行请求 」------ 在同一个长连接上,客户端可以连续发送多个 HTTP 请求,无需等待前一个请求的响应,服务器按请求顺序返回响应,TCP 连接的利用率被拉满。
区别 4:缓存机制的极致增强,HTTP1.1 完善了缓存策略,解决 1.0 缓存的模糊性
HTTP1.0 的缓存机制非常简陋,只有Expires一个字段,存在严重缺陷;HTTP1.1 在其基础上新增了一系列缓存相关的请求头 / 响应头,缓存逻辑更精准、更灵活、更高效,也是你的服务器「静态资源缓存」的核心实现依据:
- HTTP1.0 缓存:仅支持
Expires: 绝对时间,表示资源的过期时间,缺点是「依赖客户端本地时间」,如果客户端时间错误,缓存逻辑直接失效; - HTTP1.1 缓存:新增核心字段
Cache-Control:替代 Expires 的核心缓存头,支持相对时间 (如max-age=3600表示缓存 1 小时),优先级高于 Expires,不依赖客户端时间,精准可靠;ETag/If-None-Match:资源的唯一标识(如文件哈希值),服务器对比 ETag 判断资源是否修改,比时间戳更精准;Last-Modified/If-Modified-Since:资源的最后修改时间,客户端携带该头发起请求,服务器判断是否需要返回新资源;- 新增缓存状态码
304 Not Modified:资源未修改时,服务器仅返回状态码,不返回资源正文,大幅节省带宽。
区别 5:支持「分块传输编码 (Transfer-Encoding: chunked)」,解决大文件传输难题
- HTTP1.0 缺陷:传输数据时,必须在请求头 / 响应头中设置
Content-Length: 数据长度,表示正文的字节数;如果数据长度无法提前获知(如动态生成的网页、大文件下载、流式数据),则无法传输,因为不知道何时传输完成; - HTTP1.1 解决方案:新增
Transfer-Encoding: chunked分块编码,无需设置 Content-Length,数据被拆分成多个小块 传输,每个块包含「块长度 (十六进制)+\r\n + 块数据 +\r\n」,最后以0\r\n\r\n表示传输结束;
项目落地:服务器必须实现 chunked 的解析和组装------ 解析客户端的 chunked 请求体,组装 chunked 响应体,这是处理大文件的必备能力。
- 支持字节范围请求 (Range/206) :HTTP1.1 新增
Range: bytes=0-1024请求头,支持「断点续传」和「分块下载」,服务器返回206 Partial Content状态码,这是下载类服务器的核心特性;HTTP1.0 无此功能; - 新增更多精准的状态码 :HTTP1.0 只有 20 + 个状态码,HTTP1.1 扩充到 40 + 个,比如
206(部分内容)、409(冲突)、410(永久删除)、413(请求体过大),错误处理更精准; - 支持更多的请求方法 / 编码协商 :HTTP1.1 新增
PUT/DELETE/OPTIONS等方法,支持Accept-Encoding(压缩格式)、Accept-Language(语言)等协商头,协议的扩展性更强。
1.0 短连接,1.1 默认长连接;2. 1.1 必传 Host 头,支持虚拟主机;3. 1.1 支持管线化请求;4. 1.1 缓存机制更完善;5. 1.1 支持 chunked 分块传输 + 断点续传。
二、HTTP 请求的【标准完整结构】
HTTP 是纯 ASCII 字符的无状态文本协议 ,所有的 HTTP 请求报文都是「固定三段式结构」,没有任何二进制数据,这也是 HTTP 协议解析简单的核心原因。
- HTTP 协议的行结束符 是:
\r\n(回车 + 换行,两个字符),所有行(首行、每一个请求头)都必须以\r\n结尾;- HTTP 请求的头部与正文的分界符 是:
\r\n\r\n(连续两个行结束符,即「空行」),空行表示请求头部结束,后续的所有数据都是请求正文;- 字段分隔:请求首行的三个部分用空格 分隔;请求头部的键值对用
: 空格分隔(如Host: www.baidu.com)。
标准 HTTP 请求完整结构(POST 请求为例,GET 无正文)
cpp
[请求首行] Method URL HTTP-Version \r\n
[请求头部] Header1: Value1 \r\n
Header2: Value2 \r\n
...
HeaderN: ValueN \r\n
[空行] \r\n
[请求正文] body_data...(可选,GET/HEAD无正文,POST/PUT有正文)
1. 请求首行(请求行,必须有,唯一行)
请求的第一行,是 HTTP 请求的核心标识,由 3 个部分组成,空格分隔,末尾必加 \r\n:
Method:请求方法,如GET/POST/PUT/DELETE/HEAD,你的服务器至少要实现GET和POST;URL:请求的资源路径 + 参数,如/index.html?name=test,需要做 URL 解码;HTTP-Version:协议版本,如HTTP/1.1、HTTP/1.0。
2. 请求头部(响应头同理,可选多行,可无)
由若干个键值对 (Header: Value) 组成,每行一个键值对,末尾必加\r\n,顺序无关 ,字段数量不限;核心必解析的头部:Host、Content-Length、Transfer-Encoding、Connection、Content-Type,其余头部按需解析即可。
3. 请求正文(请求体,可选,可有可无)
HTTP 请求的数据载荷 ,是纯二进制 / 文本数据,只有当请求方法是 POST/PUT 时才有正文 ,GET/HEAD/DELETE 等方法无正文;正文的读取规则:
- 如果有
Content-Length头:正文长度固定,读取指定字节数即可; - 如果有
Transfer-Encoding: chunked:正文是分块编码,按 chunked 规则解析; - 无上述两个头:无正文。
三、分段解析 HTTP 请求(首行 + 头部 + 正文)
TCP 协议是流式字节协议,存在两个核心特性,这是 HTTP 解析的「拦路虎」,也是所有解析逻辑的出发点:
- 粘包:多个 HTTP 请求的数据被拼接在一起,一次性发送到服务器;
- 半包:一个完整的 HTTP 请求数据,被拆分成多个 TCP 数据包,分批到达服务器;
核心结论:绝对不能用「一次性读取完整请求再解析」的方式,这种方式在流式数据下会直接导致解析失败、数据丢失、程序崩溃!
核心三板斧 → 「缓冲区暂存 + 状态机解析 + 分段递进」
这是所有高性能 HTTP 服务器的标准答案(Nginx/muduo/ 你的项目均采用此方案),无任何例外,是唯一正确的解析方式
-
为每个客户端的 TCP 连接(conn fd),分配一个独立的读缓冲区(char buf []),用于暂存从 socket 中读取的、未完成解析的 TCP 流式数据;
-
定义一个解析状态机枚举 ,标记当前的解析阶段,状态是递进式切换 的,只有上一阶段解析完成,才会进入下一阶段,不会回退:
cppenum ParseState { PARSE_STATE_REQUEST_LINE, // 正在解析请求首行(初始状态) PARSE_STATE_HEADER, // 正在解析请求头部 PARSE_STATE_BODY, // 正在解析请求正文 PARSE_STATE_FINISH, // 解析完成 PARSE_STATE_ERROR // 解析失败 }; -
每次从 socket 中读取到新数据,都追加到缓冲区末尾,然后从缓冲区的起始位置开始,按当前的解析状态进行解析;解析完成的部分从缓冲区中「摘除」,未解析的剩余数据留在缓冲区,等待下一次新数据到来后继续解析;
-
所有解析的核心依据是 HTTP 的分隔符规则 :
\r\n分行、\r\n\r\n分隔头部和正文。
阶段一:解析【请求首行】(初始状态,必须先完成)
- 从缓冲区中查找第一个
\r\n,这个\r\n就是请求首行的结束位置; - 截取
\r\n之前的字符串,按空格 切分成 3 个部分,分别赋值给method(请求方法)、url(请求路径)、version(协议版本); - 合法性校验:比如 method 是否是 GET/POST,version 是否是 HTTP/1.0/1.1,校验失败则设置状态为
PARSE_STATE_ERROR,返回 400 错误; - 解析完成后,将缓冲区的指针挪动到该
\r\n的下一个字符 ,并将解析状态切换为PARSE_STATE_HEADER。
阶段二:解析【请求头部】(首行解析完成后进入)
- 从当前缓冲区指针位置开始,循环查找
\r\n,每次找到一个\r\n,就截取该行的字符串,作为一个请求头; - 对每个请求头,按 **
: 空格** 切分成key-value键值对,存入一个unordered_map<string, string>的头部映射表中; - 核心终止条件:当找到连续的两个
\r\n(即\r\n\r\n) 时,说明请求头部解析完成; - 合法性校验:比如 HTTP1.1 是否有
Host头,校验失败则返回 400 错误; - 解析完成后,将缓冲区指针挪动到
\r\n\r\n的下一个字符 ,然后判断是否有请求正文:- 无正文(如 GET 请求):直接切换状态为
PARSE_STATE_FINISH; - 有正文(如 POST 请求):切换状态为
PARSE_STATE_BODY。
- 无正文(如 GET 请求):直接切换状态为
阶段三:解析【请求正文】(头部解析完成后,可选阶段)
这是解析中最复杂的部分,但规则固定,只有两种情况,二选一,无其他可能 ,解析的核心是「根据头部字段确定正文的读取规则」,这也是 HTTP1.1 的核心特性体现:
情况 1:存在 Content-Length 头部 → 定长正文解析(最常见,POST 表单提交)
- 从头部映射表中取出
Content-Length的值,转换为整数content_len,即正文的总字节数; - 判断缓冲区中剩余的数据长度是否 ≥
content_len:- 是:截取缓冲区中前
content_len个字节作为请求正文,解析完成,切换状态为PARSE_STATE_FINISH; - 否:缓冲区数据不足,不做任何处理,等待下一次新数据到来后继续解析,直到数据足够;
- 是:截取缓冲区中前
- 解析完成后,挪动缓冲区指针,摘除正文数据
情况 2:存在 Transfer-Encoding: chunked 头部 → 分块正文解析(HTTP1.1 独有,大文件上传)
- 按 chunked 的标准格式解析,规则是固定的,无需提前知道总长度:
- 第一步:读取一行(
\r\n分隔),该行是十六进制的数字,表示「当前块的字节长度」; - 第二步:读取对应长度的字节,作为当前块的实际数据;
- 第三步:跳过块末尾的
\r\n,重复上述步骤; - 终止条件:当读取到的块长度为
0时,再读取末尾的\r\n\r\n,表示分块传输结束;
- 第一步:读取一行(
- 所有块的数据拼接起来,就是完整的请求正文;解析完成后切换状态为
PARSE_STATE_FINISH。
核心补充:如果既没有
Content-Length也没有chunked,则无请求正文,直接完成解析。
解析完成 / 失败的收尾逻辑
- 解析完成(
PARSE_STATE_FINISH):处理业务逻辑(路由匹配、处理请求、组装响应),处理完成后,根据Connection头判断是否关闭 TCP 连接(短连接关闭,长连接保留); - 解析失败(
PARSE_STATE_ERROR):向客户端返回400 Bad Request错误,然后关闭 TCP 连接; - 缓冲区复用:解析完成的部分被摘除后,剩余的未解析数据留在缓冲区,缓冲区会被复用,无需重新分配内存,减少开销。
四、URL 编解码的【核心思路 + 实现逻辑】
服务器在解析完请求首行的URL后,必须先对 URL 做解码,才能正确处理请求路径和参数 ------ 这是 HTTP 协议的规范要求,也是项目中「必踩的坑」,如果不做 URL 编解码,会导致「中文乱码、特殊字符解析失败、参数丢失」等问题。
为什么需要 URL 编解码?
HTTP 协议规定:URL 中只能包含「合法的安全字符」,其他字符都是「非法字符」,如果 URL 中直接包含非法字符,会导致服务器解析 URL 时出现歧义、解析失败,甚至引发安全问题。
- 合法安全字符 :英文字母 (a-zA-Z)、数字 (0-9)、特殊符号
- _ . ~,以及分隔符/? & = #(有特殊语义); - 非法字符 :空格、中文、日文、以及其他特殊符号(如
% $ + 空格 中文),这些字符绝对不能直接出现在 URL 中,必须经过「编码」后才能传输。
核心定义:URL 编解码也叫 百分号编码 (Percent-Encoding) ,遵循 RFC3986 规范,是 HTTP 协议的标准实现,编码和解码是完全可逆的。
一、URL 编码(Encode)的核心思路
编码规则(无例外,所有非法字符都遵循此规则)
- 对 URL 中的合法字符 :直接保留,不做任何处理;
- 对 URL 中的非法字符 :
- 步骤 1:将该字符转换为对应的 ASCII 码值(如果是中文 / 多字节字符,先转成 UTF-8 编码的多字节数组,再对每个字节分别处理);
- 步骤 2:将 ASCII 码值转换为 两位的十六进制字符串(小写 / 大写均可,推荐小写);
- 步骤 3:在两位十六进制字符串的前面添加一个百分号
%; - 最终结果:该非法字符被替换为
%xx的格式。
经典编码案例
- 空格 → ASCII 32 → 十六进制 20 → 编码后:
%20 +→ ASCII 43 → 十六进制 2B → 编码后:%2B%→ ASCII 37 → 十六进制 25 → 编码后:%25- 中文「你」→ UTF-8 编码是
0xE4 0xBD 0xA0→ 编码后:%E4%BD%A0
二、URL 解码(Decode)的核心思路
URL 解码是编码的完全反向操作 ,服务器解析 URL 时,主要做的是解码操作 ------ 客户端传过来的 URL 是编码后的%xx格式,服务器需要解码成原始的字符,才能正确处理路径和参数。
解码规则(无例外)
- 遍历 URL 的每个字符,对字符做判断:
- 如果当前字符 不是
%:直接保留,追加到结果字符串中; - 如果当前字符 是
%:说明这是一个编码后的字符,需要做解码;
- 如果当前字符 不是
- 解码核心步骤:
- 步骤 1:取
%后面的连续两位字符,这是一个十六进制的字符串; - 步骤 2:将该十六进制字符串 转换为对应的十进制整数(即原始的 ASCII 码值);
- 步骤 3:将该整数转换为对应的 ASCII 字符;
- 步骤 4:将该字符追加到结果字符串中,然后跳过这两位字符,继续遍历;
- 步骤 1:取
- 对中文等多字节字符:解码后会得到 UTF-8 的多字节数组,再将该数组转换为对应的中文字符即可。
经典解码案例
%20→ 20 → 32 → 空格%E4%BD%A0→ E4→228, BD→189, A0→160 → UTF-8 解码为「你」
项目中 URL 编解码的实现要点(
- 解码时必须判断:
%后面是否有足够的两位字符,否则是非法 URL,返回 400 错误; - 中文必须用UTF-8 编码 / 解码,这是现代浏览器的默认标准,否则会出现中文乱码;
- 编解码的实现是纯字符串操作,无任何 IO 开销,效率极高,无需担心性能问题。