上一节讲的是一问一答的规矩,本节站到服务器那一侧:它接到请求后要走四步(收下请求 → 找到内容 → 装好包装 → 发出响应),而"装好包装"这一步最讲究——它决定了响应里的那几个搬运字段:Content-Type(是什么)、Content-Length(多长)、Content-Encoding(压没压)、Content-Range(这是第几段)、ETag(版本标记)。其中最能体现设计巧思的是最后两个:分段传输让下载可以断点续传、视频可以拖进度;版本标记让同一张图第二次访问时,服务器只回一句 304 就完事——正文一个字节都不重传。
4.5 从浏览器这一侧看完了整场对话:请求报文三段、响应报文三段、状态码五族、"无状态"与 Cookie。
本节换个位置——站在服务器这一侧,看它怎么把"一个文件"装进响应、怎么搬过去。上节结尾留下的两个尾巴(Content-Type 凭什么是权威、304 到底怎么发生的)都在本节结清。
一、服务器接到请求后的四步
(配图一)服务器不是"接到请求就把文件扔出去",中间有几步必做的功课:
① 收下请求——看三样东西:方法(GET 要东西 / POST 递东西)、路径(要哪个资源)、查询串(附带参数)。这三样正是 4.4 里"URL 六段中真正发出来的那部分"。
② 找到内容——两条完全不同的路(收 4.1 的静态与动态):
| 类型 | 服务器做什么 | 举例 |
|---|---|---|
| 静态内容 | 按路径去磁盘上读那个文件 | logo.png、style.css |
| 动态内容 | 把参数交给程序,等它算出内容 | 搜索结果页、登录后的首页 |
找不到就是 404(4.5 说过:404 是"东西不在",不是"网站挂了")。
③ 装好包装——本节的重点(下两节细讲):定类型、定长度、要不要压缩、要不要只给一段、附上版本标记。
④ 发出响应——三部分照旧:状态行 + 头部 + 空行 + 正文(收 4.5)。
"找到内容"是业务问题,"装好包装"是协议问题。
前者取决于网站怎么搭的(静态还是动态),后者每个服务器都要守同一套 HTTP 规矩——这也是为什么 nginx、Apache、IIS 换着用,浏览器都能正常显示。
二、"装好包装"的五个字段
(配图一下半)这几个头是服务器这次搬运的说明书:
1. Content-Type——这是什么内容
格式是 类型/子类型(这套叫法源自 MIME):
Content-Type: text/html; charset=UTF-8
Content-Type: image/png
Content-Type: application/json
Content-Type: application/octet-stream ← 二进制流,说不清是什么就用它
两个要点:
- 它是权威,不是后缀。4.5 说过"浏览器看
Content-Type不听后缀";从服务器这侧看就是:它查一张"后缀 → 类型"的对照表(业界叫mime.types),把.html翻译成text/html。表里没有的后缀,就只能给application/octet-stream。 charset也在这里。4.2 讲的<meta charset="UTF-8">是备用方案——HTTP 头上的charset优先级更高。所以"页面中文乱码"的两大嫌疑犯:响应头说 UTF-8、文件实际存的却是 GBK(或反过来)。
2. Content-Length——正文多少字节
告诉对方"收够这么多字节就算收完了"(收 3.19:TCP 那边其实也在数,两本账各管一层)。
动态页面是"边算边吐"的,第 1 字算出来时还不知道总共多少字节,没法预先声明长度。于是有了 Transfer-Encoding: chunked(分块传输):
- 把内容切成一块块发,每块前面写着"这块多长"
- 最后发一个长度为 0 的块表示"完了"
所以:Content-Length 和 Transfer-Encoding: chunked 不会同时出现——一个是"提前报数",一个是"边发边报"。
3. Content-Encoding——压过没有
Content-Encoding: gzip
网页的 HTML、CSS、JS 都是文本,压缩率常常能到 70%~80%。服务器压好再发,浏览器收到自己解压。
用了压缩之后,Content-Length 报的是"压缩后"的长度——它描述的是这次真正要传多少字节,不是文件在磁盘上多大。
F12 网络面板里那一列"大小"常常写两个数(传输大小 / 资源大小),差的就是压缩省下来的部分。
4. Content-Range——这只是第几段
配合范围请求用(见第四节),让客户端能"只要这个文件的一小截"。
5. ETag / Last-Modified——版本标记
Last-Modified:文件最后修改时间ETag:给这份内容算出的版本标记(像指纹)
它们是给缓存用的(第五节图二)——下次你再来要同一份东西,服务器靠它们判断"能不能省下这次传输"。
三、一个文件,其实是"一段段"搬过去的
你下载一个 48 KB 的图片:
- 应用层(HTTP)的账:Content-Length: 49152
- 传输层(TCP)的账:它被切成好几个数据段,各自编号、各自确认,丢了重传(收 3.18、3.6)
HTTP 从头到尾只把图片当成"一串字节",它并不操心这串字节在路上被切了几段、走了哪条路。
分块传输(chunked) 和 范围传输(Range) 是两件不同的事,很容易混:
| 谁发起 | 干什么用 | 状态码 | |
|---|---|---|---|
| 分块传输 | 服务器自己决定 | 不知道总长度时边算边发 | 仍是 200 |
| 范围传输 | 客户端提要求(Range:) |
只要文件的一部分 | 206 Partial Content |
四、范围请求:为什么下载能"接着下"
客户端请求一小段:
GET /video.mp4 HTTP/1.1
Range: bytes=0-1023 ← 我只要前 1024 个字节
服务器回:
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/48000000 ← 这是总长 4800 万的文件的第 0~1023 字节
Content-Length: 1024
(这一小段的内容)
它的三个日常用途:
- 断点续传:下载断了,下次从断的地方接着要,不用从头再来
- 视频拖进度条:拖到哪儿,就只请求那一段(这也是为什么"拖一下就缓冲")
- 多线程下载:同时开几条连接各要一段,拼起来
看到 206 不要慌,它不是错误——它明确表示"你要的那一小段,我给你了"。4.5 那张状态码表里 2xx 那一族,除了 200 还有它。
五、第二次访问为什么快:缓存的两条路
这是"传输"这件事最划算的一笔账。同一张图,第二次访问时浏览器可能连问都不问,或者问了也只拿回一句"没变"。(配图二)
第一条路 · 强缓存:根本不发请求
第一次访问时服务器在响应里给了:
Cache-Control: max-age=600
意思是"十分钟内你直接用你手里那份,别来问我"。于是十分钟内的再次访问,浏览器直接从硬盘/内存里取图,网络面板里一条新请求都不会出现(显示成"来自磁盘缓存")。
第二条路 · 协商缓存:问了,但只回一个头
十分钟过了,浏览器不敢乱用了,于是带着上次收到的版本标记去问:
GET /logo.png
If-None-Match: "abc123" ← 上次你给我的标记,东西变了没?
服务器一看:没变。于是只回:
HTTP/1.1 304 Not Modified
(没有正文)
省下的就是那 48 KB 正文——头只有几十字节。如果变了,就正常回 200 + 新内容 + 新标记。
- 强缓存:
Cache-Control: max-age=…没过期 → 连请求都不发(最省) - 协商缓存:过期了就拿
ETag/Last-Modified去问 → 没变就回 304,只省正文
这正是 3.21 那个思路的延续:3.23 说过"网络里的状态都有寿命",缓存也一样——不设寿命的缓存比没有缓存更危险。
Last-Modified 的精度只到秒,而且"修改时间变了但内容其实没变"(比如只是重存了一次)也说不清。
ETag 是针对内容算出来的标记,内容不变标记就不变——所以它是更准的那把尺子。
注意两者用的请求头不一样:If-None-Match 配 ETag、If-Modified-Since 配 Last-Modified。
六、响应里还有两个常见动作
① 重定向——服务器说"你要的东西不在这儿了":
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/about/
浏览器会自动去 Location 那个新地址(4.4 讲的"末尾斜杠"、example.com → www.example.com,走的就是它)。
② 决定了是"显示"还是"下载"——同样是给一个文件,加不加这个头效果完全不同:
Content-Disposition: attachment; filename="report.pdf"
加了就是弹出下载框;不加,浏览器能直接显示的就直接显示(图片、PDF、HTML 都会直接打开)。
七、动手 Demo(Demo43)
目标:把 F12 里那几个响应的"搬运字段",一个个亲眼看到。
第 1 步 · 看 Content-Type 带头带尾
curl -I https://example.com
找 Content-Type: text/html; charset=UTF-8——charset 就在这一行,这也是 4.2 那个 <meta charset> 的"正式版"。
第 2 步 · 换个资源,看类型怎么变
分别 curl -I 一个 .png、一个 .json、一个 .css,看 Content-Type 从 image/png 变到 application/json。服务器就是按后缀查表得出的,认这张表你就理解了"后缀的作用到底是什么"。
第 3 步 · 亲手要一次压缩
curl -H "Accept-Encoding: gzip" -I https://example.com
看响应里出现 Content-Encoding: gzip,再对比第 1 步的 Content-Length——通常小了一大截,这就是"压缩后的长度"。
第 4 步 · 抓一次分块传输
curl -v "https://example.com" | head -50
在 curl 输出的响应头里找 Transfer-Encoding: chunked——有它就不会有 Content-Length。动态页面里很常见。
第 5 步 · 只要前 100 个字节(范围请求)
curl -r 0-99 -v https://example.com
看状态行变成 206 Partial Content,并且有一个 Content-Range。这就是"断点续传"和"视频拖进度"的真实机制。
第 6 步 · 亲手制造一次 304 先拿到标记:
curl -I https://example.com
记下 ETag 的值,然后拿它去问:
curl -I -H 'If-None-Match: "刚才那个值"' https://example.com
状态行应该变成 304,而且没有 Content-Length、没有正文。
第 7 步 · 在 F12 里看"省了多少钱" 网络面板打开,刷新页面,看「大小」那一列的两个数字(传输大小 / 资源大小)——差出来的就是压缩和缓存省下的部分。 再看静态资源那条,状态是 304 或"来自磁盘缓存"。
第 8 步 · 用本地服务器验证"类型由后缀表决定"
python -m http.server 8000
用 4.2 那个 index.html 访问一次,然后把文件改名成 index.txt 再访问——你会发现浏览器不把它当网页渲染了,直接当文本显示(因为服务器给了 text/plain)。
这一下就把"后缀 → Content-Type → 浏览器怎么显示"这条链走通了(收 4.5)。
八、30 秒实验
- 30 秒:
curl -I同一个地址两次,看第二次的响应头里有没有ETag/Last-Modified。 - 30 秒:
curl -r 0-9 -v https://example.com——只取 10 个字节,看 206 和Content-Range。 - 30 秒:F12 里随便点一张图,把「传输大小」和「资源大小」两个数字抄下来,算一下压缩省了多少。
九、常见误区
- "
Content-Type就是看文件后缀。" —— 后缀只是服务器查表的依据;真正决定浏览器行为的是Content-Type(4.5 那条"后缀无所谓"的完整答案)。 - "
Content-Length是文件在磁盘上多大。" —— 它说的是这次要传多少字节。压缩之后它比文件小。 - "文件是整块传过去的。" —— 应用层看到的是一串字节,传输层早把它切成好几段各自编号了(收 3.18 / 3.19)。
- "304 是出错了。" —— 304 是最省的一次成功:东西没变,正文不用重传。
- "每次打开网页都要问服务器。" —— 强缓存没过期时一个请求都不发("来自磁盘缓存"就是这么来的)。
- "分块传输是大文件的专利。" —— 恰恰相反,动态页面(边算边吐、事先不知道长度)才是它最常见的舞台。
- "范围请求是服务器主动切好的。" —— 是客户端先提
Range:,服务器才回 206。 - "
Content-Length和Transfer-Encoding可以一起写。" —— 不能,一个是提前报数、一个是边发边报,二选一。 - "缓存过期了就只能重新全下载。" —— 过期只意味着"该问一下了",问完没变就是 304,还是不下载。
- "
ETag就是修改时间。" —— 那是Last-Modified。ETag是针对内容算的标记,更精确(改动时间变了、内容没变时它不会变)。
十、动手自测
- 服务器接到请求后有哪四步?哪一步是"协议问题"而不是"业务问题"?
Content-Type的格式是什么?服务器是怎么得出它的?charset写在哪?- 什么时候会出现
Transfer-Encoding: chunked?它和Content-Length能同时出现吗? Content-Encoding: gzip出现时,Content-Length指的是什么长度?- 分块传输和范围传输有什么区别?各是谁发起的?状态码各是什么?
- 范围请求能解决哪三件日常事?
- 什么样的响应会让浏览器"根本不发请求"?什么样的响应会让它"发了但没拿正文"?
ETag和Last-Modified有什么不同?它们各自配哪个请求头使用?Content-Disposition: attachment会带来什么变化?- 一个 48 KB 的图片,"应用层的长度"和"传输层被切成的段"是什么关系?
答案:
① 收下请求 → 找到内容 → 装好包装 → 发出响应。"装好包装"是协议问题(每个服务器都守同一套 HTTP 规矩),"找到内容"才是业务问题(静态读文件 / 动态调程序)。
② 格式是 类型/子类型(如 text/html、image/png、application/json)。服务器按文件后缀查一张对照表(mime.types)得出;charset 就写在 Content-Type 后面(如 text/html; charset=UTF-8),优先级高于 HTML 里的 <meta charset>。
③ 当服务器事先不知道正文总长度时(动态页面边算边吐)会用它。不能同时出现——Content-Length 是提前报数,Transfer-Encoding: chunked 是边发边报(用长度为 0 的块收尾)。
④ 指的是压缩后的长度——它描述"这次实际要传多少字节",所以会比磁盘上的文件小。
⑤ 分块传输由服务器自己决定(因为不知道长度),状态码仍是 200;范围传输由客户端先提 Range:,服务器回 206 Partial Content。
⑥ 断点续传(断了接着下)、视频拖进度条(只取拖到的那段)、多线程下载(几段同时下再拼)。
⑦ 响应里带 Cache-Control: max-age=… 且还没过期时,浏览器直接用本地副本,不发请求;带 ETag / Last-Modified 时,浏览器过期后会带标记去问,服务器回 304,于是请求发了、正文没传。
⑧ Last-Modified 是最后修改时间(精度只到秒),ETag 是针对内容算出的版本标记(更准)。ETag 配 If-None-Match,Last-Modified 配 If-Modified-Since。
⑨ 加了它,浏览器不显示内容,改成弹下载框(并可用 filename 指定保存的名字);不加的话,能直接显示的类型(图片、PDF、HTML)会被直接渲染出来。
⑩ 没关系也有关系:应用层的账是"一共 49152 字节"(Content-Length),传输层把它切成好几个数据段分别编号、确认、必要时重传(收 3.18)。HTTP 只把这串字节当整体,不操心底层切了几段。
十一、下一步学什么
- ☑ 记录Web网站的文件(HTTPCookie) —— 本节把"服务器怎么把文件装进响应"讲完了,也结清了 304 这笔账。这就把 4.5 留下的那张纸条(Cookie)扶正做主角:它怎么来、长什么样、存在哪、谁管它——顺带理清一条最容易被绕晕的界线:纸条(Cookie)在你电脑上,账本(会话)在服务器上。
相关笔记
- 请求Web网站的文件(HTTP) —— 上一节:一问一答的规矩与状态码;本节站在服务器一侧展开"响应"这一半
- Web网站是如何构建的(Web网站) —— "找到内容"这一步的两种来源:静态读文件 / 动态调程序;以及"上百次请求"里每一次都要走这四步
- 制作Web网页(HTML) ——
<meta charset>与响应头里的charset是"备用与正式"的关系,中文乱码的根子 - 设计Web网页的外观(样式表) —— CSS 能被缓存、第二次几乎不花时间,机制就是本节第五节的强缓存与 304
- Web网站的地址(URL) —— 301 重定向带来的"多条 URL",会拉低缓存命中率,也是"一资源多地址"的麻烦
- 对应用程序的数据进行可靠传输(TCP) —— 传输出错由 TCP 兜着,HTTP 只管把字节交给它
- 用TCP对数据进行分割处理(序列号、ACK号) —— "应用层的一整块"与"传输层的若干段"是两本账
- 数据的称谓(信息、数据段、数据包、数据帧) —— 同一个文件在不同层有不同的名字
- 网络的电话簿(域名解析、主机名、DNS) —— 缓存都要有"寿命"这个思路,从 DNS 的 TTL 一脉相承到
Cache-Control