# HTTP传输大文件
30 秒速记
- 四招:压缩(变小)、分块传输(边生成边发)、范围请求(只要一段)、多段范围(一次要几段)
- 压缩:
Accept-Encoding/Content-Encoding,只对文本有效,图片视频压不动 - 分块:
Transfer-Encoding: chunked,长度未知也能发,不能和Content-Length同时出现,只在HTTP/1.1里有 - 范围:
Range: bytes=x-y→206+Content-Range;越界416;支持与否看Accept-Ranges - 多段:
multipart/byteranges+boundary分隔;几招可以组合,解决的是不同问题
HTTP 传大文件有四招,按问题选:内容能压就先压,长度不知道就分块发,只要一部分就用范围请求。 压缩针对文本,HTML、JS 压完能小一大半,图片视频本身已经压过,再压没意义。分块传输是服务器不用等内容全部生成好,生成一块发一块,内存压力小,流式渲染、导出报表常用。范围请求是客户端说「我只要第 1000 到 1999 字节」,视频拖进度条、断点续传都靠它。这几招不互斥,比如动态生成的文本可以压缩后再分块发。
四种手段对照着记最清楚:
| 手段 | 解决什么 | 关键头 | 状态码 |
|---|---|---|---|
| 压缩 | 数据太大 | Accept-Encoding / Content-Encoding |
200 |
| 分块传输 | 长度未知、要边生成边发 | Transfer-Encoding: chunked |
200 |
| 范围请求 | 只要其中一段 | Range / Content-Range / Accept-Ranges |
206 / 416 |
| 多段范围 | 一次要好几段 | Content-Type: multipart/byteranges; boundary=... |
206 |
前端日常最常见的几个场景:
- 静态资源(
JS、CSS、HTML):构建时预压缩成.br/.gz,Nginx按Accept-Encoding直接返回,省掉实时压缩的CPU。 - 流式
SSR:React 18的renderToPipeableStream、Next.js的流式渲染,在HTTP/1.1下就是靠chunked先把页面骨架发出去,数据好了再补后面的块。 - 视频播放:
<video>第一次请求就会带Range: bytes=0-,服务端返回206后,拖进度条时浏览器会按新位置再发范围请求。 - 大文件上传:方向反过来,前端用
file.slice()切片分别上传,服务端拼接。这是应用层的方案,HTTP的Range只管下载。
Node 里不写 Content-Length 直接多次 res.write(),HTTP/1.1 下会自动变成 chunked:
require('http').createServer((req, res) => {
res.write('第一块\n') // 立刻发出
setTimeout(() => res.end('第二块\n'), 1000) // 1 秒后发第二块并结束
}).listen(3000)
// curl -i localhost:3000 能看到 Transfer-Encoding: chunked
面试官追问
追问 1视频页用了 chunked,用户拖到 30 分钟处还是从头加载,为什么?
chunked 只是服务器怎么把这次响应分批发出去,客户端没法通过它指定从哪开始。拖进度条要的是范围请求,<video> 会自动发 Range,前提是服务端支持并返回 206。
追问 2要下载一个几 GB 的文件,服务端先全读进内存再返回可以吗?
不行,几个人同时下载内存就爆了。用流式读取边读边写,Node 里就是 fs.createReadStream(file).pipe(res)。文件长度已知的话带上 Content-Length 就行,长度未知才用 chunked。
追问 3同一个静态服务既有大日志文本又有视频,统一开 gzip 可以吗?
按类型开。文本开,Nginx 用 gzip_types 列出 text/plain、application/json、application/javascript 这些;视频、图片别加进去,压不动还白费 CPU。
追问 4怎么一眼判断一个下载地址支不支持断点续传?
curl -I 看响应有没有 Accept-Ranges: bytes,再实际发一个 curl -r 0-99 -I 看是不是 206。只返回 200 就是不支持,下载器只能从头来。
追问 5HTTP/2 下还有 chunked 吗?
没有了。HTTP/2 自带二进制分帧,数据天然一帧一帧地发,规范明确不允许用 Transfer-Encoding: chunked。压缩和范围请求在 HTTP/2 里照样用。
# 数据压缩
30 秒速记
- 浏览器
Accept-Encoding: gzip, deflate, br报能力 → 服务器挑一个压缩 →Content-Encoding回告 - 压缩率 50% ≈ 同带宽下传输时间减半,文本类效果最明显
- 图片、音视频本身已高度压缩,再
gzip基本不变小,甚至略大 Nginx:gzip on默认只压text/html,JS、CSS、JSON要自己加进gzip_types- 静态资源最好构建时预压缩(
gzip_static),小于1KB左右的响应没必要压
数据压缩就是服务器用浏览器支持的算法把响应体压小再发,是传大文本最基础、性价比最高的优化。 浏览器在 Accept-Encoding 里列出能解的格式,服务器挑一个压缩,再用 Content-Encoding 告诉浏览器用的哪个。100KB 的 HTML 压到 30KB 很常见,相当于带宽没变网速快了好几倍。但它只对文本有效,图片视频本来就是压缩格式。实际配置时有个坑,Nginx 的 gzip on 默认只压 text/html,JS、CSS 和 JSON 接口要在 gzip_types 里手动加上,不然开了跟没开差不多。
gzip on;
gzip_min_length 1k; # 太小的响应不压
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_static on; # 有 app.js.gz 就直接发,不用实时压缩
通常浏览器在发送请求时都会带着“Accept-Encoding”头字段,里面是浏览器支持的压缩格式列表,例如 gzip、deflate、br 等,这样服务器就可以从中选择一种压缩算法,放进“Content-Encoding”响应头里,再把原数据压缩后发给浏览器。
如果压缩率能有 50%,也就是说 100K 的数据能够压缩成 50K 的大小,那么就相当于在带宽不变的情况下网速提升了一倍,加速的效果是非常明显的。
不过这个解决方法也有个缺点,gzip 等压缩算法通常只对文本文件有较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高度压缩的,再用 gzip 处理也不会变小(甚至还有可能会增大一点),所以它就失效了。
不过数据压缩在处理文本的时候效果还是很好的,所以各大网站的服务器都会使用这个手段作为“保底”。例如,在 Nginx 里就会使用“gzip on”指令,启用对“text/html”的压缩
面试官追问
追问 1给 JPEG、视频统一套一层 gzip,负责人说只要压缩就一定更小,对吗?
不对。这些格式本身已经压缩过,熵很高,gzip 找不到可压的重复内容,结果基本一样大,加上头部开销还可能略大,CPU 白烧。SVG 例外,它是文本,压缩效果很好。
追问 2开了 gzip on,JS 文件在 Network 面板里还是没有 Content-Encoding,为什么?
gzip_types 没包含 application/javascript,默认只压 text/html。另外检查文件是不是小于 gzip_min_length,以及前面是不是有一层代理把压缩去掉了。
追问 3旧浏览器只声明 gzip,网关固定返回 br,有什么问题?
客户端解不开,直接白屏或报错。服务端必须在 Accept-Encoding 里出现过的格式中选,br 和 gzip 都准备着,按请求头挑。
追问 4压缩响应体变小了,但缺了 Content-Encoding,会怎样?
浏览器把压缩字节当原文解析,页面显示乱码,JSON 直接解析失败。压缩和打标必须在同一层做,常见问题是应用压了没打头,或者网关解压了没删头。
追问 5实时压缩吃 CPU,怎么优化?
静态资源构建时就压好,比如用 compression-webpack-plugin 产出 .gz / .br,Nginx 开 gzip_static 直接发。只有接口这类动态内容才实时压,压缩级别用中等,比如 gzip_comp_level 设 5 左右,再高收益很小。
# 分块传输
30 秒速记
Transfer-Encoding: chunked:响应体拆成多块依次发,接收方按顺序拼回去- 适合长度事先不知道的内容:流式渲染、数据库边查边出的报表、日志流
- 每块 = 十六进制长度 +
\r\n+ 数据 +\r\n,长度只算数据本身 - 以长度
0的块结束:0\r\n\r\n;没有它客户端会一直等 - 和
