🌐 第四章 Web网站的运行原理
📄 一句话概括

上一节讲的是一问一答的规矩,本节站到服务器那一侧:它接到请求后要走四步(收下请求 → 找到内容 → 装好包装 → 发出响应),而"装好包装"这一步最讲究——它决定了响应里的那几个搬运字段:Content-Type(是什么)、Content-Length(多长)、Content-Encoding(压没压)、Content-Range(这是第几段)、ETag(版本标记)。其中最能体现设计巧思的是最后两个:分段传输让下载可以断点续传、视频可以拖进度;版本标记让同一张图第二次访问时,服务器只回一句 304 就完事——正文一个字节都不重传。

📝 本节和 4.5 的分工

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     ← 二进制流,说不清是什么就用它

两个要点:

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——版本标记

它们是给缓存用的(第五节图二)——下次你再来要同一份东西,服务器靠它们判断"能不能省下这次传输"。

三、一个文件,其实是"一段段"搬过去的

📝 别把两本账混了

你下载一个 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 就是"部分内容"

看到 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 说过"网络里的状态都有寿命",缓存也一样——不设寿命的缓存比没有缓存更危险。

⚠️ 为什么 `ETag` 比 `Last-Modified` 更可靠

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 秒实验

  1. 30 秒:curl -I 同一个地址两次,看第二次的响应头里有没有 ETag / Last-Modified。
  2. 30 秒:curl -r 0-9 -v https://example.com——只取 10 个字节,看 206 和 Content-Range。
  3. 30 秒:F12 里随便点一张图,把「传输大小」和「资源大小」两个数字抄下来,算一下压缩省了多少。

九、常见误区

  1. "Content-Type 就是看文件后缀。" —— 后缀只是服务器查表的依据;真正决定浏览器行为的是 Content-Type(4.5 那条"后缀无所谓"的完整答案)。
  2. "Content-Length 是文件在磁盘上多大。" —— 它说的是这次要传多少字节。压缩之后它比文件小。
  3. "文件是整块传过去的。" —— 应用层看到的是一串字节,传输层早把它切成好几段各自编号了(收 3.18 / 3.19)。
  4. "304 是出错了。" —— 304 是最省的一次成功:东西没变,正文不用重传。
  5. "每次打开网页都要问服务器。" —— 强缓存没过期时一个请求都不发("来自磁盘缓存"就是这么来的)。
  6. "分块传输是大文件的专利。" —— 恰恰相反,动态页面(边算边吐、事先不知道长度)才是它最常见的舞台。
  7. "范围请求是服务器主动切好的。" —— 是客户端先提 Range:,服务器才回 206。
  8. "Content-Length 和 Transfer-Encoding 可以一起写。" —— 不能,一个是提前报数、一个是边发边报,二选一。
  9. "缓存过期了就只能重新全下载。" —— 过期只意味着"该问一下了",问完没变就是 304,还是不下载。
  10. "ETag 就是修改时间。" —— 那是 Last-Modified。ETag 是针对内容算的标记,更精确(改动时间变了、内容没变时它不会变)。

十、动手自测

❓ 自测一下
  1. 服务器接到请求后有哪四步?哪一步是"协议问题"而不是"业务问题"?
  2. Content-Type 的格式是什么?服务器是怎么得出它的?charset 写在哪?
  3. 什么时候会出现 Transfer-Encoding: chunked?它和 Content-Length 能同时出现吗?
  4. Content-Encoding: gzip 出现时,Content-Length 指的是什么长度?
  5. 分块传输和范围传输有什么区别?各是谁发起的?状态码各是什么?
  6. 范围请求能解决哪三件日常事?
  7. 什么样的响应会让浏览器"根本不发请求"?什么样的响应会让它"发了但没拿正文"?
  8. ETag 和 Last-Modified 有什么不同?它们各自配哪个请求头使用?
  9. Content-Disposition: attachment 会带来什么变化?
  10. 一个 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 只把这串字节当整体,不操心底层切了几段。

十一、下一步学什么

相关笔记