很多人天天跟 HTTP 打交道接口通了就万事大吉一报错就只会看状态码是不是 200。但真要把 HTTP 协议从头到尾捋一遍问几个为什么能讲清楚的人并不多。这篇文章就是一次完整的 HTTP 总结从协议演进、报文结构、请求方法、状态码、连接机制、缓存再到 HTTPS 和版本升级全部串起来讲一遍。既适合刚接触前后端开发的新手建立整体认知也适合有一定经验但知识点比较零散的老手查漏补缺。读完你会发现很多之前踩过的坑其实协议早就把原因写在明面上了。1. HTTP 协议的演进脉络每一代解决了什么问题1.1 从 0.9 到 1.1互联网初期的协议成形HTTP 协议诞生之初远比现在简单。最早的 0.9 版本只有一个 GET 请求没有请求头没有状态码服务器返回一段 HTML 就断开连接。那个年代的 Web 就是纯文档浏览所以协议只要做到把内容取回来就够了。到了 1.0 版本请求头和状态码出现了Content-Type 也开始支持不同类型的内容传输。但 1.0 有一个明显的毛病每个请求都要新建一次 TCP 连接用完即断。一个网页如果引了 20 张图片浏览器就得建立 20 次 TCP 连接。TCP 的三次握手开销加上慢启动过程直接让页面加载慢得离谱。HTTP/1.1 解决了这个问题核心手段就是默认开启持久连接。同一个 TCP 连接上可以连续发送多个请求不用每次都重新握手。同时 1.1 还加入了 Host 头允许一台服务器用不同域名跑多个网站也就是虚拟主机。还有分块传输编码和缓存控制指令这些基础能力一直沿用到今天。记住一个核心结论HTTP/1.1 是目前兼容性最好、使用范围最广的版本你日常调试的绝大多数请求都是这个版本。1.2 HTTP/2二进制分帧与多路复用带来的变化HTTP/1.1 虽然有了持久连接但它在同一时刻只能串行处理请求。一个连接上发了一个请求必须等响应回来才能发下一个。所以浏览器为了加快加载速度只能同时打开多个 TCP 连接通常一个域名下会开 6 个左右这就是人们常说的浏览器并发连接数限制。HTTP/2 的出现改变了这个局面。它把数据拆成更细的帧并且在同一个 TCP 连接上通过流来并发传输多个请求响应。这就是所谓的二进制分帧和多路复用。简单理解就是一条高速公路上画了多个车道不同请求的车可以并排跑不再需要排队等单车道。除了多路复用HTTP/2 还做了头部压缩。HTTP 请求里的 Header 其实非常重复尤其 Cookie 这类字段动不动就几百字节。HTTP/2 用 HPACK 算法把重复的头部信息压缩掉实测下来头部体积能减少一大半。HTTP/2 的另一个特性是 Server Push服务器可以在客户端请求之前主动推送资源。这个设计想法很好但实际使用中因为缓存命中率难以预测很多项目并没有大规模采用。1.3 HTTP/3绕过 TCP 队头阻塞的尝试HTTP/2 的多路复用解决了 HTTP 层的队头阻塞但底层的 TCP 仍然存在队头阻塞问题。什么是 TCP 队头阻塞简单说就是一个 TCP 连接里传输的包如果丢了后续的包即使已经到了也必须等丢失的包重传成功才能交给上层处理。这在弱网环境下特别明显。HTTP/2 把多个请求都塞进同一条 TCP 连接一旦某个包丢失所有请求都得跟着等反而比 HTTP/1.1 的表现还差。HTTP/3 的思路是彻底甩开 TCP改用基于 UDP 的 QUIC 协议。QUIC 自己实现了可靠传输、拥塞控制、连接迁移等功能丢包时只影响单个流不会拖累其他流。同时它把 TLS 加密直接集成到协议内部握手只需要 1 个往返连接建立更快。不过 HTTP/3 目前主要是在对性能要求极高的场景下使用比如视频会议、直播、弱网环境。日常业务系统如果还没有特殊需要HTTP/2 仍然是性价比最高的选择。2. 报文结构与常见 Header看完就能读懂抓包数据2.1 请求报文与响应报文的完整结构HTTP 报文的结构其实非常固定不管请求还是响应都是起始行 Header 区 空行 消息体。下面用一个实际的请求来拆解。一个典型的 POST 请求长这样POST /api/login HTTP/1.1 Host: example.com Content-Type: application/json Content-Length: 42 User-Agent: Mozilla/5.0 Accept: application/json {username:admin,password:123456}第一行是请求行包含请求方法、路径和协议版本。从第二行开始到空行之前全是请求头。空行之后才是请求体。注意空行是必须的很多初学者做底层 Socket 解析时不处理这个空行最后发现读出来的数据差一行其实问题就出在这里。响应报文的结构类似第一行是状态行内容大概是这样的HTTP/1.1 200 OK Content-Type: application/json Content-Length: 27 Date: Wed, 16 Nov 2024 10:00:00 GMT {code:0,message:ok}状态行包含协议版本、状态码和状态描述。状态码后面那段英文描述其实只是为了让人看懂程序判断时基本只看数字。2.2 需要重点掌握的头部字段Header 里面有很多字段我不打算把整个 RFC 都背一遍只说日常开发中最常用、最容易出问题的几个。Host 字段代表目标域名和端口。如果你在写反向代理或者做虚拟主机这个字段极其重要。多个域名共用一台服务器时后端必须靠 Host 头来区分到底要路由到哪个服务。Content-Type 和 Content-Length 这两个字段是一对。Content-Type 告诉接收方消息体的格式是 JSON 还是表单还是纯文本。Content-Length 告诉接收方消息体到底有多长。接收方读完 Content-Length 指定的字节数就算读完一条消息准备读下一条。这两个字段如果对不上轻则解析失败重则引发请求走私类的安全漏洞。Transfer-Encoding: chunked 和 Content-Length 是互斥的。当服务器流式返回数据、无法提前算出总长度时就会用 chunked 编码。它把数据分成多个块每块开头是十六进制长度块结束后用 0 结尾。理解了这个机制你就知道为什么有些接口能在还没算完的时候就开始返回一部分数据了。还有一类常见的字段是 Accept 系列比如 Accept-Encoding 表示客户端支持哪种压缩格式常见的有 gzip、br。服务器按照这个字段决定是否压缩、用哪种算法压缩。线上抓包时如果你发现响应体是一堆乱码先检查一下是不是把 Accept-Encoding 去掉了浏览器一般会自动带上它而 curl 默认并不带。Cookie 字段也值得一提。HTTP 是无状态协议服务器怎么记住你是谁靠的就是 Cookie。客户端每次请求都会把本地存储的 Cookie 放在 Header 里传给服务器服务器通过会话机制反查用户状态。实际开发中要注意 Cookie 的传输开销它每次请求都会跟着走Cookie 太大对请求性能的影响非常直接。3. 请求方法与幂等性动词背后的语义3.1 常用方法到底该怎么选HTTP 方法有很多但日常开发里真正高频使用的就 GET、POST、PUT、DELETE、PATCH 这几种。很多人选方法全凭习惯用的最多的就是 GET 和 POST剩下的一律不碰。这种用法不能说错但会丢失很多语义上的好处。GET 的语义是获取资源它是幂等的并且不应该有副作用。什么叫幂等同一个请求执行多少次结果都是一样的。GET 请求可以被浏览器缓存可以被收藏到书签。正因为有这些特性GET 请求不应该用来做修改数据这种操作。POST 的语义是提交数据给服务器处理它不幂等同一个 POST 请求发两次可能产生两条订单记录这也是为什么要做防重提交。PUT 的语义是把资源完整地替换为请求体它是幂等的。比如你要更新一整条用户信息用 PUT 比较合适。PATCH 则是对资源做部分修改它不要求幂等。DELETE 自然是删除资源也是幂等的。方法选得好对做接口设计和文档编写都很有帮助。对方看到你用的是 PUT就能猜到这是全量更新看到 PATCH就能知道这是局部修改。语义清晰能减少大量的沟通成本。3.2 幂等性对超时重试的影响幂等性这个话题最容易在接口超时重试时暴露问题。设想一个场景客户端发起支付请求网络抖动导致响应超时客户端立刻重试了一次。如果这个接口用的是 POST两次请求就可能导致两笔支付这是生产事故级别的bug。正确的做法是对这类非幂等操作要么用 POST 但带上全局唯一的请求 ID服务器记录这个 ID重复请求直接返回第一次的结果要么把接口设计成幂等的比如下单前先创建一个订单号拿订单号去操作同一个订单号重复操作不会产生新订单。抓包排查超时问题的时候也别忘了这一层。如果看到客户端自动重发请求服务端还是同样的处理逻辑那问题很可能就出在幂等性上。超时了重试一次这个看似无害的动作在非幂等接口上就是灾难。4. 状态码语义与线上排查一眼定位问题4.1 状态码分类速查状态码是服务器给客户端的最直接反馈。很多开发者只知道 200 表示成功、404 表示找不到但线上问题的排查往往就藏在那些不常见的码里。状态码按百位数字分类1xx 是信息性响应平时基本见不到最典型的是 101 Switching Protocols用于 WebSocket 升级协议。2xx 是成功200 最常规201 表示资源创建成功204 表示响应没有消息体。3xx 是重定向301 永久跳转、302 临时跳转、304 表示命中协商缓存未修改。这里容易混淆的是 301 和 302尤其是做 URL 迁移时该用 301 还是 302 要想清楚用错了会影响搜索引擎收录。4xx 是客户端错误。400 通用参数错误、401 未认证、403 权限不足、404 资源不存在、405 方法不允许、409 冲突。最容易让人头疼的是 401 和 403 的区别401 是说你没有证明你是谁403 是已经证明你是谁了但你没有权限。如果分不清这两个排查权限问题时会走弯路。5xx 是服务端错误。500 通用错误、502 网关收到上游无效响应、503 服务不可用、504 网关超时。5xx 类的错误往往和网关配置、上游服务状态、超时设置有关。4.2 高频状态码的排查案例先拿 502 来说。502 Bad Gateway 意味着网关比如 Nginx已经收到了你的请求但上游应用没有给出有效响应。常见原因有三个上游服务挂了、上游服务超时了、上游服务的响应被网关判定为无效。排查时第一步是看上游应用日志确认它到底有没有收到这个请求。如果收到了但处理慢就需要调大网关的 proxy_read_timeout如果压根没收到就要检查 IP 和端口的连通性。再来看 504 Gateway Timeout。这个状态码是网关等不及上游的回应主动放弃了。排查看两个时间一是网关的超时配置二是上游应用的实际处理时间。如果接口确实需要长耗时比如导出报表要跑 30 秒那要么调大超时时间要么改成异步任务加轮询不要硬扛同步等待。429 Too Many Requests 是限流状态码。服务端通过这个码告诉你请求太频繁了歇会儿再来。配合 Retry-After 头使用客户端就能知道到底要等多久。线上系统如果这个码出现得频繁要么是客户端被恶意刷量要么是限流参数的阈值设得太低。正常业务被误伤时响应里带一个 X-RateLimit-Remaining 之类的头会方便排查。304 Not Modified 经常被误解为报错其实它是在说你之前缓存过这个资源经过协商服务器确认它没有变化你就继续用本地缓存吧。这个状态码对页面性能的影响非常积极它让浏览器避免了重复下载大文件。5. 连接管理与性能优化从 keep-alive 到多路复用5.1 TCP 连接复用与浏览器并发限制HTTP 底层靠 TCP 传输。TCP 连接建立一个来回成本不低三次握手外加上 TLS 的话还要再加几轮。所以 HTTP/1.1 引入的 keep-alive 机制对性能的提升非常可观一个 TCP 连接建立后可以反复发送多个请求。但 keep-alive 不是万能的。HTTP/1.1 的持久连接上请求响应是严格一对一的顺序关系。一个请求没结束后面的请求就得排队。这就是 HTTP 队头阻塞。浏览器为此只能多方下注一个域名同时建立多个 TCP 连接但连接总数又不能无限膨胀于是就有了并发连接数的限制默认一般是 6 个。你打开一个网页浏览器会为这个域名自动建立最多 6 个 TCP 连接来并行加载资源。如果你用浏览器开发者工具网络面板能看到这些连接会发现每个标签页里的请求被分到了不同的 Connection ID 上。这个机制决定了HTTP/1.1 下的页面性能优化一个重要思路就是把资源分散到多个域名绕过连接数限制。这也是早年域名分片技术的由来。5.2 队头阻塞HTTP/1.1 的软肋与 HTTP/2 的解法HTTP/1.1 的队头阻塞在于应用层必须按顺序处理。虽然你可以开多个连接但单个连接上还是得排队。HTTP/2 的多路复用在这个层面解决了问题一个 TCP 连接里面开了多个流流的帧可以交错传输互不影响。这就好比 HTTP/1.1 是一条单车道只能一辆车一辆车过HTTP/2 是在同一条路上画出了多车道每个请求的车可以并行开。代价是实现的复杂度明显上升帧、流、优先级这些概念都是 HTTP/1.1 时代没有的。但 HTTP/2 并没有治愈所有队头阻塞。它只是把 HTTP 层的队头阻塞消除了TCP 层依然有一个问题TCP 段丢失会导致后续数据等待重传。因为 HTTP/2 的所有流都共享一个 TCP 连接一个 TCP 包丢了会影响所有流。这也解释了为什么弱网条件下 HTTP/2 有时候反而比 HTTP/1.1 慢以及 HTTP/3 为什么会改走基于 UDP 的 QUIC。做性能优化时我的建议是如果你的站点还在用 HTTP/1.1而且静态资源很多优先升级到 HTTP/2。大多数时候这是成本最低、见效最明显的一次协议层优化。如果升级以后发现弱网下表现依然一般再考虑 HTTP/3。6. 缓存机制让浏览器帮你少跑几趟6.1 强缓存与协商缓存的分工HTTP 缓存是性能优化里面最值得花时间的一块。理解缓存机制之后很多页面加载慢的问题可以直接在无代码改动的前提下解决掉。缓存分两大类强缓存和协商缓存。强缓存的意思是浏览器直接从本地缓存里取资源不发任何请求给服务器。它靠两个头控制Expires 和 Cache-Control。其中 Cache-Control 是更现代的方案优先级也更高。max-age3600 表示这个资源在 3600 秒内都可以直接用本地缓存。协商缓存的意思是浏览器拿本地缓存的资源去问服务器这个还新鲜吗服务器根据条件判断如果资源没变就返回 304浏览器继续用本地缓存如果变了就返回 200 加新的资源内容。协商缓存的判断依据主要是 ETag 和 Last-Modified。ETag 是资源内容的一个哈希标识Last-Modified 是资源最后修改的时间。注意一个优先级问题Cache-Control 的优先级高于 ExpiresETag 的优先级高于 Last-Modified。也就是说如果同时定义了强缓存时间未过期根本不会走到协商缓存那一步。只有强缓存失效了才会带着 ETag 或 Last-Modified 去服务器做新一轮协商。6.2 缓存策略设计实战参考设计缓存策略的时候核心问题是想清楚哪些资源可以放心缓存缓存多久。日常项目里最常用的一个参考策略是这样的静态资源文件比如图片、CSS、JS 文件如果文件名带版本哈希比如 app.8f3k2a.js可以用 Cache-Control: max-age31536000也就是一年。因为文件名变了就是新资源旧缓存永远不会被错误使用。HTML 页面不做强缓存或者只做很短时间的强缓存比如 max-age60然后配合协商缓存来控制刷新速度。这样发布新版本后用户最多晚 60 秒看到首页更新。API 接口一般不主动设置缓存头。需要缓存的话多用协商缓存避免返回过期的业务数据。实际踩坑最多的场景是这样的静态资源没有带版本号却又设置了很长的缓存时间。这会导致文件更新后用户浏览器还在用旧文件怎么刷新都没用。解决方法是强制刷新CtrlF5或者给资源加个新的查询参数。但这个方法治标不治本最干净的方案还是让文件名随内容变化。另外提一下 Service Worker 层面的缓存。它属于前端的离线缓存机制可以在网络不可用的情况下直接返回缓存。它的优先级比 HTTP 缓存更高因为 Service Worker 在网络请求发出之前就已经拦截了。这种缓存适合离线优先的应用但要注意版本管理和快速更新策略否则会出现用户永远在用老页面的问题。7. HTTPSHTTP 的安全外衣7.1 TLS 握手到底在干什么HTTPS 本质上就是 HTTP over TLS它做的事情是在 TCP 之上加了一层安全协议。TLS 要解决两个问题一是确保通信对象是真的不是冒牌货二是确保传输内容加密路上被人截获也看不懂。TLS 握手的第一步是客户端和服务器协商加密套件。客户端发一个 ClientHello列出自己支持的 TLS 版本和加密算法服务器从中选一个回 ServerHello。接着服务器把自己的证书发给客户端。这一步非常关键证书里面包含了服务器的公钥以及可信机构对证书内容的签名。客户端拿到证书后会用本地保存的根证书去验证签名是否可信。验证通过客户端生成一个对称加密密钥用服务器的公钥加密后发给服务器。服务器用私钥解密双方就获得了同一个会话密钥。之后的数据传输全部用这个会话密钥做对称加密。握手一共需要几次往返老版本 TLS 1.2 需要 2 个 RTTTLS 1.3 优化到了 1 个 RTT。HTTP/3 更进一步直接把 TLS 集成到协议里建连和握手同步完成所以可以做到 0-RTT 或 1-RTT。7.2 证书校验里的几个坑证书是 HTTPS 里面最容易出问题的地方。最常见的坑自签名证书导致客户端不信任。开发环境里大家都图省事自己签发证书但到了真机上或者浏览器里CA 校验不过连接直接失败。解决方法是把自签名证书导入系统的信任列表或者直接给开发环境安装一个本地 CA 工具来签发证书。另一个坑是证书过期。很多线上故障都源于运维忘了定期更新证书用户打开页面直接显示连接不安全。排查这种问题不需要看应用日志直接在服务器上用命令检查证书有效期就行。我习惯把证书续期做成自动化任务配上到期前 30 天的告警杜绝人工遗忘。还有就是证书链不完整的问题。服务器返回的证书链如果缺少中间证书客户端无法完成链式验证。很多用户安装证书时只装了服务器证书没把中间证书一并配置导致一部分客户端能访问、一部分提示不安全。验证证书链可以用在线检测工具也可以本地模拟握手查看返回的完整证书链。还有一个日常容易忽视的点混合内容。页面本身是 HTTPS 的但里面嵌了 HTTP 的图片或脚本浏览器就会阻止这些不安全的内容加载。排查这类问题打开开发者工具的 Console 就能看到明确的警告信息把对应资源改成 HTTPS 地址就好。8. 常见问题排查与工具用法实测几个典型案例8.1 curl 里的高频调试参数排查 HTTP 问题curl 是我第一个打开的工具。效率极高而且能精确控制请求内容。最基础的是 -v 参数它能打印出整个请求和响应过程的细节包括 TLS 握手信息、请求头、响应头、耗时等。调试接口连通性、证书问题时-v 是不可或缺的。-H 用于添加请求头比如curl -H Authorization: Bearer xxx。-d 用于发送请求体默认会带上 POST。如果想看响应头单独加 -i。想模拟请求来自某个域名用 -H Host: example.com。想测试走了 HTTP/2用 --http2。curl 还有一个隐藏能力是可以分段查看耗时。用curl -w输出时间变量可以分别看到域名解析时间、TCP 连接时间、TLS 握手时间、首字节时间、总时间。这个对定位慢在哪个环节很有帮助。比如 DNS 解析时间异常长那就去查本地解析配置TLS 握手时间过长就重点看证书链和加密套件协商。8.2 三个真实场景排查复盘说几个我实际遇到过的案例。第一个案例接口偶发超时HTTP 状态码是 200但响应体为空。查了应用日志发现服务端确实收到了请求处理逻辑也跑了但 Nginx 层返回了空内容。最终定位到是网关和上游之间的 keep-alive 连接被服务端回收网关不知道还在用旧连接发请求导致请求没有到达应用就被丢弃。解决办法是在 Nginx 和服务端之间统一连接空闲超时配置并且适当设置 proxy_http_version 1.1 和 Connection 头。第二个案例页面图片偶尔加载不出来。用浏览器开发者工具看发现加载失败的图片都带 403 状态码。检查后发现是图片所在域名的防盗链配置生效了浏览器发出的 Referer 被服务器拒绝。解决方式是把当前业务域名加进防盗链白名单。这个案例说明403 不一定是权限问题也可能是 Referer 校验问题。第三个案例客户端反反复复报请求头太大。查看 Nginx 日志发现确实有 400 错误。原因很简单某个页面塞了太多 Cookie超过了 Nginx 默认的 large_client_header_buffers 限制。解决办法是调整 Nginx 的这个参数但更根本的方向是减少 Cookie 的传输。把不必要的 Cookie 改成短域名或者直接清理问题就不再复发。8.3 浏览器开发者工具怎么看协议浏览器开发者工具除了看 Response 和 Timeline 之外还有一个很容易被忽略的表格列Protocol。在 Network 面板的表头右键勾选 Protocol就能看到每个请求走的是 h1、h2 还是 h3。这个信息对排查性能问题非常有用。如果你发现某个域名下所有资源走的是 h1说明 HTTP/2 没有生效如果看到有些请求显示为 h3说明已经在走 QUIC 了。配合 Connection ID 列还能看到 HTTP/1.1 下同一个域名建了多少条连接多路复用是否真的在用。开发者工具里另外一个值得多用的是Copy as cURL功能。右键任何一条网络请求可以直接复制成一条 curl 命令。这在复现线上问题的时候特别好用从 DevTools 复制命令到终端加一个 -v 就能完全复现请求过程的细节排查效率和纯猜完全不一样。9. 从 HTTP/1.1 升级到 HTTP/2迁移配置要点9.1 开启 HTTP/2 的前置条件如果没有反代层或者网关层你通常只需要在入口处开启 HTTP/2。以常见的反向代理服务器配置为例开启 HTTP/2 的代码就是listen 443 ssl http2;不同版本写法略有差异但真正决定能不能升的前提是HTTPS 必须已经配置好。HTTP/2 在主流浏览器里都要求走 TLS所以你首先得把 HTTPS 彻底搞定。第二个前置条件证书链要完整。HTTP/2 对证书的要求比 HTTP/1.1 更严格某些实现下证书链不完整会直接导致握手失败。升级之前先把证书链检测一遍把中间证书补全。第三是排查是否有依赖 HTTP/1.1 特性的旧逻辑。比如有些客户端可能手动组装 HTTP 报文如果它不识别 HTTP/2 的帧格式就会出问题。不过正常的浏览器和现代网络库都不存在这个问题主要是排查老旧的 SDK。9.2 打开 HTTP/2 之后的变化开启 HTTP/2 后最直观的感受是大量的静态资源请求不再排队多个资源可以在同一连接上并行传输。拿一个有很多小图标的页面来说HTTP/1.1 下浏览器要开 6 条连接甚至等待排队HTTP/2 下所有请求在一条连接上同时跑加载速度提升非常明显。开启 HTTP/2 之后还需要关注优先级策略。HTTP/2 支持给流设置优先级比如页面主 HTML、首屏 CSS 和 JS 可以设置高优先级懒加载图片设置低优先级。合理设置优先级可以大幅改善首屏渲染速度。但优先级设错了也可能导致资源互相等待反而拖慢速度。新手建议先别调优先级让默认行为跑一段时间通过核心指标来观察实际收益。从 HTTP/1.1 切到 HTTP/2 之后之前为绕过连接数限制做的域名分片反而成了负担。因为 HTTP/2 本来就在一条连接里多路复用多个域名反而降低了连接复用率还增加 DNS 解析成本。所以迁移后一个重要的清理工作就是把分片域名收敛回来能合并成一个域名的就合并成一个。最后提一句 HTTP/3 的开启。目前主流服务器软件都有对应的 QUIC 配置但 HTTP/3 依赖 UDP 端口要注意防火墙和负载均衡是否放行相关流量。客户端浏览器的兼容性也还在演进不建议在没有充分测试的情况下直接全量切换。可以灰度一部分流量观察传输失败率、加载时间等指标稳了再铺开。我个人在实际操作中最大的体会是HTTP 协议不像高深的算法那样需要天赋它更像一套约定俗成的交通规则。规则本身不复杂但用得好不好、排查问题时有没有意识到这里是靠规则决定行为的差距就非常大了。放下抓包工具前多问自己一句这个状态码、这个头、这个超时参数背后的语义是什么很多疑难杂症自己就露出了答案。把这篇总结里的内容全部消化掉再回头看日常开发里的网络问题你会觉得清晰得可怕。
阅读完成 · 觉得有帮助?