TCP OTA 服务端手册
通用 OTA 服务 · TCP 通道 · 服务端 / 运维视角 · 协议版本 1
← 协议总览 客户端手册 →

TCP OTA 服务端手册

通用 OTA 服务 TCP 通道的服务端视角文档:实现契约(收发包、动作处理、错误码选择、 分片与会话参数)与运维要点。开发服务端、排查线上问题看这份; 设备端接入看 《TCP OTA 客户端接入手册》。

读者 服务端 / 运维 协议版本 1 监听 0.0.0.0:8011(mux 转发目标) 回包必须带 本请求的 sid 配套:客户端手册 →
服务端要点速查(完整规则见后文各章)
必须守住的回显(多设备并发不串台的前提)
  • sid 回包带本请求帧的会话号,逐请求记录,不能缓存在连接对象上
  • svc_type 同样逐请求回显(mux 按 (type, sid) 路由)
  • 链路:tcp_mux ↔ 本服务之间是一条长连接,同时承载多台设备
报文与帧上限
  • 64 KB 应用消息上限(TCP_OTA_MSG_MAX),超限回 err=2
  • 4 MiB 单帧负载上限(TCP_FRAME_MAX_DATA),超出判为帧头损坏
  • 14 B 帧头固定长度,多字节字段大端
关键阈值
  • 600 s 下载会话空闲回收
  • 64 并发会话表上限(满 → err=13)
  • 16 单次 pull 片数上限(硬顶 128)
  • 60 KB changelog 上限(超 → err=11)
  • 512 发送队列槽位(不足即回 again)
TCP 通道参数([transfer.tcp])
  • CHUNK 默认 8192,区间 128 ~ 65535
  • BATCH 16 RETRY 2
  • IDLE 300 s CONNS 64

一、服务端在链路中的位置

1、链路上的角色

角色职责与协议的关系
设备端发语义请求、按帧拆包、按 sid 匹配响应协议的发起方,见 客户端手册
FRP / 公网隧道把 tcp.qftek.top:32200 映射到内网 mux纯转发,不解析内容
tcp_mux分配会话号 sid、应答心跳、按 (type, sid) 路由完整帧透传:本服务收到的就是设备原始帧
OTA 服务(本手册)帧编解、动作处理、会话管理、分片下发、版本 / 差分 / 灰度决策监听 0.0.0.0:8011,被 mux 当作普通 TCP 上游
设计要点 业务逻辑与链路解耦:引擎只认 7 类语义动作(hello / check / start / pull / finish / abort / report / changelog 里的统一请求结构),适配层负责把帧 + JSON 翻译成 ota_req_t、把 ota_resp_t 序列化成报文。 换协议只换适配层,引擎不动。

2、多设备并发与 sid 回显

tcp_mux 与本服务之间只有一条 TCP 长连接,这条连接上同时承载多台设备 (各自不同 sid)。因此:

  • 每处理一个请求帧,就记下这个帧的 sid 与 svc_type, 并在回包里原样带上;
  • 绝不能把 sid 缓存在连接对象上 —— 期间其它设备的请求会把它改掉。
/* ✅ 正确:逐请求记录 */
if (f->frame_type == DATA) { cur_sid = f->sid; cur_svc = f->svc_type; }
send_frame(DATA, cur_svc, cur_sid, payload, len);

/* ❌ 错误:多设备并发必然串台 */
conn->sid = f->sid;              // 设备 A 的请求
... send_frame(..., conn->sid)   // 期间设备 B 的请求可能已改掉它
实测踩过的坑 6 台设备并发时数据互相串台(A 的响应发给了 B)。改成逐请求记录后,6 路并发 100% 正确。 本服务的 handle_frame() 就是按这个原则实现的:进入分发前先记录本请求的 sid / svc_type,后续所有回包都用这份记录。

二、帧收发契约

1、帧头与同步字

与客户端一致(详见 客户端手册 · 帧格式),服务端实现要点:

  • 同步字固定 AA 55 5A A5 FF EC(6 字节),帧头 14 字节,多字节字段大端;
  • 帧长 = 14 + 数据长度,无填充、无帧级校验和;
  • 发 JSON 响应用 DATA(0x00)帧,一帧一条消息; 二进制分片的每个字节帧也是独立的 DATA 帧。

2、收帧容错

场景处理要求
流里出现垃圾字节用同步字重新对齐(memchr + memcmp 逐点匹配),不能让解析器卡死
长度字段不合理超过 TCP_FRAME_MAX_DATA(4 MiB)判为帧头损坏:丢弃同步、重新找帧
读到的数据不足一帧保留半帧状态(解析器状态机),下次读入继续拼
跨包同步字同步字可能被 TCP 分段切开,需支持跨读边界匹配

3、发帧要求

  • 回包的 frame_type / svc_type / sid 取本请求的值(见 一、2);
  • JSON 响应与字节帧都在同一连接上按序发出(pull 的 JSON 元信息帧在前, cnt 个字节帧紧随其后);
  • 发送队列满时不丢帧:pull 走 again 流控 (见 六、4),其它响应排队发送。

三、请求处理契约

1、报文校验与错误映射

场景错误码说明
负载为空2empty payload
负载 ≥ 64 KB(TCP_OTA_MSG_MAX)2message too large,防超大报文打爆单条消息缓冲
JSON 解析失败 / 未知动作1unknown act 'xxx';解析失败不触发固件库扫描
该动作必填字段缺失(如 product)2missing product
pull / finish / abort 缺 sid8missing sid / session not found
未知字段一律忽略 解析器只取它认识的键,多余的字段不报错 —— 这是协议向前演进的前提,客户端多加字段不会被打回。

2、动作分发表

动作处理路径成功响应
hello引擎统一处理(读本协议分片参数){"ok":1,"pver","chunk","min","max"}
check引擎:写锁 → 产品 → 禁用 → 灰度 → 固件 → 版本 / 差分{"ok":1,"latest","upd","busy","force","hw","size","md5","delta","dsize"}
start引擎:分配下载会话、决定全量 / 差分{"ok":1,"sid","trans","size","total","chunk","md5"}
pull适配层专有:读会话、算片数、发 JSON + 字节帧{"ok":1,"cnt","next","eof","done",…}
finish适配层专有:结束会话、记录产物 md5{"ok":1,"sid","size","md5"}
abort释放会话(未带 sid → err=8){"ok":1}
report仅记日志(event / msg){"ok":1}
changelog读产品日志文件{"ok":1,"content":"…"}

3、按需扫描固件库

  • 只有 check / changelog / start 三类动作会触发固件库扫描(它们才开始「一次新的判断 / 传输」);
  • 扫描有最小间隔(SCAN_INTERVAL_SEC,默认 10 秒),间隔内重复请求零系统调用开销;
  • 写锁(write_busy)每秒检查,秒级生效,不受扫描间隔影响;
  • 配置热加载走同一套节奏:只在收到设备请求时看一眼配置文件, 冷却 ≥10 秒(与 SCAN_INTERVAL_SEC 取大者),冷却期内零 IO; 改完配置 → 下一个请求即生效(日志出现 配置已热加载, 统计行有「配置重载 N」计数);
  • 设计意图:没有升级任务时系统完全静止,不空转、不浪费 CPU (闲时既不扫固件库,也不读配置文件)。

4、响应序列化规则

  • 失败:{"ok":0,"err":N,"msg":"…"}(msg 供日志 / 排障, 客户端不必解析);
  • 成功:{"ok":1, …} + 该动作的字段;字段按「是否非零 / 非空」决定是否输出, 客户端只读它关心的;
  • check 的 busy 字段与 latest 一起判断是否输出; 无更新时附带 "nonew":1,便于客户端直接分支。
维护中为什么故意不带 latest 写锁开启时的响应是 {"ok":1,"upd":0,"busy":1}:不暴露任何版本信息。 如果实现成「带 latest 但 upd=0」,客户端会把维护误判成「已是最新」—— 语义完全不同。

四、各动作实现要点

1、hello

  • 返回本协议的 pver、允许区间 min / max 与生效分片 chunk;
  • 客户端带了 chunk → 按本协议区间钳制后返回;
  • 带了 product → 顺带返回该产品配置里的推荐分片(同样钳制), 客户端可以据此少配一个参数。

2、check

判定顺序(前面的条件命中就直接返回,不读固件文件):

  1. 写锁 / 维护中 → busy=1 + upd=0(不暴露版本)
  2. 产品不存在 → err=3
  3. 产品停用 → err=4
  4. 灰度未命中(按 did)→ err=10
  5. 固件文件缺失 → err=5
  6. 版本比对 → 相同则「无更新」:upd=0 + nonew=1(ok 仍为 1)
  7. 有更新时再选差分:补丁可用且客户端版本支持差分 → delta=1 + dsize
版本判定用「不相等」而不是「小于」 cv 与服务端目标版本一致才算已是最新;不一致(更高或更低)都返回有更新。 这样可以支持服务端降级发布,且与既有通道口径一致。
差分可用条件说明
二进制分片开关打开([proto] ENABLE_BINARY_CHUNK)关闭时一律下发全量
客户端版本能解析且 ≥ 产品配置的 MIN_CLIENT_VERSION不满足则不下发补丁(客户端若强行要差分,start 会拿到 err=14)
存在 cv → 目标版本 的补丁文件缺失时 start 指定 trans:"D" 会拿到 err=6

3、start

  • 分配下载会话并返回 sid;会话表满 → err=13;
  • trans 返回实际采用的方式:请求显式指定就按指定(无补丁 → err=6、 版本过低 → err=14),未指定则「有补丁优先差分」;
  • size 是本次传输对象的大小:全量 = 完整包;差分 = 补丁大小;
  • md5 恒为目标完整包的 MD5(差分路径下即「旧包 + 补丁」合成产物的校验基准);
  • total = ⌈size / chunk⌉(按实际生效分片向上取整)。

4、pull

要点实现规则
会话校验会话不存在 / 已关闭 → err=8
片数钳制请求 batch > 本协议上限 → 钳到上限;硬顶 128
二进制模式(bin=1)先发元信息 JSON(cnt/next/eof/done/chunks[]),再发 cnt 个字节帧
文本模式(bin=0)分片以 hex 内嵌 JSON、一帧返回;片数按 64 KB 消息上限反推(见 六、3)
发送队列不足全有或全无:不发部分数据,返回 {"ok":1,"cnt":0,"again":1,"next":0,"eof":0}
off 越界(off ≥ size)返回 {"ok":1,"eof":1},不报错(客户端「最后一片后再问一次」是常见实现)
片元信息chunks[i] 给出 {off, len, crc}:off 是目标文件里的偏移,crc 为 CRC32(小写 hex,可选校验)

5、finish / abort / report

  • finish:结束下载会话,返回 sid / size / 目标完整包 md5;请求里的 result 与客户端产物 md5 仅记日志,用于对账排障,不参与决策;
  • abort:直接释放会话,回 {"ok":1}(缺 sid → err=8);
  • report:把 event / msg 写进日志后回 {"ok":1},客户端上报失败原因靠它落盘。

6、changelog

  • 返回更新日志全文;没有日志文件时回 {"ok":1,"content":""},不是错误;
  • 日志超过 60 KB(OTA_CHANGELOG_MAX)→ err=11 (不截断成半截 JSON);
  • cv 可忽略:一律返回「最新版本的日志」;
  • 错误分支与 check 对齐:缺 product → err=2、 未知产品 → err=3、停用 → err=4、灰度未命中 → err=10、维护中 → err=7、读文件失败 → err=11。

五、错误码触发场景全表

服务端视角:什么情况回哪个码。客户端侧的建议动作见 客户端手册 · 错误码全表。

err服务端触发场景线上报文
0成功{"ok":1,…}
1未知动作 / JSON 解析失败(含空动作名){"ok":0,"err":1,"msg":"unknown act 'x'"}
2参数缺失或非法:空负载、消息 ≥ 64 KB、缺 product、to 与发布版本不符{"ok":0,"err":2,"msg":"missing product"}
3产品不存在(目录名不匹配,含编码不一致){"ok":0,"err":3,"msg":"unknown product 'X'"}
4产品 OTA_ENABLE=0{"ok":0,"err":4,…}
5固件文件缺失 / 超大小护栏被拒{"ok":0,"err":5,"msg":"firmware file missing: …"}
6trans:"D" 但 cv 对应不上任何补丁基线{"ok":0,"err":6,…}
7维护中(写锁)时收到非 check 的动作{"ok":0,"err":7,…}
8会话不存在 / 已过期 / 已关闭;缺 sid{"ok":0,"err":8,"msg":"session not found"}
9off 越界(保留码):当前实现把 off ≥ size 视为「已完成」,回 {"ok":1,"eof":1},该码暂未在 TCP 通道使用—(保留)
10灰度开启且 did 不在灰度池{"ok":0,"err":10,"msg":"device not in gray pool"}
11文件 IO 失败:读固件 / 日志失败,日志超 60 KB{"ok":0,"err":11,…}
12已是最新(内部语义标记)check 线上表现为 {"ok":1,"upd":0,"nonew":1}
13并发下载会话达上限(64){"ok":0,"err":13,"msg":"too many sessions (64)"}
14客户端版本低于产品 MIN_CLIENT_VERSION,不支持差分{"ok":0,"err":14,…}

六、分片与传输参数

1、钳制规则与优先级

客户端传来的 chunk 一律钳制到本协议区间;生效值的优先级(高 → 低):

  1. start 请求里的 chunk
  2. product.ini 的 CHUNK_SIZE
  3. [transfer.tcp] DEFAULT_CHUNK_SIZE
  4. [transfer] DEFAULT_CHUNK_SIZE(公共默认)

2、TCP 通道参数

配置项默认说明
DEFAULT_CHUNK_SIZE8192客户端未指定时使用;8 KB 让 1 MB 固件约 128 次往返
MIN/MAX_CHUNK_SIZE128 / 65535钳制区间(太小往返多,太大客户端缓冲吃紧)
MAX_BATCH16单次 pull 最多几片(16 × 8KB = 128 KB / 次)
SEND_RETRY2发送失败重试次数
IDLE_TIMEOUT_SEC300mux ↔ 服务连接的空闲回收(≠ 下载会话空闲 600 秒)
MAX_CONNS64本协议最大并发连接数

3、文本模式片数反推

bin=0 时每片以 hex 内嵌 JSON(体积 ×2 加包装),因此按消息上限反推安全片数:

/* 每片约占用 chunk*2 + 64 字节,留出 JSON 包装余量 */
int per          = chunk * 2 + 64;
int max_by_size  = (TCP_OTA_MSG_MAX - 256) / per;   /* TCP_OTA_MSG_MAX = 64 KB */
if (batch > max_by_size) batch = max_by_size;
建议 引导客户端用二进制模式(bin=1):文本模式只适合小补丁或调试, 且要求客户端自己解 hex。

4、发送队列与 again

  • 每连接一个发送队列(512 槽);二进制模式下每片占一个槽,元信息 JSON 还要占 1 格;
  • 可用槽位不足时不裁剪 cnt 偷发一部分,而是整体回 again=1 —— 保证「声明 N 片就一定发 N 片」;
  • 客户端按 off 不变、退避 50 ms 重发即可(见 客户端手册 · 流控信号 again)。
不要用「少发几片」实现流控 实测踩过的坑:JSON 说 cnt=N 但实际只入队 M<N 片,客户端会把下一个 长度 0 的帧当成数据片,报「分片长度不符」整包失败。全有或全无是硬要求。

5、限速与传输节奏

  • 下载限速在 [throttle] 配置:分「空闲时段 / 拥挤时段」两档速率 (默认 00:00~17:00 = 8 Mbps,17:00~24:00 = 2 Mbps);
  • PER_CONN=0 表示所有连接共享一份速率(保服务器出口), =1 表示每连接独立;
  • 限速只影响发送节奏,不影响正确性:表现为客户端更频繁地收到 again, 不会出现数据缺口。

七、会话与资源管理

1、会话生命周期

  • start 创建下载会话(分配 sid);
  • pull / finish 都会刷新活动时间;
  • 空闲超过 600 秒(SESSION_IDLE_SEC)自动回收并记日志;
  • finish / abort 立即释放;服务重启后会话全部丢失 (客户端需重新 start,check 结果仍有效)。

2、上限与回收

项值说明
并发下载会话表64引擎级(OTA_SESSION_MAX),满 → err=13
本协议最大连接数64[transfer.tcp] MAX_CONNS
连接空闲回收300 sIDLE_TIMEOUT_SEC,回收的是 mux ↔ 服务的连接
文件描述符—单线程 epoll,连接上限即 fd 用量上限

3、内存与并发

  • 固件用 mmap 共享映射:N 台设备同时下载同一固件只占一份物理内存;
  • MD5 带缓存(按 mtime + size 失效,条目上限 256),避免每个 check 都重算大文件;
  • 下载走 writev(mmap),不做编码与复制 —— 二进制模式的大流量路径零拷贝。

八、固件库与产品名(服务端视角)

1、产品名匹配

  • product = 固件库目录名,按字节精确比对(区分大小写);
  • 名字缓冲 FW_NAME_MAX = 128 → 上限 127 字节;
  • 允许中文;客户端发 GBK 字节时会自动归一化成 UTF-8 再比一次(老设备兼容), 两种编码指向同一份固件;
  • 目录组织、补丁命名、发布流程见 ota/docs/固件管理.md。

2、版本与差分基线

  • 版本号按 X.Y.Z 解析(各段 1~4 位数字,可带 v 前缀); 解析失败的 cv 一律签名为「不支持差分」(不下发补丁);
  • 补丁按 cv → 当前发布版本 查找;找到才给 delta=1; 找不到就把客户端要的差分拒掉(err=6),客户端回退全量;
  • 校验基准始终是目标完整包 MD5:全量时它等于文件 MD5,差分时用于校验合成产物。

3、灰度与写锁

开关位置效果
OTA_ENABLEproduct.ini0 → 该产品 err=4(客户端应静默跳过)
GRAY_ENABLE / GRAY_ID_POOLproduct.ini只对池内 did 开放,未命中 → err=10
write_busyfirmware/WRITE_LOCK.ini维护中:check → busy=1+upd=0;其它动作 → err=7
MAX_FILE_SIZE_MB服务端配置超过护栏(默认 300 MB)的文件不参与 OTA,按「固件缺失」处理
为什么 did 必须传 灰度按设备唯一 ID 命中;客户端不传 did 时,灰度开启的产品一律视为未命中。 灰度池为空表示「不限制」还是「全不放行」由产品配置决定,发布前务必在面板上验证一次。

九、运维与排障

1、与协议相关的配置项

段配置项影响
[server]LISTEN_PORT / LISTEN_HOST监听端口须与 mux 的转发目标一致(默认 8011);改端口要重启
[transfer.tcp]分片 / 批 / 重试 / 空闲 / 连接数见 六、2;热更新生效
[proto]PROTO_VERhello 下发的 pver
[proto]ENABLE_BINARY_CHUNK关掉则一律文本模式且不下发差分
[firmware]ROOT / LOCK_FILE / SCAN_INTERVAL_SEC / ENABLE_MMAP固件库位置、写锁、扫描冷却、共享映射
[throttle]时段与速率、PER_CONN下载限速,见 六、5

2、自检与日志

cd /home/user/install/ota

./ota_server config/ota_config.ini --check-config   # 打印生效配置 + 固件库全貌(版本/大小/MD5/补丁)
python3 scripts/test_ota_e2e.py --via-mux           # 经真实链路端到端自测
python3 scripts/verify_real.py --via-mux            # 真实大包 + 官方差分补丁验证
  • 日志关键字:check:(版本判定 / 差分选择)、start:(会话与传输方式)、 pull 相关(队列满 → 退避)、客户端上报(report 内容);
  • 响应里的 msg 与日志同源,排障时先看 err 再看日志里的产品名与版本号。

3、常见故障对照表

现象原因 / 处理
客户端拿到 err=3目录名与请求的 product 是否完全一致(含大小写、编码)
客户端拿到 err=5 / 自检显示「固件缺失」TARGET_FW_NAME 与实际文件名是否一致;文件是否超大小护栏
客户端拿到 err=6补丁文件名基线版本与客户端当前版本不匹配(make_patch.py --list 查看)
客户端校验产物 MD5 失败日志里找「产物 MD5 与官方包不一致」:差分基线不匹配或旧包被改过
多设备并发时数据串台回包是否逐请求回显本请求的 sid(见 一、2)
改配置不生效是否设了 write_busy = 1(会暂停热加载检查)
大量 err=13会话泄漏:客户端没 finish / abort; 或并发设备数确实超过 64(需扩容会话表)
客户端频繁收到 again发送队列吃紧或限速生效:属正常流控,客户端退避重发即可

十、相关文档

文档给谁看 / 内容
TCP OTA 客户端接入手册设备端接入:帧格式、动作字段、状态码、升级流程、Python 参考客户端
ota/docs/README.md服务端架构、完整配置说明、编译与 systemd 运维
ota/docs/固件管理.md面向运维:固件目录组织、发布流程、差分补丁规则、故障排查
ota/docs/TCP OTA 协议与接入开发文档.md协议 md 源文档(客户端手册的内容来源)