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。流程很简单:

  1. 客户端发送 MTU Exchange Request,带上自己支持的 MTU 值
  2. 服务端回复 MTU Exchange Response,带上自己支持的 MTU 值
  3. 双方取较小值作为本次连接的 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 的位置都串起来了。你仔细看看,应该能一目了然。

GATT 协议层次结构 Profile(应用层) Service(服务) — UUID 标识 Characteristic(特征) 声明 + Value + 属性(读/写/通知) Descriptor(描述符)— 如 CCCD ATT 层 — MTU / PDU L2CAP 层 MTU = ATT 层最大包长(默认 23 字节),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 或者分段发送。