2. BLE 通信模型:ATT、GATT、Service 与 Characteristic
上一章我们聊了 BLE 的物理层和链路层,说白了就是数据怎么在空中飞。但真正让开发者头疼的,是数据飞上去之后怎么组织、怎么读写、怎么通知——这就是 ATT 和 GATT 的活儿了。
我个人习惯把 BLE 通信模型比作一个「医院体检系统」:Attribute 是每个体检项目(测血压、抽血),Service 是科室(内科、外科),Characteristic 是具体的检查指标(收缩压、舒张压)。你想想看,没有这套分层,设备之间连「你想读什么」都说不清楚。
核心一句话:ATT 定义了数据怎么读写,GATT 定义了数据长什么样。两者缺一不可。
2.1 Attribute Protocol(ATT)—— 最底层的读写规则
ATT 是 BLE 通信的基石。它把设备抽象成一张「属性表」,每个属性都有一个句柄(Handle)、一个类型(UUID)、一组权限(Permissions)和一段值(Value)。
我记得刚接触 BLE 时,总觉得 ATT 就是简单的「读-写-通知」三个操作。后来踩了坑才明白,ATT 的权限模型才是真正的暗礁。
2.1.1 ATT 的六种 PDU
ATT 协议定义了 6 种操作类型,分为请求/响应、命令、通知、指示四类:
| 操作类型 | 方向 | 是否需要响应 | 典型场景 |
|---|---|---|---|
| Read Request / Response | Client → Server → Client | 是 | 读取设备名称、电池电量 |
| Write Request / Response | Client → Server → Client | 是 | 写入配置参数,需要确认 |
| Write Command | Client → Server | 否 | 高频数据写入,不关心结果 |
| Notification | Server → Client | 否 | 传感器数据推送,丢一帧无所谓 |
| Indication | Server → Client | 是(Confirm) | 关键事件上报,必须可靠 |
我曾经踩过的坑:Notification 和 Indication 的 MTU 限制。默认 MTU 只有 23 字节,扣掉 3 字节 ATT 头,实际有效载荷只有 20 字节。如果你一次发超过 20 字节的数据,必须分片。很多新手直接塞 100 字节进去,结果对端只收到前 20 字节,剩下的全丢了。
2.1.2 ATT 权限模型
每个 Attribute 都有三组权限:
- 访问权限:可读、可写、可读可写、不可读不可写
- 加密权限:无需加密、需要加密、需要配对、需要授权
- 认证权限:是否需要 MITM 保护
嗯,这里要注意:权限是在 ATT 层强制检查的。如果你在 GATT 层配了「可读」,但 ATT 层没配读权限,GATT 层的配置就是废纸。我见过一个项目,工程师在 GATT 层配了 20 个可读特征,结果手机端一个都读不到——因为 ATT 层默认全禁了。
2.2 Generic Attribute Profile(GATT)—— 数据的组织规范
ATT 只提供了「怎么读写」,GATT 则定义了「读写什么」以及「数据长什么样」。GATT 把 ATT 的属性表组织成层级结构:
GATT Profile
├── Service(服务)
│ ├── Characteristic(特征)
│ │ ├── Value(值)
│ │ ├── Descriptor(描述符)
│ │ └── Descriptor(描述符)
│ └── Characteristic(特征)
│ ├── Value
│ └── Descriptor
└── Service(服务)
└── ...
说白了,GATT 就是给 ATT 那张「扁平属性表」加了一套目录系统。没有 GATT,你只能按句柄号 0x0001、0x0002 去读数据,根本不知道哪个句柄对应什么功能。
2.3 Service 与 Characteristic 的定义
2.3.1 Service(服务)
Service 是一组相关功能的集合。每个 Service 都有一个 UUID 标识:
- 16-bit UUID:蓝牙 SIG 定义的标准服务,比如电池服务(0x180F)、设备信息服务(0x180A)
- 128-bit UUID:厂商自定义服务,比如你的智能灯泡专用服务
我个人习惯:能用标准服务就别自己造。标准服务的 UUID 短,省空间;而且手机端的系统蓝牙框架通常对标准服务有原生支持,省去很多适配工作。
2.3.2 Characteristic(特征)
Characteristic 是 GATT 中最核心的数据单元。它包含三个部分:
- Declaration(声明):固定句柄,包含特征属性、值句柄、UUID
- Value(值):实际数据存放的地方
- Descriptors(描述符):对值的补充说明,比如单位、范围、配置
举个例子,电池服务(0x180F)下的电池电量特征:
| 元素 | 句柄 | UUID | 值/说明 |
|---|---|---|---|
| Service Declaration | 0x0010 | 0x180F | 电池服务 |
| Characteristic Declaration | 0x0011 | 0x2803 | 属性: Notify | Read, 值句柄: 0x0012, UUID: 0x2A19 |
| Characteristic Value | 0x0012 | 0x2A19 | 当前电量百分比(0-100) |
| Client Characteristic Configuration | 0x0013 | 0x2902 | 0x0000(禁用通知)/ 0x0001(启用通知) |
避坑指南:CCCD(Client Characteristic Configuration Descriptor,0x2902)是 Notification 和 Indication 的开关。很多新手只配了特征属性为 Notify,却忘了配 CCCD 的读写权限。结果手机端订阅通知时,写 CCCD 失败,通知永远发不出来。我曾经因为这个 bug 排查了整整两天。
2.4 Notification 与 Indication 的深度对比
这两个概念是 BLE 开发中最容易混淆的。我直接上对比表:
| 对比项 | Notification | Indication |
|---|---|---|
| 是否需要确认 | 否(Fire-and-Forget) | 是(需要 Confirm) |
| 可靠性 | 低,可能丢包 | 高,保证送达 |
| 吞吐量 | 高,可以连续发送 | 低,必须等 Confirm 才能发下一条 |
| 典型应用 | 心率数据、温度传感器 | 报警事件、固件升级关键帧 |
| 对端负载 | 低,无需处理响应 | 高,需要生成 Confirm |
你想想看,心率监测器每秒发 10 次数据,如果用 Indication,每次都要等手机确认,数据根本发不出去。反过来,如果是一个烟雾报警器触发报警,丢一次 Notification 可能就出大事了——这时候必须用 Indication。
2.4.1 实际开发中的选择策略
我总结了一个简单的决策树:
- 数据频率 > 10Hz → 用 Notification(比如加速度计数据)
- 数据频率 < 1Hz 且必须可靠 → 用 Indication(比如门锁状态变更)
- 数据频率 1-10Hz → 看业务容忍度,能接受偶尔丢包就用 Notification
注意:Notification 虽然不需要确认,但链路层有重传机制。如果链路质量好,Notification 的可靠性其实不低。我做过一个测试:在 1 米距离、无遮挡的环境下,连续发送 10000 次 Notification,丢包率为 0。但在 10 米距离、隔一堵墙的情况下,丢包率飙升到 3%。所以,别盲目迷信 Indication,也别完全信任 Notification——要根据实际场景测试。
2.5 本章知识体系总览
下面这张图把 ATT、GATT、Service、Characteristic、Notification/Indication 的关系串起来了:
从这张图可以看得很清楚:GATT 是「组织者」,ATT 是「执行者」,L2CAP 和 Link Layer 是「运输队」。你在应用层看到的 Service 和 Characteristic,最终都会映射成 ATT 层的一张属性表。
2.6 实战建议
最后,分享几个我在项目中沉淀下来的习惯:
- 先画 Service 树,再写代码。用一张纸画出你的设备有哪些 Service、每个 Service 下有哪些 Characteristic、每个 Characteristic 用什么 UUID、支持哪些操作。画清楚了再动手,能省一半调试时间。
- MTU 协商一定要做。默认 23 字节 MTU 太坑了。连接建立后,第一时间发起 MTU 请求,争取到 247 字节甚至更大。这样一次 Notification 就能发 244 字节有效数据,省去分片逻辑。
- CCCD 的权限别漏。如果你配了 Notify 属性,一定要确保 CCCD 的 Descriptor 是可读可写的。否则手机端订阅通知时,写 CCCD 会失败。
- 测试 Notification 的丢包率。别想当然。在不同距离、不同遮挡条件下测试,看看你的应用能不能容忍这个丢包率。如果不能,果断换 Indication 或者加应用层确认。
一个小技巧:调试 ATT 层问题时,用抓包工具(比如 Ellisys 或 nRF Sniffer)看 ATT PDU 的 opcode。如果手机端发 Read Request 后没收到 Response,大概率是 ATT 权限没配对。如果 Notification 发出去手机没收到,先检查 CCCD 的值是不是 0x0001。
好了,这一章的内容就到这里。ATT 和 GATT 是 BLE 开发者的基本功,理解透了,后面的 Security Manager、L2CAP 增强功能、甚至蓝牙 Mesh 的模型层,学起来都会轻松很多。