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 中最核心的数据单元。它包含三个部分:

  1. Declaration(声明):固定句柄,包含特征属性、值句柄、UUID
  2. Value(值):实际数据存放的地方
  3. 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 的关系串起来了:

BLE 通信模型分层架构 GATT(Generic Attribute Profile) 定义数据的组织方式:Service → Characteristic → Descriptor ATT(Attribute Protocol) 定义数据的读写规则:Handle + UUID + Permissions + Value L2CAP + Link Layer(逻辑链路控制与适配协议 + 链路层) 负责数据的分片、重组、重传和加密 核心数据单元 • Service:功能集合,如电池服务 (0x180F) • Characteristic:数据点,如电量百分比 (0x2A19) • Descriptor:补充说明,如 CCCD (0x2902) • UUID:标识符,16-bit 标准 / 128-bit 自定义 通信机制 • Read/Write:客户端主动读写 • Notification:服务端推送,无需确认 • Indication:服务端推送,需要确认 • CCCD:控制 Notification/Indication 的开关

从这张图可以看得很清楚:GATT 是「组织者」,ATT 是「执行者」,L2CAP 和 Link Layer 是「运输队」。你在应用层看到的 Service 和 Characteristic,最终都会映射成 ATT 层的一张属性表。

2.6 实战建议

最后,分享几个我在项目中沉淀下来的习惯:

  1. 先画 Service 树,再写代码。用一张纸画出你的设备有哪些 Service、每个 Service 下有哪些 Characteristic、每个 Characteristic 用什么 UUID、支持哪些操作。画清楚了再动手,能省一半调试时间。
  2. MTU 协商一定要做。默认 23 字节 MTU 太坑了。连接建立后,第一时间发起 MTU 请求,争取到 247 字节甚至更大。这样一次 Notification 就能发 244 字节有效数据,省去分片逻辑。
  3. CCCD 的权限别漏。如果你配了 Notify 属性,一定要确保 CCCD 的 Descriptor 是可读可写的。否则手机端订阅通知时,写 CCCD 会失败。
  4. 测试 Notification 的丢包率。别想当然。在不同距离、不同遮挡条件下测试,看看你的应用能不能容忍这个丢包率。如果不能,果断换 Indication 或者加应用层确认。

一个小技巧:调试 ATT 层问题时,用抓包工具(比如 Ellisys 或 nRF Sniffer)看 ATT PDU 的 opcode。如果手机端发 Read Request 后没收到 Response,大概率是 ATT 权限没配对。如果 Notification 发出去手机没收到,先检查 CCCD 的值是不是 0x0001。

好了,这一章的内容就到这里。ATT 和 GATT 是 BLE 开发者的基本功,理解透了,后面的 Security Manager、L2CAP 增强功能、甚至蓝牙 Mesh 的模型层,学起来都会轻松很多。