1. GATT 基础回顾:协议栈架构与角色模型
做 BLE 开发的朋友,GATT 应该是打交道最多的一个层。我刚开始接触时,总觉得它就是个「属性读写器」,后来踩了几个坑才明白——GATT 的设计哲学直接决定了你产品的功耗、吞吐量和兼容性。
这一章我们快速过一遍基础。老手可以当复习,新手建议逐段看,后面优化章节会反复引用这些概念。
1.1 GATT 在蓝牙协议栈中的位置
先看一张整体架构图。GATT 跑在 ATT(Attribute Protocol)之上,而 ATT 又依赖 L2CAP 提供的数据通道。说白了,GATT 就是给 ATT 的「属性」赋予了业务含义。
关键认知: GATT 不是传输层,它是「数据组织层」。它不关心数据怎么分包、怎么重传,它只关心「这个数据属于哪个服务、哪个特征」。
这张图我建议你存下来。后面讲 MTU 优化、PDU 合并、通知批量发送时,你都得回来想清楚——你改的是哪一层?
1.2 Server / Client 角色模型
GATT 的角色划分很简单:
- GATT Server — 持有属性数据的一方。通常是传感器、外设。
- GATT Client — 发起读写请求的一方。通常是手机、网关。
我的经验: 很多人以为「Server 就是从机,Client 就是主机」。其实不对。一个设备可以同时是 Server 和 Client。比如手机连了手表,手机是 Client;但手机又把数据转发给云,手机对云来说又成了 Server。角色是按「谁持有数据」划分的,不是按连接角色。
举个例子。你做一个心率胸带:
- 胸带是 GATT Server — 它存着心率值、电池电量、设备信息。
- 手机 App 是 GATT Client — 它发请求读心率,或者订阅心率通知。
但胸带也可能需要读手机的当前时间。这时候胸带就临时充当 Client,手机变成 Server。嗯,这种场景在「时间同步」服务里很常见。
1.3 Profile / Service / Characteristic 层级关系
这是 GATT 最核心的数据组织方式。我习惯把它比作「文件系统」:
| 层级 | 类比 | 说明 | UUID 示例 |
|---|---|---|---|
| Profile | 应用程序 | 一个或多个 Service 的集合,定义完整用例 | 心率 Profile (0x180D) |
| Service | 文件夹 | 一组相关功能的集合,包含多个 Characteristic | 电池服务 (0x180F) |
| Characteristic | 文件 | 一个具体的数据点,包含值、属性、描述符 | 电池电量 (0x2A19) |
| Descriptor | 文件属性 | 描述 Characteristic 的配置或元数据 | CCCD (0x2902) |
注意: Profile 在协议栈里不是一个实体。它只是规范文档里的概念。真正在 ATT 层注册的是 Service、Characteristic 和 Descriptor。我见过有人试图在代码里创建一个「Profile 对象」,结果发现 SDK 根本不支持——因为 Profile 不存在于属性表中。
再画一张层级关系图,帮你建立直观印象:
1.4 Characteristic 的四个核心要素
每个 Characteristic 由四部分组成。我建议你把它刻在脑子里:
- 声明(Declaration) — 一个 ATT 属性,告诉 Client「这个 Characteristic 的 UUID、值句柄、属性是什么」。
- 值(Value) — 实际数据存放的地方。比如心率值 72 bpm。
- 属性(Properties) — 定义这个值支持哪些操作:读、写、通知、指示等。
- 描述符(Descriptors) — 附加信息,比如 CCCD(Client Characteristic Configuration Descriptor)控制通知开关。
避坑指南: 我曾经在一个产品里把 Characteristic 的属性设成了「可读 + 可通知」,但忘了加 CCCD。结果手机端死活收不到通知。查了两天才发现——没有 CCCD,Client 没法使能通知。这个坑我建议你记下来。
1.5 属性表与句柄
GATT Server 内部维护一张「属性表」。每个属性有一个唯一的句柄(Handle),从 0x0001 开始递增。Client 通过句柄来定位属性。
举个例子,一个简单的心率服务属性表长这样:
| 句柄 | 类型 (UUID) | 值 | 权限 |
|---|---|---|---|
| 0x0001 | Primary Service (0x2800) | 0x180D | 只读 |
| 0x0002 | Characteristic (0x2803) | 0x10 | 0x0003 | 0x2A37 | 只读 |
| 0x0003 | Heart Rate Measurement (0x2A37) | [0x00, 0x48] | 可读 / 通知 |
| 0x0004 | CCCD (0x2902) | 0x0000 | 可读 / 可写 |
注意看句柄 0x0002 的 Characteristic 声明里,包含了值句柄 0x0003 和 UUID 0x2A37。这就是 Client 发现服务的依据——它先读声明,拿到值句柄,再去读实际数据。
我的习惯: 设计服务时,我会先画一张属性表草图,标清楚每个句柄的用途。这样写代码时不容易漏掉 CCCD,也方便后面做 MTU 优化时计算属性总长度。
1.6 本章小结
这一章我们覆盖了三个核心概念:
- GATT 在协议栈中的位置 — 它跑在 ATT 之上,是数据组织层。
- Server/Client 角色 — 谁持有数据谁是 Server,角色可以切换。
- Profile/Service/Characteristic 层级 — 像文件系统一样组织数据。