HTTP

  • 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,你的服务器至少要实现GETPOST
  • URL:请求的资源路径 + 参数,如/index.html?name=test,需要做 URL 解码;
  • HTTP-Version:协议版本,如HTTP/1.1HTTP/1.0

2. 请求头部(响应头同理,可选多行,可无)

若干个键值对 (Header: Value) 组成,每行一个键值对,末尾必加\r\n顺序无关 ,字段数量不限;核心必解析的头部:HostContent-LengthTransfer-EncodingConnectionContent-Type,其余头部按需解析即可。

3. 请求正文(请求体,可选,可有可无)

HTTP 请求的数据载荷 ,是纯二进制 / 文本数据,只有当请求方法是 POST/PUT 时才有正文 ,GET/HEAD/DELETE 等方法无正文;正文的读取规则:

  • 如果有Content-Length头:正文长度固定,读取指定字节数即可;
  • 如果有Transfer-Encoding: chunked:正文是分块编码,按 chunked 规则解析;
  • 无上述两个头:无正文。

三、分段解析 HTTP 请求(首行 + 头部 + 正文)

TCP 协议是流式字节协议,存在两个核心特性,这是 HTTP 解析的「拦路虎」,也是所有解析逻辑的出发点:

  1. 粘包:多个 HTTP 请求的数据被拼接在一起,一次性发送到服务器;
  2. 半包:一个完整的 HTTP 请求数据,被拆分成多个 TCP 数据包,分批到达服务器;

核心结论:绝对不能用「一次性读取完整请求再解析」的方式,这种方式在流式数据下会直接导致解析失败、数据丢失、程序崩溃!

核心三板斧 → 「缓冲区暂存 + 状态机解析 + 分段递进

这是所有高性能 HTTP 服务器的标准答案(Nginx/muduo/ 你的项目均采用此方案),无任何例外,是唯一正确的解析方式

  1. 为每个客户端的 TCP 连接(conn fd),分配一个独立的读缓冲区(char buf []),用于暂存从 socket 中读取的、未完成解析的 TCP 流式数据;

  2. 定义一个解析状态机枚举 ,标记当前的解析阶段,状态是递进式切换 的,只有上一阶段解析完成,才会进入下一阶段,不会回退:

    cpp 复制代码
    enum ParseState {
        PARSE_STATE_REQUEST_LINE,  // 正在解析请求首行(初始状态)
        PARSE_STATE_HEADER,        // 正在解析请求头部
        PARSE_STATE_BODY,          // 正在解析请求正文
        PARSE_STATE_FINISH,        // 解析完成
        PARSE_STATE_ERROR          // 解析失败
    };
  3. 每次从 socket 中读取到新数据,都追加到缓冲区末尾,然后从缓冲区的起始位置开始,按当前的解析状态进行解析;解析完成的部分从缓冲区中「摘除」,未解析的剩余数据留在缓冲区,等待下一次新数据到来后继续解析;

  4. 所有解析的核心依据是 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

阶段二:解析【请求头部】(首行解析完成后进入)

  1. 从当前缓冲区指针位置开始,循环查找\r\n ,每次找到一个\r\n,就截取该行的字符串,作为一个请求头;
  2. 对每个请求头,按 **: 空格** 切分成key-value键值对,存入一个unordered_map<string, string>的头部映射表中;
  3. 核心终止条件:当找到连续的两个\r\n(即\r\n\r\n 时,说明请求头部解析完成;
  4. 合法性校验:比如 HTTP1.1 是否有Host头,校验失败则返回 400 错误;
  5. 解析完成后,将缓冲区指针挪动到\r\n\r\n的下一个字符 ,然后判断是否有请求正文:
    • 无正文(如 GET 请求):直接切换状态为PARSE_STATE_FINISH
    • 有正文(如 POST 请求):切换状态为PARSE_STATE_BODY
阶段三:解析【请求正文】(头部解析完成后,可选阶段)

这是解析中最复杂的部分,但规则固定,只有两种情况,二选一,无其他可能 ,解析的核心是「根据头部字段确定正文的读取规则」,这也是 HTTP1.1 的核心特性体现:

情况 1:存在 Content-Length 头部 → 定长正文解析(最常见,POST 表单提交)

  • 从头部映射表中取出Content-Length的值,转换为整数content_len,即正文的总字节数;
  • 判断缓冲区中剩余的数据长度是否 ≥ content_len
    • 是:截取缓冲区中前content_len个字节作为请求正文,解析完成,切换状态为PARSE_STATE_FINISH
    • 否:缓冲区数据不足,不做任何处理,等待下一次新数据到来后继续解析,直到数据足够;
  • 解析完成后,挪动缓冲区指针,摘除正文数据

情况 2:存在 Transfer-Encoding: chunked 头部 → 分块正文解析(HTTP1.1 独有,大文件上传)

  1. 按 chunked 的标准格式解析,规则是固定的,无需提前知道总长度:
    • 第一步:读取一行(\r\n分隔),该行是十六进制的数字,表示「当前块的字节长度」;
    • 第二步:读取对应长度的字节,作为当前块的实际数据;
    • 第三步:跳过块末尾的\r\n,重复上述步骤;
    • 终止条件:当读取到的块长度为0时,再读取末尾的\r\n\r\n,表示分块传输结束;
  2. 所有块的数据拼接起来,就是完整的请求正文;解析完成后切换状态为PARSE_STATE_FINISH

核心补充:如果既没有Content-Length也没有chunked,则无请求正文,直接完成解析。

解析完成 / 失败的收尾逻辑
  1. 解析完成(PARSE_STATE_FINISH):处理业务逻辑(路由匹配、处理请求、组装响应),处理完成后,根据Connection头判断是否关闭 TCP 连接(短连接关闭,长连接保留);
  2. 解析失败(PARSE_STATE_ERROR):向客户端返回400 Bad Request错误,然后关闭 TCP 连接;
  3. 缓冲区复用:解析完成的部分被摘除后,剩余的未解析数据留在缓冲区,缓冲区会被复用,无需重新分配内存,减少开销。

四、URL 编解码的【核心思路 + 实现逻辑】

服务器在解析完请求首行的URL后,必须先对 URL 做解码,才能正确处理请求路径和参数 ------ 这是 HTTP 协议的规范要求,也是项目中「必踩的坑」,如果不做 URL 编解码,会导致「中文乱码、特殊字符解析失败、参数丢失」等问题。

为什么需要 URL 编解码?

HTTP 协议规定:URL 中只能包含「合法的安全字符」,其他字符都是「非法字符」,如果 URL 中直接包含非法字符,会导致服务器解析 URL 时出现歧义、解析失败,甚至引发安全问题。

  1. 合法安全字符 :英文字母 (a-zA-Z)、数字 (0-9)、特殊符号 - _ . ~,以及分隔符 /? & = #(有特殊语义);
  2. 非法字符 :空格、中文、日文、以及其他特殊符号(如% $ + 空格 中文),这些字符绝对不能直接出现在 URL 中,必须经过「编码」后才能传输。

核心定义:URL 编解码也叫 百分号编码 (Percent-Encoding) ,遵循 RFC3986 规范,是 HTTP 协议的标准实现,编码和解码是完全可逆的

一、URL 编码(Encode)的核心思路

编码规则(无例外,所有非法字符都遵循此规则)

  1. 对 URL 中的合法字符直接保留,不做任何处理
  2. 对 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格式,服务器需要解码成原始的字符,才能正确处理路径和参数。

解码规则(无例外)

  1. 遍历 URL 的每个字符,对字符做判断:
    • 如果当前字符 不是 %:直接保留,追加到结果字符串中;
    • 如果当前字符 %:说明这是一个编码后的字符,需要做解码;
  2. 解码核心步骤:
    • 步骤 1:取%后面的连续两位字符,这是一个十六进制的字符串;
    • 步骤 2:将该十六进制字符串 转换为对应的十进制整数(即原始的 ASCII 码值);
    • 步骤 3:将该整数转换为对应的 ASCII 字符
    • 步骤 4:将该字符追加到结果字符串中,然后跳过这两位字符,继续遍历;
  3. 对中文等多字节字符:解码后会得到 UTF-8 的多字节数组,再将该数组转换为对应的中文字符即可。

经典解码案例

  • %20 → 20 → 32 → 空格
  • %E4%BD%A0 → E4→228, BD→189, A0→160 → UTF-8 解码为「你」

项目中 URL 编解码的实现要点(

  1. 解码时必须判断:%后面是否有足够的两位字符,否则是非法 URL,返回 400 错误;
  2. 中文必须用UTF-8 编码 / 解码,这是现代浏览器的默认标准,否则会出现中文乱码;
  3. 编解码的实现是纯字符串操作,无任何 IO 开销,效率极高,无需担心性能问题。
相关推荐
hrw_embedded12 小时前
基于CH395Q网卡移植ftplib的FTP客户端
网络·stm32·ftp·linux库
不是起点的终点12 小时前
内网穿透(FRP)
网络
北京耐用通信12 小时前
耐达讯自动化Profinet转Devicenet网关:精细化工行业的“协议融合利器”
人工智能·物联网·网络协议·自动化·信息与通信
Blurpath住宅代理12 小时前
动态代理的五大优点:提升爬虫效率与安全性
网络·爬虫·动态ip·住宅ip·住宅代理
三水不滴12 小时前
计算机网络核心网络模型
经验分享·笔记·tcp/ip·计算机网络·http·https
XHW___00112 小时前
webrtc 关键模块创建的时机
网络·音视频·webrtc
Trouvaille ~12 小时前
【Linux】UDP Socket编程实战(二):网络字典与回调设计
linux·运维·服务器·网络·c++·udp·操作系统
凉、介12 小时前
静态路由探究
网络·笔记·操作系统·嵌入式
逐步前行12 小时前
STM32_内部结构
网络·stm32·嵌入式硬件
云小逸13 小时前
【nmap源码学习】 Nmap 源码深度解析:nmap_main 函数详解与 NSE 脚本引擎原理
网络协议·学习·安全