分片传输
使用技术:SLE、SSAP 属性读写、Notification、应用层分片与顺序重组、累加校验和
前置阅读:必须了解 Hello SLE 的扫描与连接流程,建议先完成 Hello Notify 的 Notification 实验。
本案例使用两块 WS53 演示固定长度的大数据传输:Client 通过 SSAP 写请求触发传输;Server 将 1024 字节测试数据拆成 6 个 Notification;Client 按分片序号顺序追加负载,并在最后一片到达后检查总长度和校验和。
学习目标
- 理解 SSAP MTU、分片头、单片负载和原始数据长度之间的关系。
- 掌握 Magic、传输 ID、片序号、总片数、负载长度和校验和的作用。
- 理解 WS53 当前实现是“严格顺序追加”,不是“携带偏移的乱序重组”。
- 掌握 Client 收包前的头部、长度、序号和缓冲区边界检查。
- 区分“触发请求已接受”“Server 已发送完成”和“Client 重组校验通过”三个验收层次。
- 了解 Notification 不提供应用层逐片确认,以及当前案例在丢片、乱序和并发传输方面的限制。
基本概念
MTU 与有效负载
SSAP MTU 限制一次属性数据可承载的最大规模。本案例双方请求 MTU 520,但没有把 520 字节全部作为业务负载,而是把每片业务负载固定为 180 字节,并在前面增加分片头。
在当前 WS53 工具链下,offsetof(sle_fragmentation_packet_t, payload) 为 16,因此:
| 分片 | 业务负载 | 实际属性数据长度 |
|---|---|---|
| 第 1~5 片 | 180 字节 | 16 + 180 = 196 字节 |
| 第 6 片 | 124 字节 | 16 + 124 = 140 字节 |
最大属性数据长度 196 字节,小于请求的 MTU 520。MTU 是链路允许值,180 字节是本案例自行选择的应用层分片负载,两者不能混为同一个参数。
分片与重组
总片数使用向上取整公式计算:
Server 通过 offset = index × 180 从源缓冲区取出当前片,但这个 offset 只存在于 Server 局部计算中,并没有写进分片头。Client 要求收到的 index 必须等于 g_next_fragment,校验通过后把负载追加到 g_reassembly_buffer[g_reassembly_length]。
因此,当前实现只能重组从 0 开始、严格连续到达的分片,不具备按偏移接收乱序分片的能力。
分片元数据与接收状态
公共协议结构定义如下:
typedef struct {
uint16_t magic;
uint16_t transfer_id;
uint16_t index;
uint16_t total;
uint16_t payload_len;
uint16_t reserved;
uint32_t checksum;
uint8_t payload[SLE_FRAGMENTATION_PAYLOAD_SIZE];
} sle_fragmentation_packet_t;
当前 16 字节头的字段为:
| 字段 | 长度 | WS53 当前值或作用 |
|---|---|---|
magic |
2 | 固定为 0x5346,识别分片协议 |
transfer_id |
2 | 固定为 1,识别传输会话 |
index |
2 | 从 0 开始的片序号 |
total |
2 | 固定为 6 |
payload_len |
2 | 当前片有效负载长度,最大 180 |
reserved |
2 | 保留字段;当前发送为 0,Client 不检查 |
checksum |
4 | 完整 1024 字节源数据的累加和 |
需要特别注意:WS53 当前协议头没有 offset 字段。源码 README 中“分片头携带偏移”“按偏移重组”的描述与实际 sle_fragmentation_protocol.h、Client 实现不一致;本文以实际 C 源码为准,按 reserved 字段和严格顺序追加说明。
Client 使用两个全局状态量:
g_reassembly_length:已经追加的有效字节数。g_next_fragment:下一片必须匹配的序号。
收到 index == 0 的分片时,Client 无条件把两者清零,开始或重新开始一次传输。
Notification 与应用层可靠性
Notification 适合连续上报,不要求对端逐片返回应用确认。本案例每发送一片等待 20 ms,目的是降低连续提交速度,但固定等待不等于可靠传输机制。
当前协议没有:
- 分片 ACK/NACK;
- 接收位图;
- 丢片超时;
- 指定分片重传;
- 断点续传;
- 完成确认。
因此,Server 输出 transfer complete 只表示 6 次本地发送调用均返回成功,不能证明 Client 已经收齐并通过校验。
完整性检查不等于可靠传输
长度和校验值用于判断重组结果是否完整、内容是否变化,但不能自动恢复丢失或错误的数据。简单累加和实现成本低,但错误检测能力有限;CRC 更适合检测传输错误,带密钥的消息认证码则用于验证数据来源和防止篡改。
可靠传输解决“数据出错后怎样恢复”,完整性检查解决“怎样发现数据不完整或已变化”,两者不能互相替代。
涉及 API
API 按实际调用阶段排列。详细参数和返回值请查阅对应 API Reference。
| 阶段 | 前置状态 | 核心 API | 调用方 | 作用 |
|---|---|---|---|---|
| Server 初始化 | 案例任务启动 | ssaps_register_callbacks()、ssaps_register_server()、ssaps_add_service_sync()、ssaps_add_property_sync() |
Server | 注册读、写和 Notify 属性 |
| 扫描与连接 | SLE 已使能 | sle_start_seek()、sle_connect_remote_device()、sle_pair_remote_device() |
Client | 查找 fragment_server 并完成配对 |
| MTU 与发现 | 配对成功 | ssapc_exchange_info_req()、ssapc_find_structure() |
Client | 请求 MTU 520 并取得属性 handle |
| 传输前读取 | 服务发现完成 | ssapc_read_req()、ssaps_send_response() |
Client / Server | 确认属性读流程成功;读取数据本身不参与分片协议 |
| 触发传输 | 读确认成功 | ssapc_write_req()、ssaps_send_response() |
Client / Server | 写入单字节 0x01 并返回触发状态 |
| 创建发送任务 | 触发值合法 | osal_kthread_create() |
Server | 创建 SLEFragmentTx 任务 |
| 发送分片 | 发送任务运行 | ssaps_notify_indicate() |
Server | 发送单个 Notification 分片 |
| 片间节流 | 单片发送成功 | osal_msleep() |
Server | 相邻分片之间等待 20 ms |
案例说明
功能规格
| 规格项 | WS53 源码值 |
|---|---|
| Server 广播名称 | fragment_server |
| 服务 UUID | 0x3333 |
| 属性 UUID | 0x3434 |
| 触发命令 | 单字节 0x01 |
| 原始数据长度 | 1024 字节 |
| 单片最大负载 | 180 字节 |
| 当前头长度 | 16 字节 |
| 最大/最小属性数据长度 | 196 / 140 字节 |
| 总片数 | 6 |
| MTU 请求值 | 520 字节,版本 1 |
| Magic | 0x5346 |
| Transfer ID | 1 |
| 片间等待 | 20 ms |
| 发送任务 | 优先级 27,栈大小 0x1000 |
公共分片结构体
Client 和 Server 共用 src/application/samples/bt/sle/sle_fragmentation/sle_fragmentation_protocol.h。总片数由公共常量计算:
#define SLE_FRAGMENTATION_DATA_SIZE 1024
#define SLE_FRAGMENTATION_PAYLOAD_SIZE 180
#define SLE_FRAGMENTATION_TOTAL_FRAGMENTS \
((SLE_FRAGMENTATION_DATA_SIZE + SLE_FRAGMENTATION_PAYLOAD_SIZE - 1) / \
SLE_FRAGMENTATION_PAYLOAD_SIZE)
Server 不发送整个 sle_fragmentation_packet_t 的固定大小,而是根据当前有效负载计算实际长度:
uint16_t packet_len =
(uint16_t)(offsetof(sle_fragmentation_packet_t, payload) +
packet.payload_len);
这样最后一片只发送 124 字节有效负载,不会发送 payload[180] 中剩余的未使用空间。
案例直接传输 C 结构体头,并在 Client 侧把接收缓冲区转换为结构体指针。跨编译器或跨架构使用时,应显式定义字节序、头长度、字段偏移和对齐方式,不能依赖当前编译器的结构体布局。
端到端交互流程
sequenceDiagram
participant S as WS53 Server
participant C as WS53 Client
S->>S: 注册服务并广播 fragment_server
C->>S: 扫描、连接、配对、MTU 交换、属性发现
C->>S: Read Request
S-->>C: 8 字节属性初值
C->>C: Read Confirm 成功
C->>S: Write Request:0x01
S-->>C: Write Response:成功
S->>S: 创建 SLEFragmentTx 任务
loop index = 0...5
S->>C: Notification:16 字节头 + 当前负载
S->>S: 等待 20 ms
end
C->>C: 严格检查序号并顺序追加
C->>C: 第 6 片后检查 1024 字节和 checksum
实际流程中,Client 不是在服务发现完成后直接写入 0x01:它先读取属性;只有 read_cfm_cb 收到成功状态才调用 sle_fragmentation_client_send_write_req()。Server 返回的 8 字节属性初值不参与后续分片重组,这个 Read Request 主要充当传输前的属性可读性和流程就绪检查。
三个验收层次
| 层次 | 关键日志 | 能证明什么 |
|---|---|---|
| 触发请求接受 | transfer request accepted |
Server 接受了单字节 0x01 的写请求 |
| Server 发送完成 | transfer complete |
6 次本地 Notification 发送调用均返回成功 |
| Client 重组通过 | reassembly complete 和 test passed |
Client 按序收到 6 片,总长度为 1024,最终累加和匹配 |
触发请求响应在创建发送任务之前返回,而且 sle_fragmentation_start_transfer() 不向写回调报告任务创建失败。因此 transfer request accepted 不能证明发送任务已经成功运行。最终功能验收必须以 Client 的 test passed 为准。
案例操作指导
准备开发板
准备两块 WS53 开发板和两个调试串口,分别作为 Server 和 Client。本案例不需要外接硬件。
配置、构建并烧录 Server
在 SDK 根目录执行:
fbb config set CONFIG_SAMPLE_SUPPORT_SLE_FRAGMENTATION_SERVER_SAMPLE=y --target ws53_liteos_app
fbb build ws53_liteos_app --clean
fbb flash ws53_liteos_app --port <SERVER_COM> --json-summary
对应的 Kconfig 选择链为:
CONFIG_ENABLE_BT_SAMPLE=y
CONFIG_SAMPLE_SUPPORT_SLE_SAMPLE=y
CONFIG_SAMPLE_SUPPORT_SLE_FRAGMENTATION_SERVER_SAMPLE=y
配置、构建并烧录 Client
Server 烧录完成后,切换到 Client 角色并重新构建:
fbb config set CONFIG_SAMPLE_SUPPORT_SLE_FRAGMENTATION_CLIENT_SAMPLE=y --target ws53_liteos_app
fbb build ws53_liteos_app --clean
fbb flash ws53_liteos_app --port <CLIENT_COM> --json-summary
对应的 Kconfig 选择链为:
CONFIG_ENABLE_BT_SAMPLE=y
CONFIG_SAMPLE_SUPPORT_SLE_SAMPLE=y
CONFIG_SAMPLE_SUPPORT_SLE_FRAGMENTATION_CLIENT_SAMPLE=y
固件包位于:
运行与验收
- 打开两块板的调试串口。
- 先启动 Server,确认进入等待连接状态。
- 再启动 Client。两端会自动完成建链和传输,无需人工输入。
- 先检查 Server 的触发与发送日志,再以 Client 的重组日志做最终判定。
Server 关键日志应包含:
[sle fragmentation server] transfer start: bytes=1024, fragments=6, checksum=130560
[sle fragmentation server] fragment sent: 1/6, payload=180
[sle fragmentation server] fragment sent: 6/6, payload=124
[sle fragmentation server] transfer complete
Client 关键日志应包含:
[sle fragmentation client] transfer request accepted, handle=0x..
[sle fragmentation client] fragment received: 1/6, total_bytes=180
[sle fragmentation client] fragment received: 6/6, total_bytes=1024
[sle fragmentation client] reassembly complete: bytes=1024, checksum=130560
[sle fragmentation client] test passed
若 Server 已输出 transfer complete,但 Client 没有 test passed,仍应判定接收端验证失败。
常见问题
| 现象 | 检查项 |
|---|---|
| Client 一直扫描 | 确认 Server 已烧录 Server 角色,并广播 fragment_server |
| 服务发现后没有触发传输 | 查看 Read Confirm 是否成功;Client 只有读确认成功才发送 0x01 |
transfer request failed |
检查写入长度和值是否为单字节 0x01,非法值返回 ERRCODE_SSAP_INCORRECT_DATA_VALUE |
packet header rejected |
检查 Magic、Transfer ID、总片数、负载长度和实际接收长度 |
sequence error |
检查是否丢片、乱序或出现并发传输;当前实现不会自动恢复 |
checksum failed |
对比实际总长度、实际累加和与最后一片携带的期望值 |
关键配置
角色与能力
顶层 SLE 案例 choice 定义两个互斥角色:
CONFIG_SAMPLE_SUPPORT_SLE_FRAGMENTATION_SERVER_SAMPLE
CONFIG_SAMPLE_SUPPORT_SLE_FRAGMENTATION_CLIENT_SAMPLE
选择 Server 后自动使能 SUPPORT_SLE_PERIPHERAL;选择 Client 后自动使能 SUPPORT_SLE_CENTRAL。
协议与任务参数
| 常量 | 值 | 源码位置 |
|---|---|---|
SLE_FRAGMENTATION_MAGIC |
0x5346 |
sle_fragmentation_protocol.h |
SLE_FRAGMENTATION_TRANSFER_ID |
1 | sle_fragmentation_protocol.h |
SLE_FRAGMENTATION_DATA_SIZE |
1024 | sle_fragmentation_protocol.h |
SLE_FRAGMENTATION_PAYLOAD_SIZE |
180 | sle_fragmentation_protocol.h |
SLE_FRAGMENTATION_TOTAL_FRAGMENTS |
6 | sle_fragmentation_protocol.h |
SLE_FRAGMENTATION_MTU_SIZE |
520 | sle_fragmentation_server.c |
SLE_FRAGMENTATION_SEND_INTERVAL_MS |
20 ms | sle_fragmentation_server.c |
SLE_FRAGMENTATION_SEND_TASK_PRIORITY |
27 | sle_fragmentation_server.c |
SLE_FRAGMENTATION_SEND_TASK_STACK_SIZE |
0x1000 |
sle_fragmentation_server.c |
修改数据长度或负载长度时,应同时检查:总片数公式、发送任务栈占用、Client 重组缓冲区、最大属性数据长度和实际 MTU。修改协议头时必须同步修改两端,不能只调整 Server 的 packet_len。
代码详解
1. 代码目录与调用关系
src/application/samples/bt/sle/sle_fragmentation/
├── sle_fragmentation.c
│ ├── sle_fragmentation_entry()
│ ├── Client 重组缓冲区与 Notification 回调
│ └── Client 最终长度和 checksum 验证
├── sle_fragmentation_protocol.h
│ └── 公共分片结构和长度常量
├── sle_fragmentation_server/src/
│ ├── sle_fragmentation_server.c
│ └── sle_fragmentation_server_adv.c
└── sle_fragmentation_client/src/
└── sle_fragmentation_client.c
sle_fragmentation_entry() 根据 Kconfig 创建 SLEFragmentServer 或 SLEFragmentClient 任务,角色任务优先级为 28、栈大小为 0x1000。Server 收到触发后另建优先级 27 的 SLEFragmentTx 任务执行实际发送。
2. Server 只接受明确的触发值
Server 写回调只接受长度为 1、首字节为 1 的请求:
如果请求需要响应,合法触发返回成功,其他内容返回 ERRCODE_SSAP_INCORRECT_DATA_VALUE。响应发送完成后,合法请求才调用 sle_fragmentation_start_transfer()。
由于启动函数返回 void,任务创建失败无法反馈到已经发送的写响应;源码也没有使用忙标志拒绝第二个合法触发。
3. Server 计算边界并逐片发送
发送任务生成递增测试数据并计算完整校验和:
for (uint16_t index = 0; index < sizeof(source); index++) {
source[index] = (uint8_t)(index & 0xFF);
}
每轮根据片序号计算源数据偏移和剩余长度:
uint16_t offset = index * SLE_FRAGMENTATION_PAYLOAD_SIZE;
uint16_t remaining = (uint16_t)(sizeof(source) - offset);
packet.payload_len =
(remaining > SLE_FRAGMENTATION_PAYLOAD_SIZE) ?
SLE_FRAGMENTATION_PAYLOAD_SIZE : remaining;
复制和 Notification 发送任一失败都会立即结束任务,不再发送后续分片。全部 6 片的发送调用都返回成功后,Server 才输出 transfer complete。
4. Client 先校验再写入缓冲区
Notification 回调依次执行以下检查:
| 检查 | 失败日志 |
|---|---|
| 回调状态成功,数据长度不少于当前头长度 | invalid packet |
| Magic、Transfer ID 和总片数匹配 | packet header rejected |
payload_len <= 180 |
packet header rejected |
| 实际数据长度严格等于头长度加负载长度 | packet header rejected |
index == g_next_fragment |
sequence error |
| 追加后不超过 1024 字节缓冲区 | sequence error |
memcpy_s() 成功 |
payload copy failed |
只有全部检查通过,Client 才追加负载、增加累计长度并递增下一片序号。源码不保存乱序分片,也不在序号错误后跳过缺失位置。
5. 0 号片重置接收状态
回调在进行序号检查之前处理:
这让新一轮传输可以从 0 号片重新开始,但没有同时检查新的 Transfer ID,因为 Transfer ID 始终固定为 1。重复或并发到达的 0 号片也会清空当前进度。
6. 最后一片触发完整性校验
每接收一片,g_next_fragment 加 1。当它等于头中的 total 时,Client 对当前重组缓冲区重新求和:
if ((g_reassembly_length == SLE_FRAGMENTATION_DATA_SIZE) &&
(checksum == packet->checksum)) {
osal_printk("[sle fragmentation client] test passed\r\n");
}
因此 test passed 的成立条件是:6 片按序通过检查、累计长度恰好 1024,并且计算结果等于最后一片头中的 checksum。它不代表协议具备重传、安全校验或多会话隔离能力。