1. 为什么期末复习计算机网络,大多数人卡在HTTP这一章
每到期末,计算机专业的学生翻开《计算机网络》教材,前几章物理层、数据链路层还能靠死记硬背撑过去,到了传输层勉强跟得上,等翻到应用层的HTTP协议,很多人就开始犯迷糊了。更常见的情况是:好像每一句话都读得懂——“HTTP是超文本传输协议”“基于请求响应模型”“默认端口80”——但合上书一做题,问“GET和POST到底有什么区别”“HTTP和TCP是什么关系”“为什么说HTTP是无状态的”,又答不上来。
我自己当年复习的时候也踩过同样的坑。后来工作了真正用抓包工具去分析接口、排查线上问题时,才把教材里那些抽象概念和实际现象一一对上号。这篇博文就打算把我认为最重要、最容易被考试和面试反复问到的HTTP协议知识点,按一条能真正理解的逻辑线重新梳理一遍。它适合正在准备计算机网络期末考试的在校生,也适合即将参加校招面试、需要快速捡起网络基础的应届生,同时——如果你是一个写后端接口或做前端联调的开发者,这篇文章同样值得花二十分钟过一遍,因为HTTP不是你背会就能用好的东西,它藏在每一个超时、重试、缓存和状态码背后。
内容上,我会从HTTP到底解决什么问题讲起,然后拆解报文结构、请求方法、状态码、连接管理、缓存机制、Cookie与Session,最后聊几个排查问题时的实战思路。争取做到:每个知识点不只是“是什么”,还讲清楚“为什么这么设计”“实际用的时候会碰到什么”。这样复习完,无论考卷出的是概念题、分析题还是抓包实操题,你都不至于无话可说。
需要提前说明的是,HTTP协议本身是一个持续演进的规范,从HTTP/1.0到HTTP/1.1,再到HTTP/2、HTTP/3,每一代都在解决上一代遗留的问题。考试和面试最常考的仍是HTTP/1.1,所以这篇的主线也以HTTP/1.1为主,HTTP/2和HTTP/3放在最后一节做对比理解。这样既不影响你应付考试,也能让你对现代Web的整体脉络有一个不落伍的认识。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先搞清楚HTTP在互联网里到底站在哪个位置
很多同学复习网络协议时最大的困惑,是分不清TCP/IP、HTTP、FTP这些协议之间的关系。它们不是并列的竞争者,而是处于不同层次、各管一段的协作关系。为了把HTTP的位置一次讲明白,我们得先从整个网络体系结构说起。
2.1 TCP/IP四层模型里的“应用层”到底管什么
教材里常见的OSI七层模型和TCP/IP四层模型,考试喜欢考对应关系,但真正帮你建立直觉的是后者。TCP/IP模型从下往上依次是:网络接口层、网际层(IP层)、传输层(TCP/UDP)、应用层(HTTP、FTP、DNS、SMTP等)。
你打开浏览器输入网址的那一瞬间,HTTP协议作为应用层协议,负责定义“浏览器想访问哪个资源、用什么方式访问、服务器该返回什么内容”这套语义;传输层的TCP负责把这份请求数据可靠地切割、编号、传输、重组,保证数据不丢不重不乱序;网络层的IP负责寻址和路由,告诉每一台路由器这个数据包该往哪个方向转发;网络接口层则负责在具体的物理链路上把比特流发出去。
打个比方:HTTP是写在信封上的内容,TCP是快递公司负责把信封完整稳妥地送到收件人手里的那套流程,IP是快递分拣中心根据地址确定运输路线的逻辑。你写什么内容、用什么措辞——这是HTTP的职责;快递公司怎么保证信封不破、顺序不乱、丢了还能补发——这是TCP的职责;分拣中心看地址选路线——这是IP的职责。三者分工明确,谁也替代不了谁。
所以当有人问“HTTP和TCP是什么关系”时,正确的回答是:HTTP基于TCP协议传输数据,HTTP规定了数据内容的格式和交互语义,TCP保证了数据能可靠地到达对端。HTTP自己不负责丢包重传,那是TCP的事。
2.2 为什么说HTTP是“无连接”的,但底层连接一直在复用
老教材上经常写“HTTP是无连接的”“HTTP是无状态的”,这两句话被无数考生拿来死记,却很少有人深究它们各自的真实含义。
“无连接”在HTTP/1.0时代的含义是:每一次请求-响应都要建立一次TCP连接,请求结束就断开,所以每个连接只服务一次。这个设计简单,但代价巨大——每次建立TCP连接都要经历三次握手,如果页面里有几十张图片,就得建几十次连接,效率和性能都非常差。HTTP/1.1引入了持久连接(Keep-Alive),默认情况下一个TCP连接可以连续发送多个请求、接收多个响应,直到一方主动关闭。所以严格来说,现代HTTP/1.1默认已经不是“无连接”的,而是“具有持久连接能力”的。考试如果还是照搬“无连接”三个字,至少要知道这个历史演变过程,否则你没法解释为什么看Network面板时,多个请求都复用同一个Connection ID。
“无状态”则是指HTTP协议自身不记忆任何请求之间的关系。服务器收到每个请求,都当作一个全新的独立请求来处理,它不知道这个请求是否来自刚才那个用户。正是因为没有“记忆”,服务器实现起来可以非常简单,但也因此带来了一个麻烦——怎么识别同一个用户?于是才有了后面的Cookie和Session机制。这一块我后面会用专门一节展开。
2.3 一份HTTP报文从浏览器到服务器要闯几道关
理解了层次关系之后,我们再看一次完整的数据流动过程,把各个协议的职责串成一条线。假设你在浏览器地址栏输入 http://www.example.com/index.html 并回车:
- 浏览器先解析URL,得到协议名(http)、主机名(www.example.com)、端口(默认80)、路径(/index.html)。
- 浏览器调用DNS解析,把主机名转换成IP地址。DNS本身也是一个应用层协议,默认走UDP的53端口。
- 拿到IP后,浏览器与服务器建立TCP连接,三次握手完成后,连接进入可用状态。
- 浏览器按照HTTP协议规则,构造一个请求报文(请求行、请求头、空行、请求体),通过TCP连接发送给服务器。
- 数据在传输过程中,TCP会进行分段、确认、重传、排序;IP层负责在多个路由器之间逐跳转发。
- 服务器收到请求后解析报文,执行对应的业务逻辑(读数据库、调用其他服务等),然后构造HTTP响应报文返回。
- 浏览器收到响应后解析响应体,渲染页面;如果页面中引用了CSS、JS、图片等额外资源,浏览器会再次发起HTTP请求获取。
这个过程中每一层都只和相邻层打交道,下层为上层提供服务,上层不必关心下层的具体实现细节。这种分层设计是计算机网络最核心的思想,考试的时候经常要求你描述“数据封装与解封装过程”——从上往下每层加上自己的头部,从下往上每层剥掉对应的头部,这个流程一定要能画出来、说清楚。HTTP报文就是最“顶层”的那部分数据,它封装在TCP段里,TCP段封装在IP数据报里,IP数据报封装在数据链路层的帧里。
3. 手撕HTTP报文结构:请求行、状态行、头部字段一次讲透
了解了HTTP的位置之后,我们正式进入协议本身。报文是HTTP协议交流的载体,所有交互语义都通过报文表达。一个HTTP报文分为两大类:请求报文(客户端到服务器)和响应报文(服务器到客户端)。它们的结构非常相似,都由三部分组成:起始行、头部字段、消息主体(可以没有)。
3.1 一个GET请求的报文逐行拆解
我们先看一个最典型的GET请求报文:
http复制GET /index.html?page=2 HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
第一行是请求行,由三部分组成:请求方法(GET)、请求URL(/index.html?page=2)、协议版本(HTTP/1.1)。注意这里请求URL通常写的是相对路径而不是完整URL,因为完整URL中的协议和主机名已经在Host头部里给出了。这是HTTP/1.1的强制要求——HTTP/1.1规定请求报文中必须包含Host头部,因为一台服务器可能同时托管多个域名(虚拟主机),服务器要靠Host字段来区分请求的是哪个站点。
从第二行开始是请求头部,每一行都是一个“字段名: 字段值”的结构,冒号后有空格。常用的头部字段这里解释几个:
- Connection: keep-alive 表示希望保持TCP连接,便于后续复用。
- User-Agent 告诉服务器客户端是什么软件、什么版本,服务器可以据此返回适配内容。注意测试接口时某些反爬策略会检查这个字段。
- Accept 声明客户端接受哪些媒体类型,q=0.9表示优先级权重,数值越高越优先。
- Accept-Encoding 表示客户端支持的压缩格式,如果浏览器没发这个字段,服务器一般就不做压缩,直接返回原始内容。
- Accept-Language 表示客户端偏好的语言。
头部字段结束后必须有一个空行,用来说明“头部结束,后面是正文”。对于GET请求,通常没有消息体,所以空行之后就没有内容了。这个空行非常重要,解析协议时靠它分隔头部和主体。
3.2 POST请求的报文多了一个“身体”
POST请求和GET请求在报文上的主要区别在于,POST通常携带请求体,用于提交数据。一个典型的POST请求报文长这样:
http复制POST /api/login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Connection: keep-alive
username=alice&password=123456
注意这里新增了两个头部:Content-Type 表示请求体的媒体类型,Content-Length 表示请求体的字节长度。服务器必须根据这两个字段来正确解析请求体。表单一类常用 application/x-www-form-urlencoded,JSON数据常用 application/json,文件上传常用 multipart/form-data,字符需要区分,这些在接口测试和抓包分析时都会反复用到。
Content-Length 容易踩坑的地方是:它必须严格等于请求体字节数,如果多写了服务器会认为请求体没发完,一直等待;少写了又可能读取到下一个请求的数据。遇到这种问题,抓包对比最直接。现代HTTP/2中这种边界问题被帧机制解决了,以后你也可以留意。
3.3 响应报文里的状态行和头部字段
服务器返回的响应报文,起始行叫状态行,结构是:协议版本、状态码、原因短语。例如:
http复制HTTP/1.1 200 OK
Date: Mon, 15 May 2023 08:12:00 GMT
Server: nginx/1.20.1
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Cache-Control: max-age=3600
Last-Modified: Mon, 15 May 2023 07:00:00 GMT
<html>...
状态行中的 200 是状态码,OK 是原因短语,原因短语是给人看的,程序主要依据状态码判断结果。后面跟的头部字段里,Server 表示服务器软件型号,Content-Type 带charset表示字符编码,Cache-Control 和 Last-Modified 是缓存相关字段,后面讲缓存时会展开。
空行之后就是响应体,也就是实际返回的网页内容、图片二进制数据或者JSON文本等。
3.4 头部字段大小写敏感吗?控制台看报文时容易忽略的细节
HTTP头部字段名是大小写不敏感的,也就是说 Content-Type 和 content-type 是同一个字段。但字段值通常大小写敏感,具体值语义由各字段定义决定,比如URL、Cookie的值等。在用代码写服务端解析逻辑时,不要做大小写敏感的字符串匹配,统一转成小写再比较最稳妥。
另外观察浏览器开发者工具Network面板时,你看到的“Request Headers”是浏览器最终发送的详情,里面会额外加很多自动字段,比如Sec-Fetch-*、Origin、Referer等,这些是现代浏览器出于安全考虑自动附加的,不必害怕,它们也遵循HTTP头部的基本语法。
4. HTTP请求方法:GET、POST之外,那些容易被忽略的存在
请求方法定义了客户端想让服务器对资源做什么操作。教材和面试中最高频的题目是GET和POST的区别,但协议里其实定义了8种方法,每一种都有明确的语义。
4.1 GET、POST、PUT、DELETE的语义边界
- 语义上,GET是请求读取指定资源,应该是安全且幂等的。所谓安全,是指GET方法不应对服务器上的资源产生副作用——服务端不该因为一次GET请求就去修改数据或触发扣款逻辑。所谓幂等,是指对同一URL执行多次GET,结果应该相同,不会因请求次数改变资源状态。
- POST是向指定资源提交数据,请求服务器处理请求体中的数据,常用于创建新资源或提交表单。POST不是安全的,也不是幂等的——同一份数据POST两次,很可能创建两条记录。
- PUT是向指定资源上传最新内容,完整替换资源。PUT是幂等的,因为不管请求多少次,最终资源状态都是你上传的那份内容。
- DELETE是删除指定资源,也是幂等的。
考试常考“哪些方法是安全的、哪些是幂等的”,记法如下:
| 方法 | 安全性(不修改服务器状态) | 幂等性(多次执行结果一致) |
|---|---|---|
| GET | 是 | 是 |
| HEAD | 是 | 是 |
| OPTIONS | 是 | 是 |
| PUT | 否 | 是 |
| DELETE | 否 | 是 |
| POST | 否 | 否 |
另外还有一个经常被忽略的HEAD方法,它和GET的唯一区别是响应不包含消息体,只返回状态行和头部。HEAD常用于探活、检查资源是否存在、获取资源的元数据,比如判断一个URL是否能访问而不必下载整个文件。
4.2 GET和POST那点“说不清道不明”的区别
网上关于GET和POST的区别,流传最广的说法是“GET参数放在URL里,POST参数放在请求体里”。这句话对,但不全对。严格来说,HTTP规范并没有规定GET不能带请求体,也没有规定POST必须带请求体,实际实现中GET偶尔也会带请求体,但多数服务器和代理会忽略或丢弃,所以最佳实践仍然是不要这么干。
更本质的区别是语义层面的:GET是安全的、幂等的,适合做查询操作;POST不是安全的、不是幂等的,适合做数据变更操作。基于这个语义,浏览器、代理服务器、爬虫程序会有不同的行为:GET请求可以被缓存、被收藏、被历史记录保存,还能被预取;POST请求一般不会被缓存,刷新页面时浏览器会提示是否重新提交,后退时也有类似处理。
另一个常见区别是传输量的限制。很多人说“GET长度限制为2048”,其实HTTP协议本身没有这个限制,限制来自浏览器和服务器的URL最大长度。不同浏览器和服务器上限不同,但为了兼容性和可维护性,URL里不要塞大数据。POST请求体的大小同样没有协议硬限制,由服务器配置决定,比如Nginx的client_max_body_size、Tomcat的maxPostSize。实际开发中,上传文件、提交富文本内容都不要用GET,原因一是语义不对,二是URL长度容易超限,三是有安全性问题(参数暴露在地址栏、会被存入日志)。
4.3 OPTIONS方法为什么在跨域请求里那么常见
OPTIONS方法用于请求服务器告知该URL支持哪些请求方法,常用于预检(preflight)场景。前端做跨域请求时,如果请求方法不是简单请求(例如带了自定义头部、使用了PUT/DELETE,或Content-Type不是三种简单类型之一),浏览器会先发一个OPTIONS请求问服务器“允许我跨域访问吗?”服务器通过Access-Control-Allow-*响应头部表明允许的源、方法、头部等。这个OPTIONS请求是不带业务数据的,只做“探测”。
抓包时看到OPTIONS请求,是前端跨域联调的正常现象。如果你在后端接口联调中遇到“OPTIONS请求返回404”,通常就是服务器没有处理预检路由,或框架默认拦截了。解决方式是在后端的路由层统一处理OPTIONS请求,返回204和正确的CORS响应头。
4.4 实际开发中自定义方法可行吗
HTTP规范支持扩展方法,但实际工程中极少有人自定义方法,因为主流的浏览器、代理、网关、WAF都不一定认识你发明的方法,可能导致请求被拒绝或转发异常。绝大部分场景用标准方法就足够了。如果觉得语义不匹配,比如想实现“修改”操作却不知道该用PUT还是POST,按这个原则选:完整替换用PUT,部分更新用POST或PATCH;先保证幂等性和安全性的预期,再考虑贴合RESTful风格。
5. 状态码:不要只记住200,也没必要背全42个
HTTP状态码有三位数,首位数字定义类别,后两位细节。绝大多数人熟悉200和404,但在实际排查问题时,你会遇到很多“看着眼熟也说不上准确含义”的状态码。这里按类别梳理一遍,把高频的、考试常考的都标注清楚。
5.1 五大类状态码的划分逻辑
- 1xx:信息性状态码。表示请求已接收,继续处理。最常用的是101 Switching Protocols,用于WebSocket升级。
- 2xx:成功。200 OK是标准成功响应;201 Created表示资源创建成功;204 No Content表示成功但响应体为空;206 Partial Content是断点续传和分块下载时返回,响应体只包含部分内容。
- 3xx:重定向。301 Moved Permanently永久重定向,搜索引擎会把权重移交给新地址;302 Found临时重定向;303 See Other表示应改用GET请求重定向;304 Not Modified配合缓存使用,表示资源未修改,可继续使用本地缓存;307/308分别对应临时/永久重定向但保持请求方法和请求体不变。
- 4xx:客户端错误。400 Bad Request语法错误;401 Unauthorized未认证;403 Forbidden禁止访问(虽然已认证但无权);404 Not Found资源不存在;405 Method Not Allowed方法不被允许;408 Request Timeout请求超时;409 Conflict资源冲突;413 Payload Too Large请求体过大;429 Too Many Requests请求频率超出限制。
- 5xx:服务端错误。500 Internal Server Error通用服务器错误;501 Not Implemented服务器不支持此功能;502 Bad Gateway网关或代理收到上游无效响应;503 Service Unavailable服务暂时不可用;504 Gateway Timeout网关超时。
5.2 301和302有什么区别?为什么老是分不清
301和302都是重定向,区别来自“永久”和“临时”两个词。301表示资源永久性地迁移到了新地址,客户端(尤其是浏览器和搜索引擎)应当缓存这个跳转结果,以后直接访问新地址;302表示这次临时跳转到别处,下次还应该访问原地址。
但这里有一个历史坑:HTTP/1.0时期定义了302,规范建议浏览器在重定向时把POST改写为GET,但很多浏览器是这么实现的;后来为了避免语义混乱,HTTP/1.1新增了303(明确使用GET)和307(保持原方法与请求体),以解决“重定向时方法丢失”的问题。所以实际上:
- 301/302 在多数浏览器的实现中,处理POST请求时都可能把方法改为GET。
- 如果要做严格的“POST请求重定向后仍保持POST”,用307或308。
开发接口时要特别注意:如果后端返回302,并且前端用的是POST请求,很多情况下你会在Network面板看到方法变成了GET,然后丢失了请求体,导致数据提交失败。解决方式是明确指定使用303或307语义,或把重定向逻辑改为前端自行判断。
5.3 407、502、504这类“看着眼熟但说不清”的状态码
407 Proxy Authentication Required,和401类似,但它是针对代理服务器认证失败。如果你的网络访问需要经过代理,而代理要求用户名密码,就会看到407。
502 Bad Gateway 意味着你访问的服务器(如Nginx)作为网关,向上游服务器(如后端Java服务)转发请求时,上游返回了无响应或非法响应。常见原因包括:后端服务崩溃、后端端口配置错误、防火墙拦截、后端响应超时。排查时先看后端服务日志,再检查Nginx的upstream配置。
504 Gateway Timeout 则是网关等待上游响应超时。它和408不同:408是客户端到服务器超时,504是服务器到上游服务的超时。后端处理时间过长、服务线程池被打满、数据库慢查询导致接口迟迟不返回,都会触发504。
5.4 排查状态码的固定思路:先看类别,再查详情
我给你一个实战总结的排查顺序:拿到非2xx状态码,首先确认它是4xx还是5xx;4xx说明问题主要出在请求方(参数错误、未认证、权限不足、资源不存在),先检查自己的请求报文;5xx说明问题出在服务端(代码异常、服务过载、依赖组件故障),需要去看服务端日志和监控。
如果遇到很生僻的状态码,不要猜。标准状态码列表是公开的,RFC 9110(替代旧RFC 7231)里有完整定义,许多知名网站也维护了详细的解释列表。在实际定位问题时,配合抓包工具查看响应正文往往比看状态码更有用——很多框架返回的错误信息里已经写明了具体原因。
6. HTTP的连接管理:从短连接到持久连接,再到管线化为什么失败
连接管理是理解HTTP性能的关键,也是考试喜欢出分析题的地方。连接管理主要讨论的是TCP连接如何在多次请求之间复用。
6.1 每次请求都新建TCP连接到底有多亏
HTTP/1.0默认短连接:每次请求都要先三次握手,传数据,然后四次挥手断开。三次握手需要1个RTT(往返时间),第四次挥手又需要额外的时间,如果每个资源都要来一遍,页面的加载时间会急剧上升。假设一个网页有10个静态资源,光建立和断开连接的开销就会占掉大量时间。所以HTTP/1.0后期已经有了非标准的Connection: keep-alive扩展,HTTP/1.1将其正式纳入默认行为。
6.2 Keep-Alive:一个连接服务多个请求,但默认不是无限长
HTTP/1.1默认启用持久连接,叫Keep-Alive。一个TCP连接建立后,可以连续发送多个请求-响应,直到客户端或服务器主动关闭。这种机制减少了重复握手的开销,也降低了延迟。
但持久连接不是永久连接。服务器会设置一个空闲超时时间,比如Nginx默认的keepalive_timeout通常是65秒或75秒,超过这个时间没有新请求,服务器就会关闭连接。超时时间的设置很微妙:太长会占用大量系统资源连接数,太短又无法发挥复用优势。实践中很多团队把Web服务器的keep-alive timeout设置在30秒到60秒之间,同时配合连接数的监控来调整。
浏览器对同一域名下的并发连接数量也有限制。HTTP/1.1时期,Chrome对同一域名的并发连接数限制通常是6个左右,所以如果一个页面有几十个资源,这些资源会排队分配在有限个连接上发送。这就是为什么图片多的网站加载慢——不是网速不行,而是连接复用和并发请求受限。
6.3 HTTP Pipielining为什么“出道即失败”
为了在持久连接上进一步提速,HTTP/1.1提出了管线化(Pipelining):客户端可以不用等第一个响应返回,就连续发送多个请求。理想情况下,多个请求可以“打包”发送,服务器按顺序依次返回响应。
但这套方案有两个致命问题:一是服务器必须严格按照请求顺序返回响应,如果第一个请求处理得很慢,后面所有请求的响应都得排队等着,造成队头阻塞(Head-of-Line Blocking);二是现实中很多代理服务器和中间设备对管线化的实现有bug,处理不好乱序响应。结果就是浏览器厂商普遍关闭了管线化支持,这个功能基本没落地。
队头阻塞从本质上说是HTTP/1.1的痼疾:同一个连接上的请求必须串行处理,前面的请求慢,后面的请求即使已经到达服务器也只能等待。后来的HTTP/2用“多路复用”解决了这个问题,把请求拆分成多个帧,可以在一个连接上并行交错传输,互不阻塞。
6.4 与连接管理相关的常见TCP细节:三次握手和四次挥手
考试几乎必考三次握手和四次挥手,这里提炼关键点。三次握手的目的不仅是“建立连接”,更是让双方确认彼此的收发能力正常,并交换初始序号。握手过程:
- 客户端发送SYN=1、seq=x的报文段,进入SYN_SENT状态。
- 服务器收到后,回复SYN=1、ACK=1、seq=y、ack=x+1,进入SYN_RCVD状态。
- 客户端收到后,回复ACK=1、seq=x+1、ack=y+1,进入ESTABLISHED;服务器收到后也进入ESTABLISHED。
为什么必须是三次而不是两次?因为如果只有两次,服务器无法确认客户端是否收到了自己的SYN+ACK;如果客户端没收到,它不会发送后续数据,而服务器却认为连接已建立,白白占用资源。三次握手相当于双方各发一次探测,确认对方“能收能发”。
四次挥手的过程是:
- 主动关闭方发送FIN报文,表示“我没有数据要发了”。
- 被动关闭方回复ACK,随后可以继续发送剩余数据。
- 被动关闭方数据发完后,发送FIN报文。
- 主动关闭方回复ACK,连接释放。
很多人不理解为什么挥手动辄四次而握手只有三次,原因是TCP是全双工的,两个方向的关闭需要独立完成。主动方关闭发送方向后,被动方的发送方向仍然可以传送数据,所以ACK和FIN必须分开,不能像握手时SYN和ACK可以合并发送。
在实际抓包中,你还会看到TIME_WAIT状态。主动关闭方在发送完最后一个ACK后,不会立即进入CLOSED,而是先进入TIME_WAIT,等待2MSL(报文最大生存时间)后才关闭。这个机制是为了保证最后一个ACK如果丢失,对方能重发FIN,主动方可以再发ACK;同时防止旧连接的延迟报文干扰新连接。高并发的短连接服务端经常出现大量TIME_WAIT,这是正常现象,但如果数量过多导致端口耗尽,就可能出现连接建立失败的问题,解决办法是开启复用或调整系统参数,不过这是系统调优的范畴了。
7. HTTP缓存机制:304和200(from cache)背后的设计逻辑
缓存是HTTP协议里最实用、也最容易被忽略的模块。面试时问“HTTP缓存是如何工作的”非常高频,实际开发中,静态资源的缓存策略直接影响页面加载速度。要理解缓存,抓住两个维度:一是客户端怎么知道自己能不能用缓存,二是服务器怎么告诉客户端资源过期了。
7.1 强缓存:说“直接用,不用问”的缓存
强缓存是指客户端本地有缓存副本,并且在有效期内直接使用,无需向服务器发起请求。浏览器Network面板中那个“200 (from disk cache)”或“200 (from memory cache)”就是强缓存命中的标志。
实现强缓存有两种头部方式:
- Expires:HTTP/1.0时代使用,是一个绝对时间,比如 Expires: Wed, 21 Oct 2023 07:28:00 GMT。它的问题是依赖客户端本地时间,如果用户改了系统时间,缓存判断就乱了。
- Cache-Control:HTTP/1.1引入,用相对时间描述,最常用的是 max-age=3600,表示从响应到达时刻起3600秒内有效。Cache-Control优先于Expires。
Cache-Control的常用取值还有:no-cache表示“使用缓存前必须先验证资源是否有效”,注意它并不代表“不缓存”;no-store表示“完全禁止缓存”;public表示任何缓存器都可以缓存;private表示只能被浏览器缓存,代理/共享缓存不得缓存。
7.2 协商缓存:说“你去问问服务器,变了没有”的缓存
当强缓存过期,或者显式使用no-cache时,客户端需要和服务器协商一次,确认资源是否仍然有效。协商缓存利用两对字段:
- Last-Modified 和 If-Modified-Since。服务器响应时返回Last-Modified表示最后修改时间;客户端下次请求时带上If-Modified-Since,服务器比较这个时间和资源的实际修改时间,如果一致,返回304 Not Modified,告诉客户端“你的缓存还能用”,客户端用原来的缓存副本;如果不一致,返回200和新资源。
- ETag 和 If-None-Match。ETag是资源的唯一标识(通常是文件内容哈希或版本号),客户端发送If-None-Match携带上次拿到的ETag,服务器对比当前资源的ETag,一致则304,不一致则200。
ETag比Last-Modified更准确:因为Last-Modified只能精确到秒,如果同一秒内内容被修改了两次,就判断不了;而ETag基于内容生成,只要内容变了,ETag就会变。实践中通常两者都返回,协商时优先用If-None-Match。
7.3 304和200的取舍:为什么协商缓存还是要发一次HTTP请求
强缓存命中时不发请求,速度快但可能拿到过期数据;协商缓存命中时虽然只用304响应,但已经发生了一次网络请求。二者各有场景:对于文件名带哈希的静态资源(如 app.8f3d2a.js),文件名变了就代表内容变了,这类资源适合用强缓存并配置较长的max-age,因为文件名变了URL就变了,旧URL的缓存永远不会被命中;对于内容经常更新但URL不变的资源,适合用no-cache配合协商缓存,保证数据不过期。
实际配置参考:静态资源CDN分发时,常用的策略是Cache-Control: max-age=31536000, immutable,同时文件名用内容哈希;HTML页面使用Cache-Control: no-cache,确保每次请求都回源验证。
7.4 抓包实操:怎么判断一个资源到底走了强缓存还是协商缓存
打开浏览器Network面板,点击一个资源,看Name那一列的Size和Time:
- 如果状态码显示200,但Size列显示“from disk cache”或“from memory cache”,说明命中了强缓存,绿色或者灰色标识,表示没有经过网络传输。
- 如果状态码是304,说明走了协商缓存,浏览器发起了请求,服务器确认资源未变,返回304,请求时间通常很短,Size列显示几十到几百字节。
- 如果状态码是200,同时Size列显示资源实际大小,说明资源重新从服务器完整下载了。
如果你要做缓存失效测试,在开发者工具里勾选“Disable cache”并打开DevTools网络面板,刷新页面时所有请求都会强制走网络,方便验证新版本是否生效。在线上的话,可以通过修改资源文件名、加版本号参数,或者刷新CDN缓存来强制更新。
8. Cookie与Session:HTTP无状态问题的最经典解法
HTTP协议是无状态的,但现代网站几乎都需要记住“你是谁”。为了弥合这个矛盾,工程师们发明了Cookie和Session这对搭档。理解它们的区别,是期末复习和高频面试题的双重考点。
8.1 Cookie是怎么被种下、携带和回收的
Cookie是一小段文本信息,由服务器通过响应头Set-Cookie字段发送给浏览器,浏览器存储起来,之后在每次请求该域名时,自动通过请求头Cookie字段原样携带回服务器。
过程大概是:
- 用户第一次访问某网站,服务器在处理完请求后,通过Set-Cookie: sessionid=abc123; Path=/; HttpOnly 返回给浏览器。
- 浏览器保存这段Cookie。
- 用户再次请求该网站时,浏览器自动在请求头加上 Cookie: sessionid=abc123。
- 服务器根据Cookie中携带的sessionid,去Session存储中找到对应的会话数据,从而识别用户。
服务器可以同时设置多个Cookie字段,每个字段有若干属性。常见的属性有:
- Domain:指定Cookie属于哪个域名,浏览器只向匹配域名的请求发送。
- Path:指定哪些路径需要携带Cookie。
- Expires/Max-Age:过期时间,不带则Session Cookie,浏览器关闭即失效。
- HttpOnly:设置后JavaScript无法通过document.cookie读取该Cookie,能降低XSS窃取风险。
- Secure:只在HTTPS连接中传输。
- SameSite:限制跨站请求是否携带Cookie,常见值Lax、Strict、None,用于防御CSRF。
8.2 Session是存哪儿的?服务端内存、Redis,还是数据库
Session的核心理念是“数据保存在服务器端,客户端只持有一个会话标识”。这个标识就是Session ID,通常存放在Cookie中。服务器收到请求后,拿Session ID去存储中查找对应的数据。
Session存储有几种常见方案:
- 服务端内存:最简单的实现,进程重启即丢失,不适合多实例部署。
- Redis:最适合生产环境的方案。Redis读写快、支持过期时间,天然适合Session这种“临时数据”。Spring Boot中常用spring-session-data-redis来统一管理Session。
- 数据库:适合有强持久化需求的场景,但高频读写会对数据库造成压力,通常还要配合缓存使用。
这种“服务端Session + 客户端Cookie”的模型有一个问题:当网站部署了多个后端服务器实例(集群环境),用户在服务器A登录了,下一个请求落到了服务器B,而B上没存这个Session,就识别不了用户。解决方式有几种:一是Session粘滞(sticky session),让同一用户的请求始终分配到同一台服务器;二是把Session集中存储到Redis等公共组件中;三是干脆不用Session,改用无状态Token。
8.3 Token无状态方案和Session方案的本质区别
Token方案,尤其是JWT(JSON Web Token),是现在接口开发中的主流。JWT把用户身份信息、过期时间、签名等打包成一个字符串,服务器不保存会话状态,每次请求时验签和解码即可。好处是天然支持水平扩展,不用共享Session存储;坏处是有状态数据无法主动失效,只能等过期。
简单对比:
| 维度 | Session + Cookie | Token(JWT) |
|---|---|---|
| 存储位置 | 会话数据在服务端 | 数据在Token字符串中 |
| 扩展性 | 需要共享存储或粘滞会话 | 无状态,天然易扩展 |
| 主动失效 | 容易 | 困难,需额外黑名单机制 |
| 安全风险 | 依赖Cookie安全属性 | Token可能被截获,需配合有效期和刷新机制 |
| 适用场景 | 传统Web应用 | 前后端分离、移动端API |
实际项目中,两种方案可以混合使用。并不是非得用JWT替代Session。安全实践上,无论是Session还是Token,都推荐使用HTTPS,并且给Cookie设置HttpOnly、Secure、SameSite等属性。敏感信息的读写要经过服务端校验,不能完全依赖客户端传来的标识。
8.4 三个和Cookie/Token有关的经典坑
-
跨域请求不携带Cookie。如果你的前端页面在a.example.com,后端接口在api.example.com,浏览器默认跨域请求不会带上Cookie。需要前端请求设置credentials: 'include',后端响应头设置Access-Control-Allow-Credentials: true,并且Access-Control-Allow-Origin不能是*,必须是具体的源地址。
-
Token放哪里决定安全边界。有人习惯把Token放在localStorage里,优点是简单,缺点是任何能执行脚本的地方(比如XSS漏洞)都能通过JS拿到它。如果放在HttpOnly的Cookie中,JavaScript拿不到,但需要处理CSRF防护。两边都有取舍,建议是:面向浏览器的Web应用,Token短期有效配合HttpOnly Cookie存储,或者使用标准的“刷新令牌”模式,做双重保障。
-
分布式Session共享配置缺失。如果改了服务器配置不应用统一Session,用户就会遇到“刚登录就退出”的诡异问题。排查这类问题的第一步,就是确认每个请求的JSESSIONID是否一致,以及Session存储是否挂了。
9. 从HTTP/1.1到HTTP/2、HTTP/3:每一代解决什么痛点
期末复习到应用层时,教材往往只写HTTP/1.1,但现在的面试官越来越喜欢追问“HTTP/2有哪些改进”“HTTP/3和HTTP/2有什么区别”。这一节不追求穷举规范,只梳理最关键的三代演进逻辑。
9.1 HTTP/2最核心的改变:多路复用、二进制分帧、头部压缩
HTTP/2没有改变HTTP的语义,也就是方法、状态码、头部字段这些概念都还在,但传输方式完全变了。原来的HTTP/1.1是文本格式,一行一行的;HTTP/2把报文拆成一个个二进制帧,在一个TCP连接上交错发送,接收方根据帧头部的流标识(Stream ID)重新组装出各个请求的报文。
这个机制带来的直接好处是:多个请求可以同时在一个连接上传输,互不阻塞,解决了HTTP/1.1的队头阻塞问题。同时,HTTP/2对头部做了HPACK压缩,大量的重复字段比如Cookie、User-Agent不再每次原样传输,显著减少了传输字节数。
HTTP/2还引入了服务器推送(Server Push),允许服务器在浏览器还没请求某些资源时就提前推送给客户端,省去浏览器解析HTML、发现资源、再发请求的往返时间。不过这个功能在实际应用中使用较少,容易造成带宽浪费,CDN厂商也大多关闭了它。
9.2 HTTP/3为什么改用UDP
HTTP/2虽然解决了HTTP层的队头阻塞,但TCP层的队头阻塞依然存在。TCP是一个按序交付的协议,如果一个数据包在传输中丢失了,接收方必须等它重传后,才能把后续已收到的数据交给上层。即使HTTP/2的多个请求是并行交错发送的,只要底层TCP丢了一个包,所有请求都会受到影响。
HTTP/3的解决思路是直接换掉传输层,不再用TCP,而是用基于UDP的QUIC协议。QUIC在用户空间实现了可靠传输、拥塞控制、加密、多路复用和连接迁移等能力,把握手时间从TCP+TLS的至少1-2个RTT压缩到0-RTT或1-RTT,并在连接迁移(比如WiFi切流量)时不需要重新握手。
HTTP/3现在已经被Chrome、Edge、Safari等主流浏览器默认支持,但普及率还没到完全替代HTTP/2的程度。考试或系统设计面试中,能说出“HTTP/3解决TCP队头阻塞,用QUIC承载HTTP语义”这个核心点就够了。
9.3 做Web开发时需要关心这些版本吗
如果你是Web开发者,日常关注的重点不必太深,但有几个实际操作点值得知道:
- 使用HTTPS时,如果服务器和浏览器都支持,现代浏览器会自动优先使用HTTP/2(h2),你不需要改业务代码。
- Nginx开启HTTP/2很简单,在listen指令后面加 http2 即可,但要确保使用了HTTPS。
- 检查一个网站是否使用HTTP/2,打开DevTools的Network面板,右键协议列,看显示“h2”还是“http/1.1”。很多图片服务器如果没开HTTP/2,资源加载速度会受影响。
- HTTP/3目前提供对等的改进,但需要用户态协议栈和CDN支持,一般开发环境中不用特意配置。
10. 期末复习和面试中的高频考点清单
把前面所有内容压缩成一份“考前一页纸”,方便你最后冲刺时快速浏览。这是多年大量习题和高频面试题浓缩后的重点,按下述顺序自测,能答上70%,期末考基本不用担心。
- 协议分层:HTTP属于应用层,TCP属于传输层,IP属于网络层,各自职责边界清晰。
- 报文结构:请求报文由请求行、请求头、空行、请求体组成;响应报文由状态行、响应头、空行、响应体组成。
- 请求方法:GET安全幂等,POST不安全不幂等,PUT/DELETE幂等;GET和POST区别从语义、缓存、数据位置、安全性四方面答。
- 状态码分类:2xx成功、3xx重定向、4xx客户端错误、5xx服务端错误;重点记301/302/304/403/404/500/502/503/504。
- 连接管理:三次握手、四次挥手、TIME_WAIT的作用;HTTP/1.1默认Keep-Alive;管线化失败原因;HTTP/2多路复用解决队头阻塞。
- 缓存机制:强缓存(Cache-Control/Expires)与协商缓存(Last-Modified/ETag);304的含义;静态资源哈希命名配合长缓存。
- Cookie与Session:Cookie是浏览器存储、请求时自动携带;Session是服务端存储,靠Session ID关联;Cookie属性HttpOnly/Secure/SameSite;Token无状态方案对比。
- 安全问题:HTTPS加密传输、HTTP明文风险;XSS与HttpOnly关系;CSRF与SameSite关系;跨域与Cookie携带条件。
- 版本演进:HTTP/1.1的问题、HTTP/2的解决思路、HTTP/3的QUIC核心思想。
建议你复习时,不要只看总结,回到报文示例上逐行理解。考试应用题经常给你一段抓包截图,让你分析请求方法、状态码、缓存命中情况,把报文结构练熟了,这类题反而是送分题。面试如果被问到“输入URL后发生了什么”,你按2.3节的路径从DNS解析、TCP握手、HTTP请求、服务器处理、响应返回、浏览器渲染逐步作答,逻辑清晰,面试官通常都会满意。如果再能补充一句“如果使用了HTTPS,还会先进行TLS握手协商加密参数”,就更完整了。
11. 从考试到实战:用抓包工具重新认识HTTP
纸上谈兵终觉浅。如果你手边有电脑,强烈建议现在就打开开发者工具,随便访问一个网站,按下面步骤操作一遍。这一步做完,你对HTTP的理解会从一个抽象概念变成看得见摸得着的东西。
11.1 用浏览器开发者工具看一次完整请求
按F12打开DevTools,切到Network标签,刷新页面,你会看到一列请求记录。点击任何一个请求,右侧面板会显示:
- Headers:请求头、响应头。
- Payload:请求参数(POST表单或JSON)。
- Preview/Response:响应内容。
- Timing:请求各阶段耗时(DNS查找、TCP握手、TLS握手、内容下载等)。
试着找一条html请求,观察响应头里的Cache-Control和Last-Modified,再找一条静态图片请求,看它是否命中了强缓存或协商缓存。这比背十遍“HTTP缓存机制”都更直观。
11.2 用curl模拟请求和响应
命令行下,curl是最轻量、最常用的HTTP测试工具。几个高频用法:
bash复制# 只查看响应头
curl -I https://www.example.com
# 查看完整请求和响应过程,-v 会打印详细
curl -v https://www.example.com
# 指定请求方法并携带数据
curl -X POST https://api.example.com/login -H "Content-Type: application/json" -d '{"username":"alice","password":"123"}'
# 跟随重定向,-L 会自动跳转
curl -L http://example.com
# 携带Cookie请求
curl -H "Cookie: sessionid=abc123" https://api.example.com/profile
用curl -v你会看到TCP三次握手的细节,还有TLS握手过程。这是把课本和现实对应起来最好的工具,没有之一。
11.3 使用Wireshark进行协议分析
如果你想要更底层的观察,Wireshark是大学实验课常用的工具。抓包时选择正确的网卡,过滤规则用http或tcp.port == 80,就能看到完整的HTTP报文和TCP报文交互过程。Wireshark适合做局域网实验和协议分析,平时开发排查问题用浏览器DevTools和curl基本就够用,不必把Wireshark当作日常主力。
11.4 实战中我常用的HTTP排查三板斧
分享一套我个人在接口联调和线上问题排查中反复使用的流程,希望对你处理实际问题有帮助:
- 先看浏览器Network面板,确认请求是否发出、URL是否正确、响应状态码是多少。大量“为什么我请求报错”的问题,80%在这一步就已经暴露了——要么URL拼错了,要么方法不对,要么参数格式不对。
- 看请求头和响应头。重点看Content-Type是否匹配,Cookie是否带上,有没有CORS跨域错误,Cache-Control是否导致缓存了旧数据。
- 看响应体和后端日志。如果响应不是预期数据,抓响应体里的error信息,然后在后端日志或全局异常处理中搜索堆栈。如果后端没有日志,那就是接口没被调用到,问题大概率出在网关或路由层。
这套方法的要诀是“从外到内逐层缩小范围”,避免直接从代码层开始猜测。HTTP协议是这套诊断方法的骨架,你越熟悉协议细节,排查起来就越快。
12. 最后分享一个我自己复习时常忽略的细节
我在实际工作中踩过这样一个坑:服务端返回了 Cache-Control: no-cache,我一直以为“no-cache就是不缓存”,结果发现每次请求还是命中缓存了。后来查清楚,no-cache的真实语义是“可以缓存,但每次使用前必须向服务器验证是否有效”,它允许缓存存储响应,只是使用前必须走协商缓存流程。如果你真的不想让任何缓存存储,应该用no-store。这个误解网上一抓一大把,我自己当初也翻了半天文档才彻底弄明白。
这也从侧面说明,HTTP协议看似只有几章,但它里面的每个字段、每个状态码背后都有一套精确的语义。考试复习时不要满足于“知道大概”,要抠到字段级细节——比如Cache-Control和Expires并存时谁优先,Connection: keep-alive是HTTP/1.1默认行为还是可选项,ETag和Last-Modified如何配合使用。这样抠完,考试答细节题时你会非常从容,面试官也会觉得你是真懂,而不是背概念。
按照惯例,最后再说一点学习建议:HTTP协议不要孤立地学,一定要结合TCP来看。很多“为什么HTTP要这么做”的答案都藏在TCP的特性和限制里。比如因为TCP按序交付,所以HTTP/1.1才有队头阻塞;因为TCP握手开销大,所以HTTP/1.1才要Keep-Alive;因为TCP是有状态的传输层,所以无状态的HTTP才需要额外的Cookie/Session方案来记录身份。把这些因果关系串起来,计算机网络对你来说就不再是一堆散落的名词解释,而是一个逻辑自洽的整体。到那时候,无论考卷还是面试题,都难不倒你了。
