分片传输
SLE SSAP 通知/写入、MTU 配置、应用层分片协议
前置阅读:Hello Notify、高吞吐传输
学习目标
- 理解数据分片的必要性——单次
ssaps_notify_indicate()能发送的数据受 MTU 限制,大文件/日志/配置表必须分片 - 掌握分片大小的计算方法(MTU − 协议头开销 = 有效载荷)
- 掌握序号 + ACK 的分片可靠传输协议设计与实现
- 理解超时重传和数据重组逻辑,处理丢包与重复包
- 能够在 WS63 上实现 50KB+ 的可靠大数据块传输
基本概念
为什么需要分片
SLE 单次 SSAP 发送受 MTU 限制(典型值 520 字节)。当传输配置文件(100KB)、日志导出(100KB+)或固件差分包(500KB+)时,必须将数据切分为多个小于 MTU 的分片,逐个发送并在接收端按序号重组。
典型使用场景
| 场景 | 数据量 | 分片数 | 可靠性要求 |
|---|---|---|---|
| 日志导出 | 100KB | ~194 片 | 低(丢几行不影响分析) |
| 配置文件下发 | 50KB | ~97 片 | 高(差一个字节 JSON 解析失败) |
| 固件差分更新 | 500KB | ~970 片 | 极高(差一个 bit 变砖) |
| 密钥/证书下发 | 2KB | ~4 片 | 极高(安全敏感数据) |
分片大小计算
分片大小不是拍脑袋定的,它由当前连接的 MTU 减去应用层协议头开销得出:
有效载荷/片 = MTU − 分片协议头
= 520 − 3 (type=1B + total/seq=2B)
= 517 字节
分片总数 = 向上取整(文件总大小 / 517)
50KB → 97 片 | 100KB → 194 片 | 500KB → 970 片
硬编码
FRAG_PAYLOAD = 516的前提是 MTU 固定为 520。正式产品建议通过sle_set_data_len()查询当前连接的实际 MTU,再动态计算分片大小——MTU 协商结果可能因对端能力而变化。
可靠传输 vs 流式传输
分片传输有两种策略,选择取决于业务对完整性的要求:
| 对比维度 | 可靠传输(本案例) | 流式传输 |
|---|---|---|
| ACK 机制 | 每片确认 | 不确认 |
| 丢片处理 | 超时重传(最多 3 次) | 忽略,继续发下一片 |
| 完整性校验 | CRC32 端到端 | 不校验 |
| 传输效率 | 较低(等待 ACK 增加延迟) | 较高(连续发送无等待) |
| 典型场景 | 配置文件、固件、密钥 | 日志导出、流媒体、调试输出 |
流式传输在发送方算好分片后直接用
ssapc_write_cmd连续发出,不等待任何 ACK。接收方按序号写入即可,丢片位置填 0 或跳过。
分片协议头设计
| 偏移 0 1 字节 |
偏移 1 ~ 2 2 字节 |
偏移 3 ~ (MTU - 1) 0 ~ (MTU - 3) 字节 |
|
type — 分片类型 0x01 = START(起始分片) 0x02 = DATA(数据分片) 0x03 = END(结束分片) 0x04 = ACK(确认应答) |
START 时:total — 总分片数 取值范围 1 ~ 65535 其他时:seq — 分片序号 从 0 开始递增,取值范围 0 ~ (total - 1) 偏移 1~2 根据 type 复用: START 存 total,DATA/END/ACK 存 seq |
data — 有效载荷 最大长度 = MTU - 3(协议头固定占 3 字节) ACK 包不携带 data MTU 为当前连接协商的最大传输单元 |
total为 2 字节,最大可分 65535 片。以每片 513 字节计,单次传输上限约 32MB。超过此上限时需分层分片(先发索引表再发数据分片)。
分片传输正常流程
sequenceDiagram
participant C as Client发送方
participant S as Server接收方
C->>S: type=START seq=0 total=97
Note over S: 创建缓冲区 malloc total*516
S->>C: type=ACK seq=0
loop 逐片发送 seq=1..95
C->>S: type=DATA seq=N data[516B]
Note over C: 启动 500ms 超时定时器
S->>C: type=ACK seq=N
Note over C: 停止定时器 准备下一片
end
C->>S: type=END seq=96 data[剩余B]
Note over S: CRC32 校验完整文件
alt 校验通过
S->>C: 校验通过 写入 NV
else 校验失败
S->>C: 校验失败 请求重传
end
超时重传与异常处理
sequenceDiagram
participant C as Client发送方
participant S as Server接收方
C->>S: type=DATA seq=7 data[516B]
Note over C: 启动 500ms 超时定时器
S--xC: ACK 丢失 空中丢包
Note over C: 500ms 超时触发 重传1
C->>S: type=DATA seq=7 data[516B]
Note over C: 重启 500ms 超时定时器
Note over C: 再次超时 重传2
C->>S: type=DATA seq=7 data[516B]
Note over C: 重启 500ms 超时定时器
Note over C: 第3次超时 重传3
C->>S: type=DATA seq=7 data[516B]
Note over S: 收到重复seq 直接回复ACK
S->>C: type=ACK seq=7
Note over C: 收到ACK 发下一片 seq=8
重传 3 次仍超时 → 认为连接异常 → 调用
sle_disconnect_remote_device()断开 → 清理发送缓冲区和定时器资源 → 上报传输失败。
涉及 API
| API | 谁调用 | 用途 |
|---|---|---|
sle_set_data_len(conn_id, tx_octets) |
发送方 | 设置/查询当前连接的最大发送载荷(MTU) |
ssaps_set_info(server_id, &info) |
Server | 在注册服务时设置 MTU、UUID 等信息 |
ssaps_notify_indicate(conn_id, handle, data, len) |
Server | 向 Client 推送分片数据或 ACK |
ssapc_write_req(conn_id, handle, data, len) |
Client | 发送分片数据(带协议栈确认,较慢) |
ssapc_write_cmd(conn_id, handle, data, len) |
Client | 快速发送数据片(无协议栈确认,可靠性由自定义 ACK 保证) |
osal_timer_start(timer_id, timeout_ms) |
发送方 | 启动超时重传定时器 |
osal_timer_stop(timer_id) |
发送方 | 收到 ACK 后停止定时器 |
本案例选
ssapc_write_cmd而非ssapc_write_req:分片需要高频发送,write_cmd跳过协议栈层确认可减少延迟。可靠性由应用层自定义 ACK + 重传协议保证。
案例说明
整体目标
Client 将 50KB 配置文件通过分片传输到 Server → Server 逐片接收并写入缓冲区 → 全部接收完成后 CRC32 校验 → 校验通过后写入 NV 持久化存储。传输过程采用可靠传输模式——每片 ACK,丢片超时 500ms 重传,最多重传 3 次。超限则终止传输并上报失败。
案例流程架构
flowchart TD
A[Client读取配置文件50KB] --> B[计算分片总数 total=97]
B --> C[发送START分片 seq=0]
C --> D[Server创建接收缓冲区]
D --> E[Server回复ACK seq=0]
E --> F{seq 小于 total?}
F -->|是| G[Client发送DATA分片 seq]
G --> H[Server 500ms内回复ACK?]
H -->|是| I[seq自增 继续下一片]
I --> F
H -->|否| J{重传次数 小于 3?}
J -->|是| G
J -->|否| K[传输失败 断开连接]
F -->|否| L[Client发送END分片]
L --> M[Server CRC32校验]
M -->|通过| N[写入NV持久化]
M -->|失败| O[请求重传整个文件]
与高吞吐传输的对比
| 对比维度 | 分片传输(本案例) | 高吞吐传输 |
|---|---|---|
| 核心目标 | 可靠性——数据不丢不错 | 速率——最大吞吐量 |
| 发送策略 | 逐片 ACK 后发下一片 | 连续发送 + QoS 流控 |
| 丢包处理 | 超时重传(最多 3 次) | 依赖底层自动重传 |
| 完整性 | CRC32 端到端校验 | 依赖底层 CRC |
| 单包延迟 | 较高(等 ACK 往返) | 较低(连续发送) |
| 适用场景 | 配置文件、固件、密钥 | 日志导出、流媒体 |
案例操作指导
硬件准备
- 2 块 WS63 开发板:一块作 Client 发送方,一块作 Server 接收方
- 2 条 USB 转串口线:供电 + 查看串口日志(波特率 921600)
- 测试数据:事先将 50KB 配置文件烧录到 Client 端 Flash 固定分区
操作步骤
- 编译烧录:分别编译
fragmentation_client和fragmentation_server示例,烧录到两块开发板 - 上电观察:两块开发板上电,Client 自动扫描并连接 Server
- 触发传输:Client 连接成功后按按键触发分片传输(或上电自动触发)
- 观察进度:通过串口日志观察传输进度——当前分片序号、ACK 状态、重传次数
- 验证结果:传输完成后,Server 打印 CRC32 校验结果和写入 NV 状态
预期日志
[SLE Client] 开始分片传输 文件大小=51200B 总分片数=97
[SLE Client] 发送 START 分片 seq=0 total=97
[SLE Client] 收到 ACK seq=0
[SLE Client] 发送 DATA 分片 seq=1
[SLE Client] 收到 ACK seq=1
...
[SLE Client] 发送 DATA 分片 seq=95
[SLE Client] 收到 ACK seq=95
[SLE Client] 发送 END 分片 seq=96
[SLE Server] 接收完成 总字节=51200B
[SLE Server] CRC32 校验通过: 0xA1B2C3D4
[SLE Server] 写入 NV 成功
[SLE Client] 分片传输完成
关键配置
| 参数 | 推荐值 | 说明 |
|---|---|---|
FRAG_PAYLOAD |
516B | MTU 520 − 4B 协议头;若 MTU 协商为其他值需动态调整 |
FRAG_TIMEOUT_MS |
500ms | ACK 等待超时;太长降低效率(等 500ms 才重传),太短导致误判重传 |
FRAG_MAX_RETRY |
3 次 | 超限则终止传输并上报失败 |
CRC_TYPE |
CRC32 | 4 字节校验值,端到端数据完整性保证 |
FRAG_START_TIMEOUT_MS |
2000ms | START 包超时可适当放宽,接收方分配缓冲区需要时间 |
分片大小不要贪大——516 字节是 MTU 520 下的最优解。设置 518~520 会导致协议头挤占到数据区域之外,SDK 底层可能拒绝发送或截断。
代码详解
1. 分片协议头定义与打包/解析
/* ── 分片协议常量 ── */
#define FRAG_TYPE_START 0x01
#define FRAG_TYPE_DATA 0x02
#define FRAG_TYPE_END 0x03
#define FRAG_TYPE_ACK 0x04
#define FRAG_HEADER_SIZE 4 /* type(1) + seq(2) + total(1) */
#define FRAG_MTU 520
#define FRAG_PAYLOAD (FRAG_MTU - FRAG_HEADER_SIZE) /* 516B */
/* ── 分片包结构 ── */
typedef struct {
uint8_t type; /* 0x01=START 0x02=DATA 0x03=END 0x04=ACK */
uint16_t seq; /* 分片序号 从0递增 */
uint8_t total; /* 总分片数 仅type=0x01时有效 */
uint8_t data[FRAG_PAYLOAD];
} frag_pkt_t;
/* ── 打包发送分片 ── */
static errcode_t frag_send_packet(uint16_t conn_id, uint16_t handle,
uint8_t type, uint16_t seq, uint8_t total,
const uint8_t *data, uint16_t data_len)
{
frag_pkt_t pkt;
pkt.type = type;
pkt.seq = seq;
pkt.total = total;
uint16_t pkt_len = FRAG_HEADER_SIZE;
if (data != NULL && data_len > 0) {
if (memcpy_s(pkt.data, FRAG_PAYLOAD, data, data_len) != EOK) {
return ERRCODE_FAIL;
}
pkt_len += data_len;
}
return ssapc_write_cmd(conn_id, handle, (uint8_t *)&pkt, pkt_len);
}
2. 发送方分片循环
/* ── 发送方全局状态 ── */
static uint8_t *g_send_file_buf = NULL; /* 待发送文件缓冲区 */
static uint32_t g_send_file_len = 0; /* 文件总长度 */
static uint16_t g_send_seq = 0; /* 当前分片序号 */
static uint8_t g_send_total = 0; /* 总分片数 */
static uint8_t g_retry_count = 0; /* 当前分片重传次数 */
static struct osal_timer *g_frag_timer = NULL; /* 超时重传定时器 */
/* ── 发送下一片 ── */
static void frag_send_next(uint16_t conn_id, uint16_t handle)
{
uint32_t offset = g_send_seq * FRAG_PAYLOAD;
uint16_t chunk = (offset + FRAG_PAYLOAD <= g_send_file_len)
? FRAG_PAYLOAD
: (uint16_t)(g_send_file_len - offset);
if (g_send_seq + 1 >= g_send_total) {
/* 最后一片:type=END */
frag_send_packet(conn_id, handle, FRAG_TYPE_END,
g_send_seq, 0, g_send_file_buf + offset, chunk);
} else {
frag_send_packet(conn_id, handle, FRAG_TYPE_DATA,
g_send_seq, 0, g_send_file_buf + offset, chunk);
}
g_retry_count = 0;
osal_timer_start(g_frag_timer, 500); /* 启动500ms超时 */
}
/* ── 启动分片传输 ── */
errcode_t frag_start_transfer(uint16_t conn_id, uint16_t handle,
const uint8_t *file_data, uint32_t file_len)
{
/* 分配发送缓冲区并拷贝文件数据 */
g_send_file_buf = malloc(file_len);
if (g_send_file_buf == NULL) { return ERRCODE_MALLOC; }
memcpy_s(g_send_file_buf, file_len, file_data, file_len);
g_send_file_len = file_len;
/* 计算分片数 */
g_send_total = (uint8_t)((file_len + FRAG_PAYLOAD - 1) / FRAG_PAYLOAD);
g_send_seq = 0;
osal_printk("开始分片传输 文件大小=%uB 总分片数=%u\r\n",
file_len, g_send_total);
/* 发送 START 分片 */
frag_send_packet(conn_id, handle, FRAG_TYPE_START, 0, g_send_total, NULL, 0);
osal_timer_start(g_frag_timer, 2000); /* START包超时放宽到2000ms */
return ERRCODE_SUCC;
}
3. 接收方顺序接收、重组与 CRC 校验
/* ── 接收方全局状态 ── */
static uint8_t *g_rx_file_buf = NULL; /* 接收缓冲区 */
static uint32_t g_rx_file_len = 0; /* 已接收字节数 */
static uint8_t g_rx_total = 0; /* 总分片数 */
static uint32_t g_rx_crc_expected = 0; /* 预期 CRC32(随START带下来或预先约定) */
/* ── 接收分片回调 ── */
static void on_frag_received(uint16_t conn_id, uint16_t handle,
const uint8_t *data, uint16_t len)
{
if (len < FRAG_HEADER_SIZE) { return; }
const frag_pkt_t *pkt = (const frag_pkt_t *)data;
osal_printk("收到分片 type=0x%02X seq=%u\r\n", pkt->type, pkt->seq);
switch (pkt->type) {
case FRAG_TYPE_START:
/* 创建接收缓冲区 */
g_rx_total = pkt->total;
g_rx_file_len = (uint32_t)g_rx_total * FRAG_PAYLOAD;
g_rx_file_buf = malloc(g_rx_file_len);
if (g_rx_file_buf == NULL) {
osal_printk("分配接收缓冲区失败\r\n");
return;
}
memset_s(g_rx_file_buf, g_rx_file_len, 0, g_rx_file_len);
/* 回复 ACK */
frag_send_packet(conn_id, handle, FRAG_TYPE_ACK, pkt->seq, 0, NULL, 0);
break;
case FRAG_TYPE_DATA:
/* 按 seq 计算偏移写入缓冲区 */
{
uint32_t offset = (uint32_t)pkt->seq * FRAG_PAYLOAD;
uint16_t data_len = len - FRAG_HEADER_SIZE;
memcpy_s(g_rx_file_buf + offset,
g_rx_file_len - offset, pkt->data, data_len);
frag_send_packet(conn_id, handle, FRAG_TYPE_ACK, pkt->seq, 0, NULL, 0);
}
break;
case FRAG_TYPE_END:
/* 写入最后一片数据 + CRC32 校验 */
{
uint32_t offset = (uint32_t)pkt->seq * FRAG_PAYLOAD;
uint16_t data_len = len - FRAG_HEADER_SIZE;
memcpy_s(g_rx_file_buf + offset,
g_rx_file_len - offset, pkt->data, data_len);
uint32_t actual_len = offset + data_len;
uint32_t crc = crc32_calc(g_rx_file_buf, actual_len);
osal_printk("接收完成 总字节=%u CRC32=0x%08X\r\n", actual_len, crc);
if (crc == g_rx_crc_expected) {
/* 写入 NV 持久化 */
uapi_nv_write(NV_ID_CONFIG_FILE, g_rx_file_buf, actual_len);
osal_printk("CRC32 校验通过 写入 NV 成功\r\n");
/* 回复成功 */
frag_send_packet(conn_id, handle, FRAG_TYPE_ACK, pkt->seq, 0, NULL, 0);
} else {
osal_printk("CRC32 校验失败 请求重传\r\n");
/* 通知对端重传整个文件 */
}
free(g_rx_file_buf);
g_rx_file_buf = NULL;
}
break;
default:
break;
}
}
4. 超时重传与错误处理
/* ── 超时重传定时器回调 ── */
static void frag_timeout_cb(uintptr_t arg)
{
uint16_t conn_id = (uint16_t)(arg & 0xFFFF);
uint16_t handle = (uint16_t)(arg >> 16);
g_retry_count++;
if (g_retry_count > 3) {
/* 超过最大重传次数 终止传输 */
osal_printk("分片 seq=%u 重传%d次失败 终止传输\r\n",
g_send_seq, g_retry_count - 1);
free(g_send_file_buf);
g_send_file_buf = NULL;
sle_disconnect_remote_device(&g_remote_addr);
return;
}
osal_printk("分片 seq=%u 超时 重传%d 次\r\n", g_send_seq, g_retry_count);
/* 重发当前分片——seq 不变 */
uint32_t offset = (uint32_t)g_send_seq * FRAG_PAYLOAD;
uint16_t chunk = (offset + FRAG_PAYLOAD <= g_send_file_len)
? FRAG_PAYLOAD
: (uint16_t)(g_send_file_len - offset);
uint8_t type = (g_send_seq + 1 >= g_send_total) ? FRAG_TYPE_END : FRAG_TYPE_DATA;
frag_send_packet(conn_id, handle, type, g_send_seq, 0,
g_send_file_buf + offset, chunk);
osal_timer_start(g_frag_timer, 500);
}
/* ── 收到 ACK 后的处理 ── */
static void frag_on_ack_received(uint16_t conn_id, uint16_t handle,
uint16_t ack_seq)
{
osal_timer_stop(g_frag_timer); /* 停止超时定时器 */
if (ack_seq != g_send_seq) {
/* ACK 序号不匹配 忽略(可能是迟到的旧ACK) */
osal_printk("忽略过期ACK seq=%u 当前seq=%u\r\n", ack_seq, g_send_seq);
return;
}
/* 判断是否发完所有分片 */
if (g_send_seq + 1 >= g_send_total) {
osal_printk("分片传输完成 总 %u 片\r\n", g_send_total);
free(g_send_file_buf);
g_send_file_buf = NULL;
return;
}
/* 发下一片 */
g_send_seq++;
frag_send_next(conn_id, handle);
}
接收方收到重复 seq 的分片时,不需要重复写入缓冲区——直接回复 ACK 即可(说明对端的 ACK 丢了,但数据已经正确接收)。