在 Android 页面里调用一次接口,日志只留下
GET /articles/42和200。页面第二次打开却更快,抓包偶尔出现304;换成弱网后,请求可能连状态码都没有。它们是同一条请求链上的不同结果,光记"HTTP 是超文本传输协议"解释不了。
一句话结论:HTTP 规定客户端如何表达请求、服务端如何表达结果,以及缓存如何复用结果;一次请求是否建新连接、是否传回响应体,还要看缓存状态、连接复用和所用的 HTTP 版本。
本文以 Android App 读取一篇文章为例,按 RFC 9110--9114 的协议语义解释主线。Android 落点参考平台网络文档与 OkHttp 的公开说明。下文的报文是教学示意,不是某个真实接口的抓包;先不展开 DNS、TLS 握手细节、代理实现和 QUIC 拥塞控制。
1. 先看全局:一次请求有哪些参与者?
text
页面发起读取
→ HTTP 客户端构造请求
→ 本地缓存判断能否直接复用
→ 需要联网时,选择已有连接或建立连接
→ 服务端处理目标资源
→ 返回状态、首部与可选的响应内容
→ 客户端处理缓存并把结果交回页面
这张图是概念流程。页面通常交给网络库一个 URL、方法和可选的请求体;网络库负责报文编码、连接管理和响应读取。缓存既可能直接给出结果,也可能让客户端带着校验条件去问服务端。服务端决定资源的表示、状态码和缓存指令。最终页面需要处理的是成功数据、业务错误或网络失败,而不是自己拼装 TCP 字节。
HTTP 在应用层定义"要对哪个资源做什么"和"结果是什么"。https:// 表示这次 HTTP 通信受到 TLS 保护;它并没有把 GET、状态码、缓存这些 HTTP 语义换掉。至于线上字节怎样组织,要看 HTTP/1.1、HTTP/2 或 HTTP/3。
2. 一次请求和响应,分别写了什么?
先把下面的例子当成 HTTP/1.1 的可读示意。真实报文还会有其他首部,HTTPS 下也不能直接从普通网络抓包中读到这些明文。
http
GET /articles/42 HTTP/1.1
Host: api.example.com
Accept: application/json
请求行给出方法、目标路径和协议版本;Host 指向目标主机,Accept 表示客户端愿意接收 JSON。空行结束首部。这个 GET 没有请求体,并不代表所有请求都没有请求体。HTTP/1.1 的起始行、首部、空行与可选消息体结构由 RFC 9112 定义。
http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: private, max-age=60
ETag: "article-42-v7"
{"id":42,"title":"HTTP 入门"}
响应的 200 表示请求成功;Content-Type 告诉客户端怎样理解内容。这里服务端允许私有缓存 在新鲜期内复用响应,并给出 ETag 作为以后校验版本的标识。max-age=60 是这条示意响应的 60 秒新鲜期,不是 HTTP 的统一默认值。
有一个层级细节:上面的文字格式只用于讲 HTTP/1.1。HTTP/2 和 HTTP/3 继续承载方法、路径、状态码与首部等语义,但线上使用帧与流,不能把这段文本当作它们的实际线格式。RFC 9110 定义通用语义,RFC 9113 与 RFC 9114 分别定义后两种映射。
3. 方法和状态码该怎样读?
读请求时,我会先看方法是否符合业务意图,再看响应状态。常见方法可以这样记:
| 方法 | 约定的意图 | 排查时要注意 |
|---|---|---|
GET |
获取目标资源的表示 | 按协议语义是安全且幂等的;不要用它触发转账、下单等状态改变。 |
POST |
让目标资源处理提交的内容 | 常用于创建或执行操作;不能仅凭方法推断重试安全。 |
PUT |
用提交的表示创建或替换目标资源的状态 | 按协议语义幂等,但相同请求的响应不一定每次相同。 |
DELETE |
删除目标资源与其当前功能的关联 | 按协议语义幂等,不等于每次都返回相同状态码。 |
"幂等"说的是重复同一请求的预期服务端效果 与执行一次相同,不是"绝不会写日志",也不是"每次都返回相同字节"。因此,连接中途断开时,客户端能否自动重试,首先取决于方法语义和接口自身的设计。特别是支付、下单一类 POST,应由业务提供幂等键或明确的去重机制,再决定重试策略。RFC 9110 的方法语义 对安全和幂等有严格定义。
状态码也先按类别读,再看具体含义:
| 类别 | 常见例子 | 给 Android 端的信号 |
|---|---|---|
2xx 成功 |
200、201、204 |
204 已成功但没有响应内容,不要强行按 JSON 解析。 |
3xx 重定向或缓存协商 |
301、302、304 |
304 表示本地已有的表示仍可用,通常配合条件请求。 |
4xx 请求侧问题 |
400、401、404、429 |
需要结合接口契约处理;重复原样请求未必有用。 |
5xx 服务端问题 |
500、503 |
可考虑有边界的重试,但仍要看操作是否可安全重复。 |
表中的处理建议是实践建议,不是状态码本身自动替 App 完成的行为。尤其要把"收到 HTTP 错误响应"和"根本没收到 HTTP 响应"分开:DNS 失败、TLS 校验失败、超时或断网时,通常没有可供读取的 HTTP 状态码。
4. 第二次打开页面,为什么可能没有新内容传回来?
继续看 /articles/42。如果客户端确实配置并保存了可用缓存,在示意响应仍新鲜时,可以直接使用缓存表示,不发网络请求。过了新鲜期,也未必需要重新下载正文。客户端可带上先前的实体标签:
http
GET /articles/42 HTTP/1.1
Host: api.example.com
If-None-Match: "article-42-v7"
服务端确认表示没变时,可能回复 304 Not Modified,不再传输那份正文。客户端用已保存的表示完成读取。所以 304 有网络往返,但没有重复下载表示内容 ;它和完全离线的缓存命中不是一回事。RFC 9110 对 304 的定义 与 RFC 9111 的缓存规则 是这条路径的依据。
| 指令或字段 | 这里要读出的意思 |
|---|---|
Cache-Control: max-age=60 |
在指定的新鲜期内可以按缓存规则复用。 |
Cache-Control: no-cache |
可以存储,但复用前通常需要向源站验证。 |
Cache-Control: no-store |
缓存不得存储这次响应。 |
Cache-Control: private |
共享缓存不能存;符合条件的私有缓存可以存。 |
ETag / If-None-Match |
一端给出版本标识,另一端用它发条件请求。 |
这里还有一个 Android 常见误判:服务端发了 Cache-Control,不等于 App 的网络库已经开启磁盘缓存。OkHttp 的 Cache 是可选且默认关闭 的;只有配置了缓存,才谈得上它的本地缓存命中与条件校验。缓存策略也不能替代敏感数据的存储保护。OkHttp 缓存文档 明确说明了默认状态与命中路径。
5. HTTP/1.1、HTTP/2、HTTP/3 改了哪一层?
三个版本共享 HTTP 的核心语义,主要差别在消息怎样在连接上传输。
| 版本 | 消息传输方式 | 常见底层路径 | 直观影响 |
|---|---|---|---|
| HTTP/1.1 | 文本起始行和首部;连接可复用 | 通常是 TCP,HTTPS 时再加 TLS | 多次请求可以复用连接,但协议本身没有 HTTP/2 那样的流多路复用层。 |
| HTTP/2 | 二进制帧与多路复用流 | 通常是 TLS over TCP | 同一连接上可并发承载多个请求;底层 TCP 丢包仍可能影响该连接的其他流。 |
| HTTP/3 | 在 QUIC 流上承载 HTTP 帧 | QUIC 基于 UDP,并集成 TLS 1.3 握手 | 不同流的可靠传输由 QUIC 管理,减少 TCP 层丢包造成的跨流阻塞。 |
这不是"版本越高,单个接口一定越快"的承诺。真实耗时还取决于连接是否复用、服务端处理、网络丢包、缓存和客户端实现。排查时先确认实际协商出的协议版本 ,别看到代码里的 https:// 就推断一定用了 HTTP/2 或 HTTP/3。RFC 9113 与 RFC 9114 分别描述了 HTTP/2 多路复用和 HTTP/3 的 QUIC 映射。
6. 回到 Android:看到慢请求或失败,先查哪里?
我的排查顺序是先分清有没有响应,再研究响应内容。没有状态码,就从设备网络、DNS、连接、TLS、超时和取消查起;拿到状态码后,再看方法、目标 URL、关键首部、服务端返回和缓存命中情况。这样的顺序比一上来把所有失败都归成"接口 500"有效。
Android 端还要守住几个边界:
- Manifest 需要
INTERNET权限;网络 I/O 不应放在主线程。平台文档说明,主线程做网络操作会触发NetworkOnMainThreadException。Android 网络连接指南 - 生产请求优先用 HTTPS,并正确验证证书。面向 Android 9(API 28)及以上版本的应用默认限制明文流量;不要为调通一个测试接口就全局放开明文。Android 明文流量说明
- 连接池复用的是连接 ,缓存复用的是响应表示。关闭或读完响应体、取消不再需要的调用,和缓存配置都属于客户端资源管理;具体 API 取决于所用网络库。
- 把 HTTP
4xx/5xx、业务 JSON 中的错误码与 I/O 异常分开记录。它们发生在不同层,决定了提示、重试和排查方向。
回头看开头的现象:第二次打开更快,可能是连接复用,也可能是缓存命中;看到 304,说明至少发生了一次条件请求;连状态码都没有,则先检查 HTTP 响应生成之前的链路。抓住"语义、缓存、传输"这三层,就能把一次 HTTP 请求从页面一直追到结果。