4. HTTP/HTTPS 协议基础:Range 请求头、Content-Range 响应头、断点续传的 HTTP 语义

做 OTA 的人,要是搞不懂 HTTP 的 Range 机制,那断点续传基本就是空中楼阁。我见过不少团队,上来就自己造轮子,搞什么分片标记、本地偏移量记录,折腾一圈发现——其实 HTTP 协议早就把这事定义得明明白白了。

说白了,断点续传在 HTTP 世界里就两个核心:客户端告诉服务器我要哪一段,服务器告诉我这一段是什么。就这么简单。

4.1 Range 请求头:客户端说「我要哪一段」

客户端发起续传时,会在请求里带上 Range 头。格式长这样:

Range: bytes=5000-9999

意思是:我要第 5000 到第 9999 字节的数据。注意,HTTP 里的字节范围是从 0 开始计数的,而且两端都是闭区间。

我遇到过有人把范围写成 bytes=5000-10000,结果多要了一个字节,校验和死活对不上。排查了半天才发现是边界问题。

Range 头还有几种变体:

格式 含义 示例
start-end 指定起始和结束字节 bytes=0-4999
start- 从起始字节到文件末尾 bytes=5000-
-suffix 最后 N 个字节 bytes=-1000
多段 同时请求多个不连续范围 bytes=0-999,2000-2999

车载场景下,最常用的是 start- 这种格式。比如下载到 5000 字节时断了,重连后直接发 Range: bytes=5000-,服务器就从断点往后发。

我的习惯:在车载 OTA 客户端里,我会把已接收的字节数持久化到 /data/ota/download_progress 文件里。每次断连后读取这个值,构造 Range 头。别放内存里,车机一断电就丢了。

4.2 Content-Range 响应头:服务器说「我给你的是哪一段」

服务器收到 Range 请求后,如果支持断点续传,会返回 206 Partial Content 状态码,并在响应头里带上 Content-Range。

格式是这样的:

Content-Range: bytes 5000-9999/10000

斜杠前面是实际返回的字节范围,斜杠后面是整个文件的总大小。这个总大小很关键——客户端拿到它,就知道整个升级包有多大,可以提前分配存储空间。

我建议你在客户端做这么一件事:

  1. 第一次请求时,不带 Range 头,看服务器返回的 Content-Length 或 Content-Range 里的总大小。
  2. 把这个总大小记下来,用来校验最终下载的完整性。
  3. 后续断点续传时,用这个总大小判断是否下载完毕。
核心要点:Content-Range 里的总大小是「整个资源的大小」,不是「剩余大小」。很多新手会搞混,以为斜杠后面是还没下载的部分。

4.3 断点续传的完整 HTTP 语义

把上面两个头组合起来,就是一次完整的断点续传流程。我画了张图,你看一眼就明白了:

HTTP 断点续传交互流程 OTA 客户端 OTA 服务器 ① GET /update.bin 无 Range 头,首次请求 ② 200 OK Content-Length: 10485760 ⚠ 下载中断 ③ GET /update.bin Range: bytes=5242880- ④ 206 Partial Content Content-Range: bytes 5242880-10485759/10485760 ✓ 下载完成

流程其实就四步:

  1. 首次请求:客户端不带 Range 头,服务器返回完整文件,状态码 200。
  2. 记录进度:客户端边下载边记录已接收的字节数。
  3. 断点续传:连接断开后,客户端用已接收字节数构造 Range 头,重新发起请求。
  4. 服务器响应:服务器返回 206 状态码,带上 Content-Range 头,从断点开始发数据。
我曾经踩过的坑:有些老旧的车载 HTTP 服务器(比如某些供应商的嵌入式 HTTPD)不支持 Range 头,遇到 Range 请求直接返回 200 完整文件。客户端必须做兼容——收到 200 时,丢弃已下载的部分,重新下载整个文件。别问我怎么知道的,问就是被坑过。

4.4 多段 Range 与车载场景

HTTP 1.1 还支持多段 Range,即一次请求多个不连续的范围。响应会使用 multipart/byteranges 格式返回。

但在车载 OTA 里,我不推荐用多段 Range。原因有三:

  • 车机内存有限,多段数据同时涌入,缓冲区管理复杂。
  • 多段响应的解析比单段复杂,容易出 bug。
  • 大部分 CDN 和代理服务器对多段 Range 支持不完善。

我的做法是:一次只请求一段,串行下载。虽然慢一点,但稳定可靠。车载系统最怕的就是不稳定。

4.5 条件请求与断点续传的配合

断点续传还有一个隐藏问题:服务器上的文件变了怎么办?

比如你下载到一半,服务器端换了一个新版本的升级包。这时候你拿着旧的 Range 去续传,拼出来的文件就是「前半段旧版本 + 后半段新版本」,校验和肯定过不了。

解决方案是用 ETag 或 Last-Modified 做条件请求:

GET /update.bin HTTP/1.1
Range: bytes=5242880-
If-Match: "abc123def456"

服务器发现 ETag 不匹配,会返回 412 Precondition Failed,客户端就知道文件变了,需要重新下载。

我的建议:在车载 OTA 客户端里,把 ETag 和已下载字节数一起持久化。每次续传前先比对 ETag,变了就清空进度重新下载。别省这一步,省了就是给自己挖坑。

4.6 HTTPS 对断点续传的影响

HTTPS 本质上不影响 Range 语义——Range 和 Content-Range 是 HTTP 层面的东西,TLS 只负责传输加密。

但有一个实际问题:TLS 握手开销。每次断线重连都要重新握手,在信号不好的车载环境里,这个开销可能比传输数据本身还大。

我一般会做这么几个优化:

  • 启用 HTTP Keep-Alive,减少握手次数。
  • 使用 Session Resumption,复用之前的 TLS 会话。
  • 如果车机支持,考虑 TLS 1.3,它的 0-RTT 模式能省一次往返。

嗯,这些优化在 4G/5G 信号稳定的地方效果不明显,但在隧道、地库这些场景里,差别就大了去了。

4.7 小结:断点续传的 HTTP 语义清单

元素 方向 说明
Range 请求头 客户端指定要获取的字节范围
Content-Range 响应头 服务器告知实际返回的范围和总大小
206 Partial Content 状态码 服务器成功处理了 Range 请求
416 Range Not Satisfiable 状态码 请求的范围超出文件大小
ETag 响应头 资源版本标识,用于检测文件是否变更
If-Match 请求头 条件请求,ETag 匹配时才执行
If-None-Match 请求头 条件请求,ETag 不匹配时才执行