引言:别再死记硬背状态码了,HTTP 其实是这么回事
很多朋友一提到 HTTP,第一反应就是一堆状态码:200、301、404、500……然后开始背"200 是成功,404 是找不到,500 是服务器错误"。背完之后呢?遇到真实问题照样抓瞎。比如你访问一个网站,浏览器转圈半天,打开开发者工具一看,一堆请求标红,你只知道"出错了",但到底是哪里错了、为什么错、怎么排查,完全没头绪。
我在日常开发和抓包调试里见过太多这样的例子。HTTP 看起来简单——不就是客户端发请求、服务器回响应吗?但真正把它搞透的人不多。很多人在网上搜"HTTP 基础",看到的都是概念罗列,学完就忘。这篇我换个思路,不讲那些教科书式的定义背诵,而是从一次完整的请求生命周期讲起,把你平时会遇到的现象、报错、工具用法全部串起来。你会理解:
- URL 里的每个部分到底在表达什么,为什么有的网址带
https有的不带 - 请求报文和响应报文里面到底装了些什么,每一行是干嘛的
- 为什么浏览器会缓存、Cookie 是怎么"赖"在你电脑上的、Session 又是什么鬼
- 面试里常问的 GET 和 POST 区别,实际工作中真正的注意事项是什么
适合谁看?刚入门的前端、后端、运维同学,或者工作中经常需要看网络请求、排查接口问题的测试和产品同学。即便你完全没接触过网络协议,只要跟着这篇文章的节奏走,也能把 HTTP 建立起一个非常扎实的图景。等到下次再有人跟你说"这个接口报 502 了",你第一反应不再是"502 是啥来着",而是"网关出问题了,上游服务挂了或者拒绝连接了,我去看看后端日志"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1. 从你在地址栏敲下网址开始:一次 HTTP 请求的完整旅程
1.1 输入 URL 的那一刻,浏览器在做什么
假设你在浏览器地址栏输入 https://www.example.com/products?id=123 然后回车。这一瞬间,背后发生的事情比你想象的多得多。我们先把这个 URL 拆开来看,很多人学了几年 HTTP,对 URL 的结构还是模糊的。
一个标准的 URL 长这样:
text复制https://www.example.com:443/products?id=123#section
拆开来看:
| 组成部分 | 示例 | 作用 |
|---|---|---|
| 协议 | https(也可以是 http、ftp 等) |
告诉浏览器用哪种语言和规则去和服务器沟通 |
| 域名 | www.example.com |
服务器的"门牌号",人类好记的名字 |
| 端口号 | 443(HTTPS 默认 443,HTTP 默认 80) |
服务器上的"房门号",一台服务器可以同时开很多服务,靠端口区分 |
| 路径 | /products |
你要访问的服务器上的具体资源位置 |
| 查询参数 | ?id=123 |
以键值对形式传给服务器的额外信息 |
| 锚点 | #section |
页面内部的定位标记,不会发送给服务器 |
这里有个很多人容易忽略的细节:端口号。你在浏览器里很少看到带端口号的网址,因为 80(HTTP)和 443(HTTPS)是默认端口,浏览器会自动补全。但如果你自己搭了个服务,比如在本地跑一个 Node.js 服务监听 3000 端口,就得在地址栏里明确写 http://localhost:3000,否则浏览器默认去访问 80 端口,结果当然是连接被拒。排查"我服务明明启动了但访问不了"这种问题,第一个就该检查端口对不对。
1.2 DNS 解析:把"门牌号"翻译成"坐标"
拿到域名之后,浏览器并不知道 www.example.com 这台服务器在互联网的哪个角落。它需要把域名翻译成 IP 地址——一串类似 93.184.216.34 的数字,这才是网络层真正认得的"坐标"。这个翻译过程就叫 DNS(Domain Name System)解析。
DNS 解析的顺序大致是:
- 浏览器缓存:浏览器自己会记一段时间内解析过的域名
- 操作系统缓存:浏览器没找到就去问操作系统,系统也有一份 DNS 缓存
- 本地 hosts 文件:如果你手动在 hosts 里配置过域名映射,它会直接生效
- 递归 DNS 服务器:通常是你的路由器或者运营商提供的 DNS,由它帮你一层层往上查询
这个过程和 HTTP 本身没有直接关系,但它直接决定了你能不能访问到服务器。常见的"网站打不开"问题,有一大半是 DNS 解析失败或解析到了错误地址。我遇到过很多次,办公室网络突然访问不了某个网站,但手机用 4G 能正常打开,基本就是公司的 DNS 出问题了。用 nslookup 或 dig 命令手动查一下域名解析结果,就能快速定位。
1.3 TCP 连接建立:三次握手不是一个抽象概念
拿到 IP 地址之后,浏览器要和服务器建立一条 TCP 连接。HTTP 本身是"无状态"的应用层协议,它依赖 TCP 来保证数据的可靠传输。TCP 连接的建立需要"三次握手":
- 客户端发一个
SYN包:我想和你建立连接 - 服务器回一个
SYN + ACK包:收到,我也准备好了 - 客户端再发一个
ACK包:确认收到,我们开始传输吧
为什么需要三次而不是两次?核心在于双方都要确认"自己发的消息对方能收到,对方发的消息自己能收到"。两次握手只能让服务器确认客户端能发能收,但客户端无法确认服务器能不能收到自己的数据。这就是网络工程师常说的"全双工通信的可靠建立"。
在实际排错中,理解 TCP 握手的意义在于:当你看到一个请求一直处于 Pending 状态,或者报 PROTOCOL_ERROR、connection timeout,你该意识到问题可能根本不在 HTTP 层,而是 TCP 连接压根没建立起来。用抓包工具看,如果只有 SYN 发出而没有 SYN+ACK 回来,说明服务器不可达,或者中间防火墙把包丢了。
1.4 发送请求与接收响应:HTTP 报文长什么样
TCP 连接建立好之后,真正的 HTTP 请求/响应才正式开始。HTTP 的报文格式非常简单,可以说是一种"有规矩的文本"。一个请求报文长这样(以访问 /products?id=123 为例):
http复制GET /products?id=123 HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: sessionId=abc123
第一行叫请求行,包含三个部分:请求方法(GET)、请求目标(路径+查询参数)、协议版本(HTTP/1.1)。后面跟着的每一行叫请求头,携带各种元信息——Host 指明目标主机,User-Agent 告诉服务器我用的是什么浏览器,Cookie 则把之前服务器发给我的"身份凭证"带回去。
请求头结束之后,会有一个空行,然后才是请求体(Body)。像 GET 这种请求通常没有请求体,但 POST、PUT 这类请求会在空行后面跟上具体的数据内容。
服务器收到这个请求后,会返回一个响应报文:
http复制HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Set-Cookie: sessionId=abc123; Path=/; HttpOnly
Cache-Control: max-age=3600
<!DOCTYPE html>
<html>...</html>
同样,第一行叫状态行,由协议版本、状态码、状态描述组成。接着是响应头,之后空行,再是响应体——也就是真正的内容数据。
这部分理解了,你就掌握了 HTTP 的核心骨架。后面所有关于缓存、Cookie、连接复用的话题,都是在这个骨架上添加细节。
2. 请求方法深入:GET 和 POST 老生常谈,但你真的会用吗
2.1 常见的请求方法远不止 GET 和 POST
很多人以为 HTTP 只有 GET 和 POST 两种方法,这其实是个天大的误解。HTTP/1.1 标准里定义了八种方法,常用的是下面这几个:
| 方法 | 语义 | 典型场景 | 幂等性 |
|---|---|---|---|
| GET | 获取资源 | 打开页面、查询列表、下载文件 | 幂等 |
| POST | 创建资源/提交处理 | 提交表单、上传数据、登录 | 不幂等 |
| PUT | 整体更新/替换资源 | 更新用户的完整信息 | 幂等 |
| PATCH | 部分更新资源 | 只改用户头像、只改昵称 | 不幂等(但语义上是) |
| DELETE | 删除资源 | 删除一条记录 | 幂等 |
| HEAD | 只获取响应头 | 检查资源是否存在、探测链接有效性 | 幂等 |
| OPTIONS | 询问服务器支持哪些方法 | 跨域请求预检 | 幂等 |
幂等这个概念很重要:意思是"同一个请求执行一次和执行多次,结果是一样的"。GET 请求查询 10 次和查询 1 次,服务器上的数据不会有任何变化;DELETE 删除一条记录,第一次删除成功,第二条再删除会发现记录已经不存在了,但最终状态都是"这条记录没了"。POST 不幂等是因为每次提交都会创建一条新数据——提交一次订单生成一个订单号,再提交一次又生成一个,不能乱用。
2.2 面试最爱问的"GET 和 POST 区别",哪些是对的
关于 GET 和 POST 的区别,江湖上流传着很多说法,有些是对的,有些是过时的、片面的。我把常见的说法逐条拆解:
"GET 参数放在 URL 里,POST 参数放在请求体里"——基本对,但不是绝对的。 GET 的查询参数确实放在 URL 的 ? 后面;POST 一般把数据放在请求体里,但也可以把参数放在 URL 上。反过来,GET 也不是绝对不能有请求体,只是大多数服务器和框架都不支持也不建议那么做。
"GET 有长度限制,POST 没有"——这个说法有误导性。 URL 的长度限制其实不是 HTTP 协议规定的,而是浏览器和服务器主动做的限制。IE 大约限制 2083 个字符,Chrome 实测对 URL 长度相对宽松但依然有限制。POST 请求体理论上没有上限,但服务器端(如 Nginx)通常也有 client_max_body_size 这样的配置控制上传大小。所以准确的说法是:URL 的长度限制来自客户端和服务器实现,POST 请求体可以承载更大数据量。
"GET 比 POST 快"——不完全对。 对于同样的网络环境,两者的传输速度没有本质差别。之所以有人觉得 GET 快,是因为浏览器对 GET 请求有更积极的缓存策略,第二次请求直接从本地缓存读取了,看起来当然快。POST 请求默认不缓存。
"POST 比 GET 更安全"——这是最大的误解。 实际上两者在网络传输过程中,如果不使用 HTTPS,都是明文传输,抓包都能看到。GET 的参数在 URL 里可能通过浏览器历史记录、服务器访问日志、Referer 泄漏出来,确实更容易暴露;但 POST 的请求体同样可以被抓包工具、中间人攻击截获。真正的安全必须靠 HTTPS 加密,而不是靠把参数放在请求体里。
2.3 实际工作中真正的选择依据
抛开面试话术,在实际设计和开发中,选择 GET 还是 POST 的标准其实非常简单:
- 读操作用 GET,写操作用 POST(或 PUT、DELETE)
- 请求会把服务器数据改变(新增、修改、删除),绝对不要用 GET
- 请求只是想查询数据,不要用 POST(虽然技术上能通,但语义不对,会给后面积累坑)
为什么语义不对会有坑?举个例子:搜索引擎的爬虫、浏览器的预加载机制,会对 GET 请求发起自动访问。如果你把一个"删除用户"的接口用 GET 实现,爬虫在抓取页面时正好碰到了这个链接,用户就被删了。这不是段子,真实世界发生过多次类似事故。HTTP 方法本身就是一种接口设计语言,遵守它的语义会让系统更安全、更可维护。
还有一个细节值得注意:URL 里的查询参数会被记录在服务器日志里。用 GET 提交敏感信息(密码、token 等),等于把凭证写在服务器的访问日志中,这是很危险的做法。有人问"那我用 POST 就安全了吧?"不,如果服务端把请求日志全部打了,POST 体一样会被记录。但至少 POST 不会把参数留在浏览器的历史记录和 Referer 中,暴露面小很多。
3. 状态码与响应头:读懂服务器想告诉你什么
3.1 状态码分类:一眼定位问题属于哪个层次
状态码是服务器用数字向客户端传递"这次请求的结果如何"。所有状态码按首位数字分为五大类:
| 分类 | 范围 | 含义 | 典型代表 |
|---|---|---|---|
| 1xx | 100-199 | 信息性响应,还在处理中 | 100 Continue |
| 2xx | 200-299 | 请求成功 | 200 OK、201 Created、204 No Content |
| 3xx | 300-399 | 重定向,需要进一步操作 | 301 Moved Permanently、302 Found、304 Not Modified |
| 4xx | 400-499 | 客户端错误,请求有问题 | 400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found |
| 5xx | 500-599 | 服务器错误,请求本身没问题 | 500 Internal Server Error、502 Bad Gateway、503 Service Unavailable |
这个分类的价值在于:看到状态码,你至少能判断责任在谁那边,排查方向就不会跑偏。 4xx 的问题基本出在请求本身——URL 写错了、参数不对、没带 token、没有权限;5xx 的问题出在服务器——代码崩了、依赖的服务挂了、负载过高。我看到很多人排查问题,接口报 404 却去翻后端日志半天,其实应该先看看请求路径拼对了没有。
3.2 几个高频状态码背后的真实场景
200 OK:请求成功,响应体里就是你要的资源。没什么好说的,一切正常。
301 Moved Permanently 与 302 Found:都是重定向,但有本质区别。301 是"这个资源永久搬走了,以后都别用旧地址了",浏览器会记住新地址,下次直接访问新地址;302 是"这次临时去另一个地址",下次访问还是会先走旧地址。实际工作中,网站改成 HTTPS 后,HTTP 的请求通常会 301 跳转到 HTTPS;登录后跳转首页通常用 302。如果你发现网页强制跳到某个莫名其妙的新地址,大概率是 301 被配置错了。
304 Not Modified:这个状态码很有意思,服务器没有返回响应体,只有一个头信息。它告诉浏览器:"你本地缓存的版本还是最新的,直接用缓存吧。"浏览器发送请求时会带上 If-Modified-Since 或 If-None-Match 头,服务器判断资源没变化,直接回 304。这就是网页第二次打开比第一次快很多的核心原因之一。
401 Unauthorized 与 403 Forbidden:经常被搞混。401 是"你没登录,或者凭证不对,请先认证";403 是"你登录了,但你没有权限访问这个资源"。区分场景:访问需要登录的页面未登录时返回 401;登录了普通账号试图访问管理员后台返回 403。排查接口时看到 403,先想想是不是 token 没带、权限不够,而不是重新登录就完事。
404 Not Found:请求的资源不存在。常见的坑是接口路径拼写错误、服务部署后路由没配置对。也有的团队为了安全,会故意把不存在的资源返回 404 而不是 403,避免泄露目录结构信息。
500 Internal Server Error:服务器内部错误,后端代码崩了、数据库连接失败、配置错误都可能触发。这是排查工作量最大的状态码,需要看服务器日志才能定位。
502 Bad Gateway 与 503 Service Unavailable:这两个在服务架构里特别常见。502 表示网关或代理服务器(比如 Nginx)从上游服务器收到了无效响应——上游服务挂了、拒绝了请求、或者返回了无法理解的内容。503 表示服务器暂时无法处理请求——服务过载、正在维护。前者多半是上游服务的锅,后者多半是流量或运维的问题。
提示:遇到 502,第一反应不是去查业务日志,而是先确认上游服务进程是否存活、端口是否在监听、负载均衡器的健康检查有没有通过。我用
curl -v命令直接请求上游服务,如果上游本身正常,再去查 Nginx 配置里的 proxy_pass 指向对不对。
3.3 用 curl 调试状态码,比起浏览器更直接
浏览器开发者工具虽然直观,但有些场景下效率不高。比如你在服务器上排查接口问题,没有图形界面,这时候 curl 就是最强工具。几个常用参数:
bash复制# 查看完整请求和响应头
curl -v https://api.example.com/users
# 只查看响应头,不下载响应体(用 HEAD 请求)
curl -I https://www.example.com
# 指定请求方法
curl -X POST https://api.example.com/users -d '{"name":"Tom"}' -H "Content-Type: application/json"
# 跟随重定向
curl -L https://example.com
# 设置超时时间(防止卡死)
curl --connect-timeout 5 --max-time 10 https://example.com
-v 参数会打印整个请求和响应过程,包括 TCP 连接、TLS 握手、请求头、响应头,信息量非常大。排查"接口响应太慢"的问题,用 curl -w 还能精确看到各阶段的耗时:
bash复制curl -w "DNS解析: %{time_namelookup}s\nTCP连接: %{time_connect}s\nTLS握手: %{time_appconnect}s\n首字节: %{time_starttransfer}s\n总耗时: %{time_total}s\n" -o /dev/null -s https://www.example.com
输出结果就能告诉你时间花在了哪个环节——是 DNS 解析慢、TCP 连接慢、还是服务器处理慢。这个技巧在性能排查时非常实用。
4. 无状态协议的妥协:Cookie、Session 与 Token 的前世今生
4.1 HTTP 的"失忆症"与它带来的问题
HTTP 协议设计之初是无状态的——每个请求都是独立的,服务器不记得你是谁。你第一次请求时带了用户名密码,第二次再发一个请求,服务器完全不记得你刚才来过。这就产生了一个问题:怎么让服务器记住"这个人已经登录过了"?
为了解决这个问题,人类发明了 Cookie。
Cookie 的机制其实很朴素:服务器在响应头里通过 Set-Cookie 下发一小段数据,浏览器把它存下来;后续每次请求,浏览器自动在请求头里带上 Cookie 字段回传给服务器。服务器一看 Cookie,就知道"哦,是上次登录的那个人"。
4.2 Cookie 的属性和安全边界
Cookie 不是简单的一个键值对,它自带很多属性,这些属性直接决定它的行为和安全级别:
| 属性 | 作用 |
|---|---|
Expires / Max-Age |
过期时间,到达时间后浏览器删除该 Cookie |
Domain |
指定哪些域名可以接收该 Cookie |
Path |
限定路径范围,只在匹配路径的请求中携带 |
Secure |
只有在 HTTPS 连接下才发送该 Cookie |
HttpOnly |
禁止 JavaScript 通过 document.cookie 读取,防 XSS 攻击 |
SameSite |
控制跨站请求是否携带 Cookie,防 CSRF 攻击 |
实操中最重要的建议:所有涉及身份认证的 Cookie 必须设置 HttpOnly 和 Secure。 不设 HttpOnly,攻击者一旦找到 XSS 漏洞,就能用脚本把你的 Cookie 偷走,直接冒充你的身份。不设 Secure,在 HTTP 明文连接下 Cookie 会被中间人直接抓走。我之前见过一些内部系统,Cookie 连 HttpOnly 都没设,这等于把大门钥匙放在门口的脚垫下面。
SameSite 属性是近年特别值得留意的。跨站请求伪造(CSRF)攻击的核心原理就是:你在 A 网站登录了,然后去访问恶意网站 B,B 页面里的脚本发起请求到 A 网站,由于 Cookie 会自动带上,服务器误以为这是你本人的操作。SameSite=Lax(浏览器默认值)能阻止大部分跨站的 Cookie 携带,极大缓解 CSRF。不过要注意,Cookie 是自动携带的,这个机制既是它的便利也是它的风险——你无法通过前端代码控制它带不带,只能通过属性来约束。
4.3 Session:把身份信息从客户端搬到服务器
Cookie 解决了"保持登录"的需求,但直接把用户名、权限等敏感信息存在 Cookie 里是不安全的(容易被篡改和伪造)。于是诞生了 Session 机制:
- 用户登录成功后,服务器在内存或数据库中创建一个 Session 对象
- 服务器生成一个随机的 Session ID,通过
Set-Cookie下发给浏览器 - 浏览器后续请求带上这个 Session ID
- 服务器拿着 Session ID 找到对应的 Session 数据,完成身份识别
Session 的本质是"数据存服务器,凭证存客户端"。 Cookie 里只存一个随机的、无法推导的 ID,真正的用户信息在服务器上,安全性提高了不少。
但 Session 机制也有天生缺陷:服务端需要维护状态,这在分布式架构下很痛苦。 用户登录请求到了服务器 A,Session 存在 A 的内存里;下一个请求被负载均衡转发到服务器 B,B 的 Session 里没有这个用户,用户就被迫重新登录。解决办法包括 Session 粘滞(固定的用户请求固定发到同一台机器)、Session 复制(各机器之间同步)、或者把 Session 集中存到 Redis 这类外部存储。无论如何,都是额外的维护成本。
4.4 Token 方案:彻底抛开服务端状态
Token 认证的核心思路是:不再在服务器保存登录态,而是把用户信息加密后签发给客户端,客户端每次请求带上,服务器验签通过就认为是合法用户。
最典型的实现是 JWT(JSON Web Token),它的结构是 Header.Payload.Signature 三部分:
text复制eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiZXhwIjoxNzE1MDAwMDAwfQ.签名部分
- Header:声明签名算法
- Payload:携带用户信息、过期时间等
- Signature:用服务器密钥对前两部分签名,防止内容被篡改
这个设计有几个天然优势:服务器不用存 Session 了,天然支持分布式架构,任意一台服务器只要持有同样的密钥就能校验 token;客户端可以是浏览器、App、小程序,不受 Cookie 机制限制。
但它也有明显的坑:JWT 一旦签发,在过期之前无法主动作废。 用户改了密码、被踢下线、权限被撤销,只要 token 还没过期,它依然有效。要解决这个问题要么把过期时间设得很短再加上刷新机制,要么引入黑名单逻辑——但那样又回到"服务端存储状态"的老路上了。技术选型没有银弹,Session 和 Token 各有取舍,关键看你的业务场景。
5. 缓存是怎么让网页"嗖"一下打开的
5.1 缓存不是只有浏览器本地那一种
很多人对 HTTP 缓存的理解停留在"浏览器把资源存下来了"。其实 HTTP 缓存体系分几个层面:
- 浏览器私有缓存:每个浏览器自己的一份,存的是个人访问过的资源
- 共享代理缓存:企业内网、运营商侧部署的缓存服务器,所有用户共享
- 网关/CDN 缓存:内容分发网络,把资源缓存在离用户更近的边缘节点上
不管是哪一层,它们的核心判断逻辑都遵循同样的规则,靠响应头里的 Cache-Control 和相关字段来协调。
5.2 强缓存与协商缓存:两种缓存策略的本质区别
HTTP 缓存策略分为两大类,理解它们的区别是掌握缓存的关键。
强缓存:浏览器判断本地缓存的资源还没过期,直接使用,不发任何请求到服务器。完全靠 Cache-Control 的 max-age 控制:
http复制Cache-Control: max-age=86400
表示资源在 86400 秒(一天)内有效。这一天内,浏览器不会请求服务器,直接从本地读取,速度极快。代价是:如果服务器上的资源已经更新了,浏览器还在用旧版本。
协商缓存:浏览器发现强缓存过期了(或者资源根本没设强缓存),会带着一个"验证凭证"去问服务器"我这个缓存还能不能用?"服务器比对后有两种回答:
- 资源没变:返回 304 Not Modified,没有响应体,浏览器继续使用本地缓存
- 资源变了:返回 200 OK 并携带新的响应体,浏览器更新缓存
协商缓存的验证凭证有两种:
| 凭证 | 来源 | 服务器如何验证 |
|---|---|---|
Last-Modified / If-Modified-Since |
服务器返回资源最后修改时间,浏览器请求时回传这个时间 | 比对文件的修改时间 |
ETag / If-None-Match |
服务器根据资源内容生成的一个唯一标识(类似指纹),浏览器请求时回传 | 重新计算资源内容的标识,比对是否一致 |
ETag 比 Last-Modified 精确:修改时间只能精确到秒,而且文件的修改时间变了不代表内容变了(比如 touch 一下,时间变了内容没变),ETag 是内容级别的校验。所以现代的缓存策略更依赖 ETag,有些服务器会同时返回两种,让浏览器优先使用 ETag。
5.3 实际项目中的缓存配置思路
关于缓存,我给一个经过验证的配置思路,这个思路在不同项目里我都用过,效果不错:
HTML 页面不缓存或短缓存:
http复制Cache-Control: no-cache
注意 no-cache 不是"不缓存",而是"每次使用前都要去服务器验证一下",对应协商缓存。HTML 是页面入口,必须保证用户拿到最新版本,所以适合用 no-cache。
静态资源(JS、CSS、图片)用长缓存 + 文件名指纹:
http复制Cache-Control: max-age=31536000
静态资源一年内强缓存。那文件更新了怎么办?靠构建工具(如 Webpack、Vite)在文件名后面加哈希指纹:app.8f3a2b.js。文件名变了,浏览器把它当成一个全新资源去请求,旧缓存自然失效。这就是经典的"缓存永不更新,内容更新靠换 URL"策略。
API 响应一般不缓存,但如果接口数据变化不频繁,可以用 ETag 做协商缓存,减少不必要的响应体传输。
注意:调试前端页面时,如果改了代码刷新还是旧的,先看看是不是缓存问题。在开发者工具里勾选 "Disable cache",或者用无痕模式打开,就能排除缓存干扰。这看起来是小事,实际排查时能省很多时间。
6. 深入 HTTP 报文:头部字段是客户端与服务器的"暗号"
6.1 不能不认识的请求头
请求头字段非常多,但日常开发真正高频用到的就那么几个。整理一下:
| 请求头 | 作用 | 注意事项 |
|---|---|---|
Host |
指定目标主机和端口 | HTTP/1.1 必须携带,虚拟主机靠它区分不同网站 |
User-Agent |
告诉服务器客户端类型 | 是什么浏览器、什么系统,有的服务器会据此做适配或拦截 |
Accept |
客户端能接受的内容类型 | 如 text/html、application/json |
Accept-Encoding |
客户端支持的压缩算法 | gzip、br 等,服务器按它决定是否压缩响应体 |
Content-Type |
请求体的媒体类型 | POST/PUT 提交数据时必须设置正确 |
Content-Length |
请求体长度 | 帮助服务器知道请求体是否传输完整 |
Authorization |
携带认证凭证 | 通常格式为 Bearer <token> 或 Basic base64串 |
Cookie |
携带浏览器存储的 Cookie | 自动携带,无需手动设置 |
Referer |
来源页面 URL | 服务器可据此做防盗链或统计分析 |
这里特别想说一下 Content-Type。它是前后端联调时最容易出问题的地方。请求体有三种最常见的格式:
http复制# 1. URL 编码的表单格式(HTML 表单默认)
Content-Type: application/x-www-form-urlencoded
# 请求体: name=Tom&age=25
# 2. JSON 格式(前后端分离项目最常用)
Content-Type: application/json
# 请求体: {"name":"Tom","age":25}
# 3. 文件上传格式
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
后端框架解析请求体的方式完全依赖这个 Content-Type 头。前端明明发的是 JSON 数据,却忘了设置 Content-Type: application/json,后端按表单格式解析,结果拿到的都是 null。这种问题在联调里非常常见,排查的时候第一反应就应该去看请求头里的 Content-Type 对不对。
6.2 响应头里的关键信息
响应头字段同样很多,常见的有:
| 响应头 | 作用 | 特点 |
|---|---|---|
Content-Type |
响应体的媒体类型 | 浏览器靠它决定怎么渲染响应内容 |
Content-Length |
响应体长度 | 传输完毕后浏览器靠它判断是否接收完整 |
Content-Encoding |
响应体压缩方式 | 如 gzip,浏览器会自动解压 |
Set-Cookie |
下发给浏览器的 Cookie | 一个响应头可以出现多次 |
Cache-Control |
缓存策略 | 前面详细讲过 |
Location |
重定向的目标地址 | 配合 301/302 使用 |
Access-Control-Allow-Origin |
跨域资源共享(CORS)的核心字段 | 指定哪些域名可以访问资源 |
Access-Control-Allow-Origin 值得展开讲,因为前端跨域问题几乎天天见。浏览器有个同源策略:默认情况下,在 a.com 页面里发请求访问 b.com 的接口,浏览器会拦截响应。要解除这个限制,服务器必须在响应头里声明:
http复制Access-Control-Allow-Origin: https://a.com
只有带上这个头,浏览器才允许前端代码读取响应数据。如果你在控制台看到 No 'Access-Control-Allow-Origin' header is present 这类报错,去后端通过 CORS 配置加上对应域名即可,前端改代码是解决不了这个问题的。
6.3 HTTP/1.1、HTTP/2 与 HTTP/3:为什么 Head-of-Line Blocking 被反复提起
既然讲到了报文结构,顺便把协议版本的区别也说清楚,因为这是面试高频、实操中也影响性能认知的知识点。
HTTP/1.1 的报文就是我们前面看到的纯文本格式,可读性强,但性能上有内在短板。每个 TCP 连接同一时间只能处理一个请求——前一个请求的响应没回来,后一个请求即使已经发出去了,也只能排队。这就是常说的队头阻塞(Head-of-Line Blocking)。浏览器为了解决这个问题,对同一域名最多开 6 个并发连接(不同浏览器数量不同),但连接数总有上限,页面资源多了还是会排队。
HTTP/2 的核心改进是引入了二进制分帧和多路复用。所有请求和响应都被拆分成一个个二进制帧,在一个 TCP 连接上交错传输,互不阻塞,彻底解决了应用层面的队头阻塞。同时它还支持头部压缩(HPACK)和服务器推送,减少了请求数量和数据量。
HTTP/3 则是把传输层从 TCP 换成了 QUIC,基于 UDP 实现。之所以要换,是因为 TCP 本身也有队头阻塞——一个 TCP 包丢了,后续所有包都要等它重传。HTTP/3 的 QUIC 在 UDP 上实现了类似 TCP 的可靠性机制,但丢包只影响丢的那一路数据,其他数据不受影响。这在高延迟、丢包率高的网络环境(比如移动网络)下优势明显。
现在大多数网站已经全面支持 HTTP/2,HTTP/3 也在快速普及中。你不需要记住所有协议细节,但至少要知道这个演进脉络,遇到"为什么接口响应很快,页面加载却要排队"这类问题时,能有一个判断方向。
7. HTTPS 与安全实践:给 HTTP 加一层"防窃听"的壳
7.1 HTTPS 到底加密了什么
HTTPS 并不是一个新的协议,它是 HTTP + TLS(传输层安全协议) 的组合。TLS 层位于 TCP 和 HTTP 之间,负责对通信内容做加密和完整性校验。
理解 HTTPS,最核心的是搞清楚它用了两套加密机制:
非对称加密:有一对密钥(公钥和私钥),公钥加密的数据只能用私钥解密,私钥加密的数据只能用公钥解密。特点是安全但慢。
对称加密:加密和解密用同一个密钥,速度快但需要先解决"密钥怎么安全地传给对方"的问题。
HTTPS 的握手过程大致是:
- 客户端向服务器发起 TLS 握手请求
- 服务器返回数字证书(里面包含服务器公钥、证书颁发机构信息等)
- 客户端验证证书合法性(确认服务器身份,防止中间人冒充)
- 客户端生成一个随机的对称密钥,用服务器的公钥加密后发给服务器
- 服务器用私钥解密,得到对称密钥
- 之后双方通信都使用对称加密,保证传输效率和安全性
这套设计用非对称加密解决了密钥分发的难题,用对称加密保证了传输性能。
7.2 为什么会出现"证书不受信任"的警告
你在浏览器里偶尔会看到"您的连接不是私密连接"的警告,原因通常是以下几种:
- 证书过期:SSL 证书有有效期(通常 1 年左右),到期后需要续期。没及时续期就会报这个错。
- 证书域名不匹配:证书是为
www.example.com签发的,你访问的是api.example.com,浏览器无法信任。 - 证书不是受信任的 CA 签发的:正式证书必须由浏览器内置信任的证书颁发机构(如 Let's Encrypt、DigiCert)签发。自己用 OpenSSL 生成的证书叫自签名证书,浏览器不认识,会警告。
- 证书链不完整:服务器配置证书时,没有把中间证书一并配置,导致浏览器无法验证完整的信任链。
证书过期排查是我工作中最常见的问题之一。 很多内部系统搭建的时候配好了 HTTPS,一年后开始有人报"访问不了,提示证书过期",运维才想起来续期。现在已经有了 certbot 这类自动续期工具,借助 crontab 定时执行就能免去手动续期,强烈建议配置上,不要让一条定时任务把系统搞宕。
7.3 实际项目中的安全配置要点
关于 HTTP 层的安全,以下几个配置思路值得记住,它们能解决绝大部分常见风险:
- 全站 HTTPS 并做 HTTP 跳转:在服务器上配置把 80 端口的请求 301 到 443,强制走加密通道。
- HSTS 头:告诉浏览器"之后一段时间内,只使用 HTTPS 访问本域名",连跳转这一步都省了,防止降级攻击。配置为
Strict-Transport-Security: max-age=31536000; includeSubDomains。 - 敏感的 Cookie 设置 HttpOnly、Secure、SameSite:前面已经详细说过,这里再强调一遍,这是我评估一个系统安全系数时最先看的地方。
- 不泄露版本信息:服务器响应头里的
Server: nginx/1.24.0、X-Powered-By: PHP/8.2这类信息,等于告诉攻击者你用的软件版本,方便他们检索已知漏洞。这些头尽量隐藏或做伪装。
8. 从开发者工具到抓包:真正学会"看"一个 HTTP 请求
8.1 Chrome DevTools 里每个面板到底在看什么
学会用浏览器开发者工具,是排查网络问题最基本的技能。打开 Chrome 的开发者工具(F12),切到 Network(网络)面板,刷新页面,你会看到一堆请求。这个面板有几个关键区域:
- 请求列表:每一行是一个请求,显示方法、地址、状态码、耗时、大小
- Headers:查看请求头、响应头、通用信息(URL、请求方法、状态码、远端地址)
- Payload / Request:查看请求体内容
- Preview / Response:查看响应体的渲染效果或原始内容
- Timing:查看请求各阶段的耗时明细
排查接口问题时,我通常按这个顺序看:
- 看状态码——判断是 4xx 还是 5xx,锁定大方向
- 看请求头——Content-Type 对不对、Authorization 带没带、Cookie 齐不齐
- 看请求体 / Payload——参数名拼错没有、数据类型对不对
- 看响应——后端返回的具体错误信息是什么
- 看 Timing——如果响应慢,时间花在哪个阶段
把请求 Copy as cURL,是前后端联调时最高效的沟通语言。 在请求上右键,选择 "Copy > Copy as cURL",就能得到一个完整的 curl 命令。把命令发给后端,对方一行命令就能复现你的请求,不用再一遍遍对参数。这是我在实际中特别喜欢用的协作方式。
8.2 抓包工具的选择:Fiddler 还是 Wireshark
浏览器的开发者工具能看到的只是浏览器发出的请求,有两个场景它搞不定:
- 移动端 App 的网络请求
- 非浏览器程序的 HTTP 请求(如桌面软件、命令行工具)
这时候就需要抓包工具。工具选择上我根据自己的使用经验给你一个参考:
| 工具 | 适用场景 | 特点 | 学习成本 |
|---|---|---|---|
| Fiddler / Charles | HTTP/HTTPS 抓包、断点修改 | 图形界面友好,可以拦截修改请求和响应,App 代理抓包常用 | 低 |
| Wireshark | 网络协议全栈分析 | 能看到 TCP、UDP、DNS 等底层包,HTTP 只是其中一层 | 高 |
| Proxyman | macOS 平台 | 界面现代,对移动端调试支持好 | 低 |
实际上,Fiddler 的原理是把自己注册为系统的 HTTP 代理,所有 HTTP 流量都会经过它,它再转发给目标服务器。抓 HTTPS 的时候,它会在本地生成一个根证书,并动态为每个域名签发子证书,这样浏览器或 App 信任了 Fiddler 的根证书后,Fiddler 就能解密看到明文内容。这个方案的便捷性背后是对"中间人"思路的实践——安全领域常说的中间人攻击,原理一模一样。区别只是你是经过授权后的善意使用,而恶意攻击中你并不知道有人在你的流量中间插了一脚。
8.3 一个完整的排查案例:接口突然 502
最后用一个真实案例把整个排查链路串起来。有一次我负责的一个服务突然大量报 502,用户反馈页面打不开,现象是:随便访问一个接口都是 502 Bad Gateway,Nginx 日志显示 upstream 请求超时。
排查步骤:
-
确认 502 是什么层报的:静态资源正常,动态接口全部 502。Nginx 处理静态文件不经过上游,静态正常、动态失败,基本确定问题出在 Nginx 和后端服务之间,或者后端服务本身。
-
就近验证后端服务:在服务器上直接
curl http://127.0.0.1:8080/health,结果长时间无响应,直到超时。说明后端服务确实有问题。 -
检查进程和端口:
ps aux | grep java发现进程还在,但netstat -tlnp | grep 8080显示端口没有监听。进一步确认,进程僵死,Java 服务可能发生了线程阻塞(Full GC 时间过长)或连接池耗尽。 -
看后端日志确认根因:发现数据库连接池全部被占用,单条数据库查询在这个时间点异常缓慢,最终导致连接池排队耗尽。
-
处理与恢复:先重启服务恢复功能,再排查数据库慢查询,定位到一条没有走索引的 SQL,加了索引后问题彻底解决。
这个案例的核心价值在于:502 只是症状,不是根因。 你需要在链路里逐层往下挖——Nginx 有没有问题?后端服务活着吗?端口通吗?日志里报什么?数据库扛得住吗?每一层有自己的检查手段,一层层排除,最终才会找到真凶。很多人在第一步看到 502 就去查代码,但实际问题在数据库,方向错了就会浪费大量时间。
写在最后:两个值得养成的小习惯
HTTP 看似简单,但它是整个互联网世界的通用语言。把它的核心机制吃透,受益的不只是刷题和面试,而是你面对任何网络问题时的那份从容。
最后分享两个我养成的习惯,对加深 HTTP 理解非常有帮助:
一是多用 curl -v 而不是浏览器看请求。 浏览器帮你隐藏了很多细节——自动加请求头、自动处理重定向、自动解压,这些都省心,但也让你对真正在网络上传送的报文无感。curl -v 会把每一行报文原原本本摆在你面前,看习惯之后,你对 HTTP 的理解会有一个质的飞跃。
二是养成看响应头的习惯。 看一个接口,不要只看状态码和返回数据,顺手把响应头过一遍。cache-control 设了什么?Set-Cookie 带什么属性?Content-Type 对不对?Server 暴露了什么?这些信息量远比你想象的大。我招人的时候,会故意问候选人"一个 304 请求之后浏览器到底做了什么",能答清楚的人,对这个协议的理解通常都不会差。
HTTP 的世界没有太多玄学。协议规则是死的,但实际应用是活的。把规则吃透,再去面对那些千奇百怪的现象,你会发现大部分问题都有迹可循——无非是报文写错了、状态没记对、缓存过期了、证书没配好。按照本文提到的链路一层层排查,你会少走很多弯路。
