1. GATT 协议基础回顾:Service、Characteristic、Descriptor 的结构与作用,MTU 与 PDU 的概念
做蓝牙开发这么多年,我见过不少工程师一上来就撸代码,结果连 GATT 的基本骨架都没搞清楚,调试时抓耳挠腮。说实话,GATT 协议本身并不复杂,但它的层级结构一旦理解不透,后面做性能优化就会处处碰壁。今天咱们就把这块地基夯实了。
1.1 GATT 的层次结构:从 Service 到 Descriptor
GATT 的全称是 Generic Attribute Profile,说白了就是定义数据怎么组织、怎么交换的一套规则。它把数据塞进一个三层结构里:Service → Characteristic → Descriptor。我习惯把这比作一个文件柜:Service 是抽屉,Characteristic 是文件夹,Descriptor 就是文件夹上的标签说明。
核心要点:GATT 的所有数据交互都围绕 Characteristic 进行。Service 只是逻辑分组,Descriptor 提供元数据。真正读写操作的目标永远是 Characteristic 的 Value。
Service(服务)
Service 是一组相关功能的集合。比如一个「电池服务」里会包含电池电量、电池状态等 Characteristic。每个 Service 都有一个唯一的 UUID 来标识。标准服务(如 Battery Service)使用 16-bit UUID,自定义服务用 128-bit UUID。
Characteristic(特征)
Characteristic 是 GATT 的数据承载单元。它包含三个部分:声明(Declaration)、值(Value)、描述符(Descriptor)。其中 Value 就是我们要读写的实际数据。Characteristic 还带有属性(Properties),比如可读、可写、可通知等。
Descriptor(描述符)
Descriptor 用来描述 Characteristic 的附加信息。最常见的两个:Client Characteristic Configuration Descriptor(CCCD) 用来使能通知或指示;Characteristic User Description 用来存一个人类可读的名字。
个人经验:我在做智能手环项目时,发现很多人把 CCCD 的配置位置搞错了。记住:CCCD 是 Descriptor,不是 Characteristic。你需要在 Characteristic 下面添加它,而不是单独创建一个。
1.2 MTU 与 PDU:数据包大小的关键
聊完结构,咱们得说说数据怎么在线上跑。这里有两个概念必须分清:MTU 和 PDU。我刚开始做蓝牙时也混淆过,后来踩了坑才彻底搞明白。
| 概念 | 全称 | 作用 | 默认值 |
|---|---|---|---|
| MTU | Maximum Transmission Unit | ATT 层一次能发送的最大数据包长度 | 23 字节(包含 3 字节 ATT 头) |
| PDU | Protocol Data Unit | 实际传输的协议数据单元,包含操作码和参数 | 取决于 MTU |
MTU 是 ATT 层的概念。默认 MTU 是 23 字节,其中 3 字节是 ATT 头部(操作码 + 句柄),剩下 20 字节才是有效载荷。这就是为什么很多人说「一次最多传 20 字节」——其实说的是默认情况下的有效数据长度。
PDU 是更底层的概念。每个 ATT 操作(如 Read Request、Write Command)都对应一个 PDU。PDU 的结构是:操作码(1 字节) + 参数(可变长度)。参数里包含句柄、偏移量、数据等。
避坑指南:我曾经在一个项目中,MTU 协商到了 512 字节,但写数据时发现还是只能写 20 字节。查了半天才发现,我用的 Write Command 不支持长包,必须用 Write Long Characteristic Value 或者分段发送。MTU 大不代表单次操作就能塞满,还要看操作类型。
1.3 MTU 协商过程
MTU 不是固定的。主从设备在连接后可以通过 MTU Exchange Request/Response 来协商一个更大的 MTU。流程很简单:
- 客户端发送 MTU Exchange Request,带上自己支持的 MTU 值
- 服务端回复 MTU Exchange Response,带上自己支持的 MTU 值
- 双方取较小值作为本次连接的 MTU
举个例子:客户端支持 512 字节,服务端支持 247 字节,那么最终 MTU 就是 247 字节。有效载荷就是 247 - 3 = 244 字节。
我建议:在性能敏感的应用中,连接建立后立即发起 MTU 协商。默认 23 字节的 MTU 对于传输大量数据来说太慢了。我做过一个 OTA 升级项目,MTU 从 23 提升到 512 后,传输速度提升了将近 20 倍。
1.4 GATT 操作与 PDU 类型
GATT 定义了多种操作,每种操作对应不同的 PDU。我整理了一张常用表:
| 操作类型 | PDU 方向 | 是否需要响应 | 典型用途 |
|---|---|---|---|
| Read Request | Client → Server | 是 | 读取 Characteristic 值 |
| Read Response | Server → Client | - | 返回读取的数据 |
| Write Request | Client → Server | 是 | 可靠写入,需要确认 |
| Write Command | Client → Server | 否 | 快速写入,不需要确认 |
| Handle Value Notification | Server → Client | 否 | 服务器主动推送数据 |
| Handle Value Indication | Server → Client | 是 | 可靠推送,需要确认 |
这里有个关键点:Notification 不需要确认,所以吞吐量更高;Indication 需要确认,可靠性更好但速度慢。我在做心率监测设备时,就用 Notification 来推送实时数据,用 Indication 来传输关键事件。
1.5 整体结构图
下面这张图把 GATT 的层级关系、MTU/PDU 的位置都串起来了。你仔细看看,应该能一目了然。
1.6 实际项目中的注意事项
说了这么多理论,最后分享几个我在项目中踩过的坑:
- MTU 协商不是自动的:很多蓝牙芯片默认不会主动发起 MTU 协商,需要你在应用层显式调用 API。我见过一个团队的产品,MTU 一直是 23,他们还以为蓝牙就这么慢。
- CCCD 的权限问题:使能通知时,客户端需要往 CCCD 写 0x0001 或 0x0002。但有些服务端把 CCCD 的写权限设成了「需要加密」,导致未配对设备无法使能通知。这个坑我踩过两次。
- PDU 长度与 MTU 的关系:MTU 增大后,单次 Read/Write 能传更多数据,但 Write Command 和 Notification 仍然受限于 MTU - 3。如果你要传超过 MTU 的数据,必须用 Prepare Write + Execute Write 或者分段发送。