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-,服务器就从断点往后发。
/data/ota/download_progress 文件里。每次断连后读取这个值,构造 Range 头。别放内存里,车机一断电就丢了。
4.2 Content-Range 响应头:服务器说「我给你的是哪一段」
服务器收到 Range 请求后,如果支持断点续传,会返回 206 Partial Content 状态码,并在响应头里带上 Content-Range。
格式是这样的:
Content-Range: bytes 5000-9999/10000
斜杠前面是实际返回的字节范围,斜杠后面是整个文件的总大小。这个总大小很关键——客户端拿到它,就知道整个升级包有多大,可以提前分配存储空间。
我建议你在客户端做这么一件事:
- 第一次请求时,不带 Range 头,看服务器返回的
Content-Length或Content-Range里的总大小。 - 把这个总大小记下来,用来校验最终下载的完整性。
- 后续断点续传时,用这个总大小判断是否下载完毕。
4.3 断点续传的完整 HTTP 语义
把上面两个头组合起来,就是一次完整的断点续传流程。我画了张图,你看一眼就明白了:
流程其实就四步:
- 首次请求:客户端不带 Range 头,服务器返回完整文件,状态码 200。
- 记录进度:客户端边下载边记录已接收的字节数。
- 断点续传:连接断开后,客户端用已接收字节数构造 Range 头,重新发起请求。
- 服务器响应:服务器返回 206 状态码,带上 Content-Range 头,从断点开始发数据。
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,客户端就知道文件变了,需要重新下载。
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 不匹配时才执行 |