
直接进入正题。HTTP这东西搞技术的人天天挂在嘴边但你要是随便拉一个人问“HTTP到底是什么”十有八九得到的回答是“就是网址前面那个http://嘛”。再追问“那它和TCP有什么关系”“为什么说HTTP是无状态的”“状态码301和302到底啥区别”基本就开始含糊了。这篇就一次把这些事讲清楚。不端着讲一堆理论术语也不刻意简化成幼儿园版本而是用一个在日常开发里实际会用到的方式把HTTP从里到外拆一遍。看完之后你再去看抓包工具里的那些请求、状态码、Header、缓存策略不会再有“每个字都认识但连起来看不懂”的感觉。这篇文的读者我默认是刚入行一到三年的开发者、转行做前端的同学以及写了几年接口但没系统捋过HTTP的后端朋友。已经能把TCP三次握手和HTTP报文结构背得滚瓜烂熟的可以绕道了。1. HTTP不是什么讲HTTP是什么之前先得说清楚HTTP不是什么。因为很多人的混乱恰恰是从“它是交通工具”还是“它是交通规则”这个类比没搞明白开始的。1.1 HTTP是协议不是传输通道你可以把HTTP理解成一份“双方约定好的沟通格式”就像两个人写信时约定好开头写称呼中间写正文结尾写落款每行不能超过多少字信纸用什么颜色。至于这封信是走路送、骑马送还是开车送HTTP不管。这个“送信的方式”在网络上对应的是TCP/IP那一层的事。HTTP是应用层协议它不关心数据包从北京到上海走了哪条光纤、经过了几个路由器、有没有丢包重传。它只关心“请求长什么样”“响应长什么样”“什么时候算结束”。所以网上经常有人说“HTTP基于TCP”这句话准确说应该是HTTP通常跑在TCP之上但它自己并不等于TCP。一个HTTP请求发出去先是TCP帮你把数据可靠地送过去中途丢了包会自动重传到了对方手里顺序不对会帮你排好序。HTTP站在TCP的肩膀上只需要管“这段内容是啥格式”就行不用操心传输可靠性。这也解释了为什么你能用一句话跟同事解释清楚“HTTP是应用层协议TCP是传输层协议IP是网络层协议。”三层各管一段互不越界。1.2 HTTP和HTTPS的关系很多人以为HTTPS是“另一个协议”其实不是。HTTPS就是在HTTP外面套了一层加密外壳这个外壳叫TLS早期叫SSL。换句话说HTTP明文跑在TCP上数据包中途被截获了里面的内容直接暴露比如你登录时提交的密码抓包工具里直接能看到。而HTTPS是先通过TLS握手协商出一个对称加密密钥之后所有HTTP报文都用这个密钥加密后再交给TCP传输就算被人截获看到的也是一堆密文。所以你在浏览器地址栏看到https://的时候可以理解为信封被胶带封死了只有发件人和收件人有拆封工具。而http://的信封谁捡到谁就能看。这里补一个开发中常见的误解HTTPS证书不是给“数据”加密的它是用来“证明你是你”的。TLS握手时服务器出示证书客户端校验这个证书是谁签发的、域名是否匹配、是否过期。确认没问题之后才进入密钥协商阶段。数据加密用的是协商出来的会话密钥不是证书本身。这个概念搞清楚了后面看TLS相关的报错会顺手很多。2. HTTP的核心机制请求-响应、无状态、无连接HTTP能做的事情总结起来其实就一句话客户端发请求服务器回响应。但这个模型背后有几个关键特性每一个都直接影响你写代码和排查问题的方式。2.1 请求-响应模型HTTP永远是客户端主动发起请求服务器被动响应。服务器不会主动给客户端推消息。你打开一个网页浏览器先发一个GET请求要HTML拿到之后解析发现里面还有CSS、JS、图片的地址于是再发一堆GET请求分别去要这些资源。整个过程都是浏览器在主动“问”服务器在被动“答”。这套模型今天看来有点原始但它的好处是简单、可靠、易于扩展。任何语言、任何平台只要按同一个格式发请求、解析响应就能互相通信。这也是HTTP能统治互联网这么多年的根本原因。至于现在大家熟悉的WebSocket它跟HTTP不是一回事。WebSocket通过HTTP Upgrade机制进行协议切换之后变成全双工的长连接服务器可以主动推送消息。但那是另一个协议了不属于HTTP的范畴。2.2 无状态到底是什么状态“HTTP是无状态的”这句话网上的解释大多让人越看越晕。其实它说的是服务器不会记住你上一次请求干了什么。两次请求之间服务器认为它们毫无关系。举个例子。你往服务器发了三次请求第一次问“11等于几”第二次问“22等于几”第三次问“刚才那个问题的答案是什么”。服务器会直接懵掉因为它压根不记得你问过什么。这就带来一个实际问题用户登录之后下一次请求服务器怎么知道“这个人已经登录过了”解决方案就是引入Session和Cookie。用户登录成功后服务器在内存里或数据库里存一份会话数据生成一个会话IDSession ID。服务器通过响应头里的Set-Cookie把这个会话ID发给浏览器。浏览器收到后存起来之后的每一次请求自动把这个Cookie带上。服务器看到这个会话ID再查一下对应的会话数据就知道“这个用户是谁、是否已登录”。所以你在写后端接口时判断“用户是否登录”的逻辑本质上就是“请求里带的Session ID能不能在我这边查到有效会话”。这里有一个实际踩过的坑后端把Session存在内存里和前端联调的时候一切正常但部署到服务器上之后用着用着突然掉登录多刷新几次又好。后来查出来是两个原因叠加一是服务器开了多个实例用户第一次请求落在A实例第二次落在B实例A实例的Session数据B实例看不到二是服务重启之后内存里的Session全没了。所以生产环境里Session要么存Redis要么直接用JWT这类无状态认证方案不能指望进程内的内存来存。2.3 无连接与Keep-Alive早期的HTTP协议设计时考虑的是“每个请求建立一个TCP连接请求结束后断开”。好处是服务器不用维护一堆空闲连接省资源。坏处也很明显打开一个网页要加载几十个资源每个资源都要经历TCP三次握手四次挥手光握手开销就能拖慢一半以上速度。所以后来HTTP/1.1引入了Connection: keep-alive机制允许多个请求复用同一个TCP连接。这个机制一直沿用到现在你打开Chrome的开发者工具在Network面板里能看到一个叫“Connection ID”的字段同一次页面加载的多个请求往往共用同一个TCP连接就是这个机制在起作用。注意这里反复出现“连接”这个词容易和“无状态”混淆。无状态说的是服务器不记忆请求之间的关系Keep-Alive说的是一条TCP连接上可以连续发多个请求。两个维度的事不冲突。2.4 HTTP/1.1的队头阻塞问题Keep-Alive虽然省了握手开销但HTTP/1.1时代还有一个臭名昭著的问题队头阻塞。在HTTP/1.1里同一个TCP连接上的请求是串行处理的。也就是说请求A没响应完请求B就算已经发出去了也不能被处理。如果A是一个特别慢的接口后面的所有请求都得排队等着。浏览器为了缓解这个问题会对同一个域名并发开6个左右的TCP连接具体数量因浏览器而异但这只是拆东墙补西墙治标不治本。这个问题真正的解法要到HTTP/2才出现后面单独讲。3. HTTP报文拆开看HTTP报文说穿了就是“一堆文本按一定格式排列”。3.1 请求报文长什么样一个典型的GET请求报文长这样GET /api/users?page1size20 HTTP/1.1 Host: api.example.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Accept: application/json Authorization: Bearer xxxxxx第一行叫请求行由三部分组成方法、URI、协议版本。上面这个例子方法是GETURI是/api/users?page1size20协议版本是HTTP/1.1。从第二行开始到空行之间全是请求头Header。每一行都是“键: 值”的格式。Host字段是必须的它告诉服务器你要访问哪个域名因为同一台服务器可能同时部署着好几个网站靠Host来区分。User-Agent标识客户端类型服务器可以用它做统计也可以用它做一些粗略的浏览器兼容判断。Accept告诉服务器客户端希望收到什么格式的响应Accept: application/json就表示“给我JSON别给HTML”。空行之后是请求体Body。GET请求一般没有BodyPOST、PUT这类的请求才会有。POST请求的报文会多出一段BodyPOST /api/login HTTP/1.1 Host: api.example.com Content-Type: application/json Content-Length: 34 {username:test,password:123456}注意这里的Content-Type它描述了Body的格式。如果是JSON服务器需要按JSON解析如果是表单格式application/x-www-form-urlencoded服务器就要按表单解析。Content-Type对不上服务器解析就会出错这是一个很常见的联调问题来源。3.2 响应报文长什么样响应报文的结构和请求报文对应HTTP/1.1 200 OK Date: Mon, 02 Oct 2023 12:00:00 GMT Content-Type: application/json; charsetutf-8 Content-Length: 56 {code:0,message:success,data:{name:test}}第一行是状态行由协议版本、状态码、状态描述组成。200 OK是“正常”的意思这个伴随了互联网几十年的短语可能比很多流行文化作品的寿命还长。Header部分和请求报文的Header结构一样。Body里放的就是实际返回的数据。3.3 必须能认全的HTTP方法HTTP协议定义了一批请求方法日常开发中最常用的有这么几个方法作用是否携带Body幂等性GET获取资源否幂等POST创建资源或触发操作是不幂等PUT整体替换资源是幂等PATCH部分更新资源是不幂等DELETE删除资源通常无幂等HEAD只获取响应头不取Body否幂等OPTIONS询问服务器支持的跨域方法否幂等幂等的含义是同一个请求执行一次和执行一百次结果一样。GET请求你发多少遍服务器的数据都不变。DELETE也是删一次和删十次最终状态都是“已删除”。但POST不一样发一次创建一条数据发两次创建两条所以不幂等。这个特性在做接口设计时很重要。比如支付回调场景如果回调通知因为网络原因被重复发送你的处理接口必须是幂等的否则用户付一笔钱你这边给他创建了两笔订单。处理方式通常是在业务层做去重比如用订单号做唯一约束重复请求直接返回已有结果。3.4 状态码速查与实战记忆状态码是响应报文里最重要的信息。别去死记硬背按大类记就够了。1xx协议层中间状态日常开发基本碰不到。2xx成功。200 Success、201 Created创建成功、204 No Content成功但无内容返回。3xx重定向。301永久重定向、302临时重定向、304 Not Modified命中缓存服务端没有返回新内容。4xx客户端错误。400 Bad Request参数不对、401 Unauthorized未认证没登录、403 Forbidden已认证但无权限、404 Not Found路径不对、429 Too Many Requests请求太频繁被限流。5xx服务端错误。500 Internal Server Error代码炸了、502 Bad Gateway网关收到上游无效响应、503 Service Unavailable服务暂时不可用、504 Gateway Timeout网关超时。两个容易混淆的点重点提一下。401和403的区别401是“你是谁我不知道”403是“我知道你是谁但你不许进来”。没登录访问个人中心应该返回401登录了但只有普通用户权限去访问管理员接口应该返回403。301和302的区别301是永久重定向比如网站从HTTP迁移到HTTPS搜索引擎看到301会把权重转移到新地址302是临时重定向比如未登录用户访问需要登录的页面临时跳到登录页。平时做接口对接重定向到新地址一般用302。如果误用301浏览器会缓存这个跳转之后直连新地址调试的时候改起来很麻烦。3.5 Header里那批高频重要字段Header字段数量很多但真正高频用到的就那么几个值得花点时间理清楚。Content-Type描述Body的类型前面提过。Content-LengthBody字节长度。服务器靠它判断Body有没有接收完整。Transfer-Encoding: chunked分块传输。响应数据是动态生成的长度事先不确定就用分块的方式一段一段传每个块前面标着长度最后一个长度为0的块表示结束。Location配合3xx状态码使用告诉客户端应该跳转到哪里。Set-Cookie服务器端设置Cookie。Cookie客户端请求时携带的Cookie值。Authorization身份凭证。常见的格式是Authorization: Bearer 。Cache-Control缓存控制指令后面专门讲。CORS相关Access-Control-Allow-Origin允许哪个源跨域访问、Access-Control-Allow-Methods允许哪些方法、Access-Control-Allow-Headers允许哪些请求头。前后端联调时跨域报错九成以上和这几个头有关。4. HTTP缓存让浏览器少干活缓存是整个HTTP体系里性价比最高的一块。把一个响应缓存好了既省服务器的带宽也缩短用户等待时间。但缓存配置坑非常多配错了要么拿不到新数据要么服务器白白背流量。4.1 强缓存 vs 协商缓存HTTP缓存分两大类强缓存和协商缓存。强缓存的意思是浏览器判断缓存还没过期直接本地取用一个请求都不发给服务器。标志性响应头是Cache-Control: max-age3600表示缓存有效时间为3600秒。在这个时间内浏览器根本不会去问服务器“内容变没变”直接用旧缓存。协商缓存的意思是浏览器带着缓存的标识去问服务器“我这个缓存还能用吗”服务器说“还能用”就返回304状态码浏览器继续用本地缓存服务器说“过期了”就返回200和新数据。这两者的区别一句话就能讲明白强缓存是“我自己说了算不去问”协商缓存是“我拿不出主意去问问”。4.2 Cache-Control的核心参数Cache-Control是缓存配置里最重要的头参数非常多挑几个高频的说。max-age秒缓存有效期。比如max-age86400就是一天。s-maxage秒针对CDN等共享缓存的有效期。设置了s-maxage时浏览器本地缓存仍按max-ageCDN缓存按s-maxage。public允许所有缓存都存一份包括CDN。private只允许浏览器本地缓存CDN不许存。no-store任何缓存都不许存每次都回源拿最新数据。类型极其敏感的数据比如账户余额用这个。no-cache这个名字很容易误解它不是“不缓存”而是“用之前必须去服务器确认一遍”作用等同于强制进入协商缓存流程。4.3 缓存配置的实际案例静态资源的缓存策略一般是这样的HTML文件一般设置Cache-Control: no-cache保证每次访问都能回源确认避免用户拿到过期的HTML。JS/CSS/图片这类带版本号的静态文件可以大胆设置Cache-Control: max-age31536000, immutable一年不更新。因为文件名里已经带了hash或者版本号内容一变文件名就变浏览器请求的是新地址完全不用担心缓存不更新的问题。这就是为什么打包工具产出的文件名长这样app.a1b2c3.js。不带版本号的接口响应基本不设置强缓存靠协商缓存来决定是否复用避免接口数据都一个月了客户端还拿的是缓存。我见过一个真实案例某前端同事在index.html的响应上设置了Cache-Control: max-age86400结果发布新版本后用户浏览器里存的是昨天的HTML里面引用的还是旧JS地址白等了好几天缓存过期页面才变新。所以在单页应用里index.html一定不能用强缓存。用no-cache或者干脆no-store都行代价只是一次304协商换来的是用户永远能拿到最新壳子。4.4 ETag与Last-Modified协商缓存需要两个响应头Last-Modified服务器返回资源最后修改时间。浏览器下次请求时带上If-Modified-Since时间相同就用缓存。ETag服务器返回资源的唯一标识通常是对内容做hash。浏览器下次请求时带上If-None-Match服务器对比后一致就返回304。ETag比Last-Modified更精确。因为Last-Modified的时间精度是秒1秒内改了两次内容就识别不出来。而ETag是对内容本身做的摘要内容变了一定能发现。所以实际项目中ETag是主力Last-Modified作为兜底。5. HTTP/2和HTTP/3到底改了什么东西HTTP/1.1挣扎了快二十年HTTP/2在2015年正式发布HTTP/3在2022年成为RFC标准。这两个大版本的更新核心都是奔着HTTP/1.1的两个老大难问题去的队头阻塞和连接建立的成本。5.1 HTTP/2的核心多路复用与二进制帧HTTP/2把原来的“文本格式报文”改成了“二进制分帧”。这不是为了花哨而是为了让多个请求可以在同一条TCP连接上并行处理。HTTP/2里一条TCP连接被划分成多个流Stream每个流里跑一个请求-响应。“分帧”的意思是把数据拆成一个个小帧每个帧上标着“我属于哪个流”。接收方收到后按流的编号重新组装数据。这样一来多个请求可以同时发送、同时接收、互不等待了HTTP/1.1的队头阻塞从根本上被解决。比如一个页面要10个资源HTTP/1.1可能要开6条连接排队HTTP/2只需要1条TCP连接10个请求同时在线上跑。HTTP/2还有一个重要特性服务端推送。服务器在返回HTML后可以主动把浏览器接下来大概率要用的CSS、JS推送给浏览器省去浏览器的再次请求。不过这个特性在实际项目中用得不算多因为有缓存和带宽的权衡问题。另外一个容易被忽略的点是头部压缩。HTTP/1.1的Header每次都要完整传一遍HTTP/2用HPACK算法做压缩会缓存上下文重复字段用很短的表索引代替省下了不少带宽。5.2 HTTP/2的遗留问题TCP队头阻塞HTTP/2解决的是HTTP层的队头阻塞但TCP本身的队头阻塞还在。TCP是可靠传输要求数据按顺序到达。如果传输过程中丢了一个包接收方会把后续已经到的包先存着不处理等丢掉的包重传回来再一起交给上层。就算HTTP/2已经把一个页面拆成了100个流只要有1个帧丢了整条TCP连接上的所有流都得卡住等重传。就好比一条多车道的高速公路上发生了一起事故所有车道的车都得停下来等事故处理完。HTTP/2把多个请求复用到了这一条路上事故一来全部受影响。5.3 HTTP/3放弃TCP拥抱UDP解决TCP队头阻塞最彻底的办法是不把HTTP建立在TCP上。HTTP/3就是这么干的。它建立在QUIC协议上而QUIC是跑在UDP之上的。UDP本身不保证可靠传输QUIC在UDP之上自己实现了可靠传输还顺便解决了队头阻塞每个流是独立传输的流之间不互相影响一个流丢包只重传那一个流不会卡住其他流。此外QUIC还把TLS握手和传输握手合并了如果之前访问过这个服务器可以做到0-RTT建立连接即第一个请求发出去的同时就能带上数据延迟大幅降低。不过HTTP/3在项目落地时仍有现实限制。UDP在某些网络环境里会被防火墙限制部分老旧设备和中间件对UDP的支持不完善。所以现在主流的情况是HTTP/1.1、HTTP/2、HTTP/3三套并存服务端配置了全套实际使用哪个版本要看客户端支持情况和网络环境。6. 抓包与排查HTTP问题实战理论讲再多最终都要落到排查问题的能力上。这里分享一些我在实际工作中高频使用的排查思路和工具。6.1 三个顺手工具Chrome DevTools的Network面板日常联调最常用。可以看请求的完整Header、Body、状态码、耗时、缓存命中情况。点击某个请求在Headers里能看到完整的请求头和响应头在Timing里能看到请求各阶段的耗时。curl命令行里调试接口最快的方式。最常用的几个参数-X指定方法、-H加请求头、-d带Body、-i展示响应头、-v输出全过程信息。一个实际的curl调试例子curl -i -X POST http://api.example.com/v1/login \ -H Content-Type: application/json \ -d {username:test,password:123456}加了-i之后能看到完整的响应头包括Set-Cookie这对排查登录问题很重要。tcpdump Wireshark碰到“Chrome里能打开curl就是不行”这种深层差异问题时直接用tcpdump在服务器上抓包把抓包文件拖回本地用Wireshark分析。6.2 高频问题排查速查表现象可能原因排查方向访问页面提示401未登录或Token过期检查请求头是否带了Authorization访问页面提示403已登录但无权限检查用户角色权限配置页面一直转圈Network里显示pending后端接口挂起没返回抓后端日志看接口是否卡在数据库或外部调用上请求返回200但页面不对可能是304复用缓存看响应头里的Cache-Control明明改了后端代码前端拿到的还是旧数据强缓存没配好检查响应头Cache-Control关掉强缓存再试跨域报错CORS头没配对检查Access-Control-Allow-Origin偶发502网关转发失败看后端服务是否正常负载均衡配置是否出问题偶发504网关等待超时看后端接口耗时是否超过网关的超时阈值Postman请求正常浏览器请求报错Cookie/Origin等浏览器附加信息不同对比两种场景下的完整请求头6.3 一个真实的排查案例有一次线上反馈“文件预览功能异常”打开开发者工具一看文件请求返回的是200但Preview里显示的是一堆乱码。合并响应头发现Content-Type是text/html而正常应该是application/pdf。原因很快定位服务器不认这个文件的扩展名把请求转到了一个默认处理逻辑返回了一个HTML错误页状态码恰好也是200。浏览器拿到Content-Type: text/html就把HTML当成文本解析了页面自然就是乱码。修复方法很简单在服务器上为这个文件类型加MIME映射然后再验证一遍Content-Type正确返回application/pdf。这个案例给我最大的教训是不要只看状态码。200 OK不代表结果OK还要看Content-Type、Body内容是否符合预期。尤其是做接口联调时“请求成功”和“功能成功”是两码事。6.4 每次排查的固定套路我在碰到HTTP相关问题时基本固定走这四个步骤先看Network面板确认请求到底有没有发出去URL对不对请求头完整不完整。再看状态码确定问题落在大方向上的哪一类重定向、客户端错误、服务端错误。再看响应内容没有内容就去看后端日志有内容就按内容分析。最后看缓存确认是不是旧缓存导致的表现不一致。这套流程看起来简单但真的能把大部分问题的定位时间压缩到几分钟以内。怕的是跳过前两步直接去后端翻日志查了半天发现前端压根没把请求发出来。写在最后的两个小建议HTTP学起来不像框架那样有陡峭的学习曲线它更像一个工具箱里的基础扳手随时会用到但平时不太会想到专门研究它。正因如此它值得你花一个下午系统捋一遍——之后就再也不用靠零散记忆来拼凑概念了。我个人在实际操作中有一个习惯每遇到一个看不懂的请求头或状态码会当场按一下F12看真实请求再顺手记到自己的笔记里。日积月累这些笔记比任何教程都管用因为它们全是从真实流量里长出来的。最后再分享一个小技巧排查问题时优先看响应头的Content-Type和Cache-Control这两个字段看明白了很多“莫名其妙”的问题会瞬间变得有理有据。