【Linux 网络编程】应用层协议 HTTP(上):URL、请求与响应、方法和状态码

🔥 星光编译者 · 个人主页

📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》

🌄 向云端飞扬,编译属于自己的代码星河。


☕ 写在开篇

  你好,这里是 星光编译者。

  这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。

  比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。

  如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。


🔥 本文定位:接着自定义协议与序列化,看看浏览器和 Web 服务器遵守的通用约定。先读懂线上报文,再理解页面为什么能显示出来。

💡 学习目标:拆解 URL;区分百分号编码与表单编码;识别 HTTP/1.1 请求、响应和正文边界;掌握 GET、POST、HEAD、OPTIONS 与常见状态码。

📌 阅读范围 :以 HTTP/1.1 为主。配套服务器与实验文件在下篇 code/ 中;Cookie/Session 与 HTTPS 只交代衔接,不合并后面的两份加餐 PDF。

上一篇我们自己规定了长度前缀和 JSON 字段:客户端怎样封装,服务器就怎样解析。浏览器却不可能为每一个网站临时更换一套私有协议。它需要一个双方都熟悉的消息格式,也需要统一的方法、状态码与字段含义。

HTTP 就提供了这样的约定。理解它的第一步,并不是背下几十个请求头,而是把"网址、请求、响应、正文"放回一轮真实通信中。


文章目录


一、从浏览器访问页面说起

1.1 浏览器收到的不只是一段 HTML

在地址栏输入 http://127.0.0.1:9090/,浏览器会根据 URL 确定连接目标,然后发送 HTTP 请求。服务器读取请求,决定返回什么,再把响应写入连接。

服务器发回的数据大致分成两类:一类是控制信息,例如状态码、正文类型、正文长度;另一类才是浏览器最终处理的内容,例如 HTML、图片、JSON 或普通文本。

浏览器先读控制信息,再决定怎样解释正文。相同的字节,配上 text/plain 可能作为文字显示,配上 text/html 则可能按网页解析。因此"我发送了一串 HTML"与"我返回了正确的 HTTP 响应"是两件事。

1.2 HTTP 与 TCP 各自负责什么

对本文的 HTTP/1.1 实验来说,TCP 负责可靠、有序地传输字节流;HTTP 负责解释这些字节:请求哪个资源、采用什么方法、处理结果如何、正文怎样定界。

TCP 不认识 GET,也不会因为看到 404 就判断连接出了故障。一个完整的 404 响应,完全可以通过正常的 TCP 连接传输。

HTTP 的名称虽然叫"超文本传输协议",正文并不限于超文本。下载压缩包、请求接口 JSON、加载图片,都可以使用 HTTP。媒体类型和应用规则决定内容含义。

1.3 一张网页可能对应多个请求

最初请求 / 得到 HTML;HTML 又引用 /logo.svg,浏览器便继续请求图片。它还可能请求样式、脚本和站点图标 favicon.ico。

所以服务端连续打印几次请求,并不一定是浏览器重复打开了页面。排查时应先比较目标路径,分清主文档和附属资源。浏览器是否请求某个图标,还会受到页面声明、缓存和具体实现影响,不能把一次观察当成固定次数。


二、把 URL 拆成有意义的部分

2.1 用一个完整例子拆解

text 复制代码
http://example.com:9090/docs/index.html?q=C%2B%2B#intro
部分 示例 作用
scheme http 说明使用的方案
host example.com 目标主机名
port 9090 显式指定端口
path /docs/index.html 标识资源路径
query q=C%2B%2B 查询部分,其内部字段格式由应用约定
fragment intro 片段标识,常用于页面内定位

主机名和可选端口属于 authority。本例没有用户名、密码等 userinfo;日常 HTTP URL 也不应拿 userinfo 作为网页登录方案。

? 开始查询,# 开始片段。q=...&page=... 是常见的查询参数写法,但通用 URI 语法本身并没有规定所有 query 都必须是键值表。RFC 3986:URI 语法

2.2 请求行中通常不是完整 URL

浏览器直接向源站请求时,常见的请求行是:

http 复制代码
GET /docs/index.html?q=C%2B%2B HTTP/1.1
Host: example.com:9090

路径和 query 放在请求目标里,目标主机由 Host 表达。片段 #intro 留在客户端处理,不随这个请求目标发送给服务器。

因此,不能把请求行里的第二项永远称为"完整网址"。更准确的名字是 request-target,请求目标 。经过正向代理时,还可能使用包含方案与主机的 absolute-form;OPTIONS * 又是另一种形式。本文服务器只实现面向源站的路径形式,以及 OPTIONS *。

2.3 80 是默认端口,不是唯一端口

http 的默认端口是 80,https 的默认端口是 443。实验选择 9090,只要客户端 URL 明确写出 :9090,仍然能使用 HTTP。

端口号帮助操作系统区分服务端点,协议格式由通信双方使用的规则决定。把服务改绑到 9090,不会自动变成另一种应用层协议。

同理,看到端口 443 也不能只凭数字证明当前字节一定是 TLS;协议识别还要结合实际通信。学习阶段把"默认约定"和"技术上只能如此"分开,会减少很多误解。


三、百分号编码到底编码了什么

3.1 编码的单位是字节

URL 中的某些字符具有分隔作用。如果一个查询参数的值本来就包含 & 或 +,必须避免它被误当成结构的一部分。

百分号编码的基本形式是 %HH:一个百分号,加上表示一个字节的两位十六进制数字。对于非 ASCII 字符,先按选定的字符编码得到字节,再逐字节编码。本文网页与表单使用 UTF-8。

原始内容 字节 百分号形式
+ 2B %2B
空格 20 %20
网 UTF-8:E7 BD 91 %E7%BD%91
络 UTF-8:E7 BB 9C %E7%BB%9C

这里不能简单理解为"把中文字符的 Unicode 码点转成十六进制,再每两位加百分号"。字符编码与百分号表示是两个步骤。换一种字符编码,得到的字节序列也可能不同。

3.2 加号什么时候表示空格

对 application/x-www-form-urlencoded 表单,空格通常编码为 +,字面量加号则编码为 %2B。所以字段值 C++ 网络 可以变成:

text 复制代码
q=C%2B%2B+%E7%BD%91%E7%BB%9C

但在通用 URL 语法中,不能见到任何 + 都改成空格。是否应用这条规则,取决于正在处理的组件与编码约定。WHATWG URL:表单编码

解码时也要小心顺序。表单解码应把原始 + 转为空格,再处理百分号字节;如果先把 %2B 解成 +,然后又把所有 + 变成空格,就会把字面量加号弄丢。

3.3 为什么不要对整个 URL 盲目编码

/ 可以分隔路径段,? 可以引出 query,& 可以分隔表单字段。对单个参数值编码时,通常要保护其中具有结构含义的字符;对整个 URL 直接套用同一规则,却可能把必要的分隔符也一起转义。

实际工作中应使用对应组件的编码函数,而不是把字符串到处做同一种"URL 编码"。反向解码也只执行约定次数;%252F 解一次是 %2F,解两次才是 /,两者可能改变路径解释。

百分号编码让字节能按约定出现在 URL 或表单中,它不是加密。知道编码规则的人可以恢复原始内容。


四、HTTP 请求与响应的报文结构

4.1 请求由请求行、头部、空行与可选正文组成

下面把控制字符写成可见的 \r\n,方便检查。真正发送时,它们应当是 CR、LF 两个实际字节。

text 复制代码
POST /echo HTTP/1.1\r\n
Host: 127.0.0.1:9090\r\n
Content-Type: text/plain\r\n
Content-Length: 7\r\n
\r\n
hello!!

第一行包含方法、请求目标和版本,三者之间使用空格。后续字段按 字段名: 字段值 表达,每行以 CRLF 结束。最后一个头部行的 CRLF,加上空行本身的 CRLF,形成 \r\n\r\n。

字段名不区分大小写,例如 Content-Length 和 content-length 表达同一个字段。字段值则需要遵循各字段自己的规则,不能把整份头部随便转成小写。方法名也区分大小写,标准方法使用 GET、POST 等写法。

4.2 响应第一行换成处理结果

text 复制代码
HTTP/1.1 200 OK\r\n
Content-Type: text/plain; charset=utf-8\r\n
Content-Length: 7\r\n
Connection: close\r\n
\r\n
hello!!

状态行包含版本、三位状态码和原因短语。200 是程序判断语义的关键;OK 是辅助说明。不能靠某个英文短语的拼写判断是否成功。

请求中的 Host 说明访问目标;响应中的 Content-Type 说明返回内容类型。并非所有字段都适合同时放在两个方向。请求头和响应头可以共享一部分字段,但各自有使用条件。

4.3 HTTP/1.1 是文本头部,不代表正文只能是文本

我们可以在终端直接读懂首行与大部分头部,所以常说 HTTP/1.1 报文具有文本形式。但正文可以是 PNG、压缩包,也可以包含 \0。

接收正文时必须根据实际字节数保存,不能使用遇到 NUL 就结束的 C 字符串操作。std::string 能保存 NUL;strlen(body.c_str()) 却只能算到第一个 NUL。这与上一篇 TCP 自定义协议里的问题完全一致。

同样,HTTP/2 和 HTTP/3 使用不同的线上帧格式。本文的逐行解析规则只针对 HTTP/1.x,不能因为它们同样叫 HTTP,就直接用 \r\n\r\n 去拆所有版本。


五、空行之后怎样判断正文结束

5.1 找到空行只完成了一半

假设请求头已经声明 Content-Length: 100,当前缓存里却只有头部和 20 字节正文。虽然找到了头部结束标记,仍然缺少 80 字节。

程序应先识别首行与头部,再根据消息定界规则收齐正文。一次 recv 可能只读到半个头,也可能同时读到正文甚至下一条请求。这些读取分组不会替 HTTP 划分消息。

5.2 Content-Length 计算字节,不计算汉字个数

如果正文是 UTF-8 的 你好,它占 6 字节,不能填写 Content-Length: 2。长度不包含状态行、头部、分隔空行,也不包含 C 字符串为方便存储而保留的终止符。

cpp 复制代码
std::string body = "hello!!";
std::string length = std::to_string(body.size()); // 7

只要 body 保存的是实际要发送的字节,size() 就能用于本例的长度计算。如果正文经过压缩,应针对实际传送的编码后内容确定对应长度,不能仍拿原始文本长度填报头。

5.3 有正文不一定有 Content-Length

PDF 用 Content-Length 解释正文边界,适合入门,但不是完整规则。HTTP/1.1 还可以使用分块传输编码;某些响应也可以通过关闭连接定界。

更要先识别特殊语义:HEAD 响应不发送正文,1xx、204 和 304 响应也不能按普通正文去读取。请求在没有适用的传输编码和长度字段时,通常按没有消息体处理,不能一律读到 EOF。RFC 9112:消息体长度

本文配套服务器只接受 Content-Length 定界的请求,对 Transfer-Encoding 明确返回错误。如果两者同时出现,直接拒绝并关闭连接。这样可以把教学范围写清楚,避免假装实现了所有 HTTP/1.1 解析规则。


六、GET 与 POST 的区别和表单实验

6.1 GET 请求资源的表示

http 复制代码
GET /search?q=linux HTTP/1.1
Host: 127.0.0.1:9090

GET 的语义是获取目标资源的表示。读取文章、搜索页面、查询接口都可能使用它。搜索参数常放在 query,但不能据此把 GET 定义成"参数一定在 URL 的请求"。

GET 请求内容没有通用定义的语义,部分实现会拒绝,所以不要把 GET 正文当成通用查询接口的约定。需要查询参数时,先按照接口约定构造 URL。

6.2 POST 提交内容供资源处理

POST 常用于提交表单或创建业务对象。正文可以是表单数据、JSON 或其他内容;服务器根据目标资源和 Content-Type 处理。

比较点 GET POST
典型目的 获取表示 提交内容供处理
HTML 表单字段位置 通常进入 query 通常进入正文
安全方法 是 否
幂等方法 是 不保证
缓存 常用 在满足相应条件时也可能被缓存
机密性 依赖 HTTPS 等保护 同样依赖 HTTPS 等保护

"安全方法"指客户端请求的语义不要求修改资源状态,服务器记访问日志不因此违反 GET 的安全语义。"幂等"指相同请求重复执行的预期效果,与执行一次相同;它不保证响应字节完全一致,也不表示永远不会失败。RFC 9110:方法属性

6.3 一个字段,两种提交方式

下篇附带的页面使用这两张表单:

html 复制代码
<form action="/search" method="get">
  <input name="q" value="C++ 网络">
  <button>查询</button>
</form>

<form action="/echo" method="post">
  <input name="q" value="C++ 网络">
  <button>提交</button>
</form>

输入框的 name 决定字段名;只有 value 而没有合适的名字,不会按这里的预期产生 q=... 字段。页面声明 UTF-8,浏览器按表单规则序列化字段。

GET 表单的目标会类似 /search?q=C%2B%2B+...;POST 表单把编码后的字段放进正文,并使用 application/x-www-form-urlencoded。配套 /echo 返回的是原始编码正文,方便观察,并没有把百分号编码恢复成中文。

6.4 两个流传很广的误区

第一,"POST 数据不在地址栏,所以安全"。HTTP 明文连接里的 URL、头部和正文都可能被观察;POST 只是换了消息位置。即便用了 HTTPS,也不应把敏感信息随意放进可能进入历史记录、日志或分享链接的 URL。

第二,"GET 只能传几 KB,POST 没有大小限制"。URL、请求头、正文大小都可能受到浏览器、服务器、代理和业务配置的限制。HTTP 方法本身不能替我们保证无限容量;服务器应明确声明并执行自己的上限。


七、HEAD、OPTIONS 与其他方法

7.1 HEAD 的响应没有正文

HEAD 与 GET 的语义相近,但服务器不能发送响应正文。它适合检查资源元数据,例如类型、长度和修改信息。

bash 复制代码
curl -I --http1.1 http://127.0.0.1:9090/

如果响应带有 Content-Length,这个值可以描述相应 GET 响应的正文长度。它不是让客户端在 HEAD 后继续等待这么多字节。实际实现中,某些只有生成正文时才能计算的字段也允许省略。

因此,不能用"响应声明长度大于零"反推一定要读取正文;请求方法同样参与响应定界。

7.2 OPTIONS 可以查询通信选项

bash 复制代码
curl -i --http1.1 -X OPTIONS http://127.0.0.1:9090/echo

配套实验会返回 Allow。OPTIONS /echo 面向具体目标,而 OPTIONS * 面向服务器整体,两者作用范围不同。

浏览器的 CORS 预检也使用 OPTIONS,但跨域许可还需要 Origin、Access-Control-Request-Method 及相应响应字段。返回一个 Allow,不等于已经配置好了跨域访问。

PDF 的 nginx 实验得到 405,说明该目标在当时配置下没有支持这个方法。它不意味着"HTTP 不支持 OPTIONS",也不能保证换一台 nginx 得到完全相同结果。

7.3 PUT 与 DELETE 不是固定的文件系统操作

PUT 通常表达创建或替换目标资源的状态;DELETE 请求移除目标资源与当前功能之间的关联。服务器可以把资源映射到数据库、对象存储或其他业务系统,并不一定直接执行磁盘文件覆盖、删除。

PUT、DELETE 按规范属于幂等方法。某次 DELETE 第一次返回成功、第二次返回 404,仍可能满足"最终删除效果相同"的幂等含义。接口设计应遵循方法语义,而不是只拿方法名当任意标签。

原资料方法表还列到 TRACE、CONNECT 等名称。TRACE 用于消息回环诊断,CONNECT 用于建立隧道,它们有各自的处理约定,本例未实现。表中出现某个方法名,不代表所有 Web 资源都必须允许它;实际能力应通过规范、服务配置与目标资源共同判断。


八、读懂状态码而非只看页面

8.1 第一位先表达类别

类别 主要含义 例子
1xx 信息性响应 100 Continue
2xx 请求成功处理 200、201、204
3xx 完成请求可能涉及后续动作 301、302、304
4xx 请求侧条件或内容有问题 400、401、403、404、405
5xx 服务端或上游处理失败 500、502、503、504

1xx 通常还不是最终响应;204 没有正文;304 用于条件请求中的缓存验证,也没有响应正文。不能把所有 3xx 都理解成"浏览器跳去另一个页面"。

8.2 常见错误码应该怎样区分

状态码 名称 重点
400 Bad Request 请求格式等有问题
401 Unauthorized 缺少适用的认证凭据,通常伴随认证挑战
403 Forbidden 服务端理解请求,但拒绝执行
404 Not Found 当前未找到资源,或服务端不愿公开其存在
405 Method Not Allowed 当前目标不支持该方法,响应应提供 Allow
500 Internal Server Error 服务端发生意外处理错误
502 Bad Gateway 网关或代理从上游得到无效响应
503 Service Unavailable 服务暂不可用,例如过载或维护
504 Gateway Timeout 网关或代理等待上游响应超时

PDF 一处把 504 写成 Bad Gateway,这里修正为 Gateway Timeout。Bad Gateway 对应 502。

8.3 404 页面漂亮,也仍然可以是 404

服务器可以在 404 响应正文中放一份完整 HTML,浏览器照样能显示标题、图片和按钮。页面"看起来正常",不能证明状态码是 200。

反过来,接口也可能在 HTTP 200 正文中放业务失败信息。排查时要同时看 HTTP 状态和应用协议,不能只看一个成功图标。好的接口设计会让二者的职责清楚,客户端也应按约定分别处理。

下篇的 /status/403、/status/504 用于观察状态行与页面表现,属于人为演示。它们并没有真的实现权限系统或等待上游,因此不能用演示结果证明对应故障已发生。


九、重定向为什么会产生第二次请求

9.1 Location 告诉客户端接下来去哪里

text 复制代码
HTTP/1.1 301 Moved Permanently\r\n
Location: /\r\n
Content-Length: 0\r\n
Connection: close\r\n
\r\n

客户端先请求旧地址,收到重定向响应后,再向新目标发出请求。服务器返回 301 并不是在连接内部替浏览器把页面内容"搬过来"。

Location 可以是完整 URL,也可以是像 / 这样的相对引用。没有可用的位置,客户端便无法按这条响应找到后续目标,所以正常重定向实现应当一并构造它。

9.2 永久、临时和方法保留要分别考虑

状态 名称 是否永久 方法处理重点
301 Moved Permanently 是 历史兼容允许把 POST 改成 GET
302 Found 否 同样存在 POST 改为 GET 的兼容行为
303 See Other 否 引导获取另一个资源,常用于提交后转到结果页
307 Temporary Redirect 否 自动重定向时保持方法
308 Permanent Redirect 是 自动重定向时保持方法

原稿把 302 的说明写成 "Found 或 See Other",这里需要区分:302 是 Found,303 才是 See Other。如果重定向后必须继续保留 POST 等方法,应考虑 307 或 308 的对应语义。

临时重定向也不等于"绝对不会缓存"。缓存行为还受状态定义和响应缓存控制影响,不能单凭"临时"二字下结论。RFC 9111:缓存存储条件

9.3 curl 默认只展示第一轮

bash 复制代码
# 只看旧地址返回的 301
curl -i --http1.1 http://127.0.0.1:9090/old

# 跟随 Location,再获取新地址
curl -i -L --http1.1 http://127.0.0.1:9090/old

第二条命令可能展示两组响应头:前一组对应重定向,后一组对应最终资源。不要把它们误当成"一个响应里出现两个状态行"。


十、用开发者工具与 curl 串起整条链路

10.1 先启动配套实验

进入下篇的 code 目录,执行:

bash 复制代码
g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror server.cpp -o http_server
./http_server 9090

服务器工作目录需要包含 wwwroot/。它只监听本机 127.0.0.1;在 Linux 环境内用 curl 测试最直接。使用 WSL 时,宿主浏览器能否通过 localhost 访问,还取决于本机网络转发配置。

10.2 把浏览器操作翻译成报文观察

浏览器开发者工具打开 Network 后,访问主页、提交 GET 表单、再提交 POST 表单。分别查看 Request URL、Method、Status、Headers 与 Payload。

POST 表单的数据显示界面可能已经解码,想比较线上字节,需要切换到源数据或对应原始视图;本例回显响应也能帮助观察百分号编码。开发者工具的呈现方式不等于网络上的原始文本。

bash 复制代码
curl -i --http1.1 'http://127.0.0.1:9090/search?q=C%2B%2B'
curl -i --http1.1 --data 'q=C%2B%2B+network' http://127.0.0.1:9090/echo
curl -I --http1.1 http://127.0.0.1:9090/
curl -i --http1.1 -X OPTIONS http://127.0.0.1:9090/echo
curl -i --http1.1 http://127.0.0.1:9090/missing

Linux Shell 中 URL 带 & 时尤其应使用引号,否则它可能被 Shell 解释为后台执行符。发请求之前,先确保命令行本身没有改变你准备传递的字符串。

10.3 建立正确的排查顺序

先确认连接是否成功,再检查请求目标与方法,然后看响应状态和头部,最后分析正文。连接被拒绝、404、正文乱码和表单值没解码,分别发生在不同阶段,解决方法也不同。

本篇把 HTTP 当作一套可以读懂的消息规则;下篇会把这些规则落实到 C++:先累积头部,按长度收正文,再映射资源、构造响应并完整发送。


🌄 愿每一次阅读、编译与调试,都能让我们离清晰可靠的代码更近一步。如果这篇记录对你有帮助,欢迎点赞、收藏,也欢迎交流你的实现与问题。

相关推荐
weixin_417396832 小时前
老酒店有线电视改造怎么做?三种路径对比与施工节奏
网络·技术·酒店·1024程序员节·综合布线·弱电
jimy12 小时前
C++ 中 xvalue 的 identity 存在哪里?和 lvalue 的 identity 有什么区别?
开发语言·c++·1024程序员节
机器学习之心2 小时前
198种组合算法+优化CNN-Transformer+SHAP分析+新数据预测+多输出!
1024程序员节
yujunl2 小时前
U9 无授权的模式WebAPI开发的总结
开发语言·1024程序员节
AI日报派送佬2 小时前
2026年10月24日AI行业日报|OpenAI离线训练前沿模型、Kimi K3.1 Pro上线API、Prism推理引擎开源
人工智能·openai·1024程序员节·ai智能体·kimi·ai日报
jiangliushi62 小时前
GPT-6.1 Sol 免费用:Python 读文件报 ‘gbk‘ codec can‘t decode?加一个参数就好
python·编码·1024程序员节
vibecoding772 小时前
Kimi K3.1 Pro 发布:2.8 万亿参数、Terminal-Bench 超 GPT-6 Astra,定价砍到 Opus 5.5 一半
人工智能·1024程序员节