第 9 篇:Attribute Protocol Overview:GATT 的运输车 ATT

1. 本篇学习范围

本文继续学习:

  • Bluetooth Core Specification v6.2

  • Vol 3:Host

  • Part G:Generic Attribute Profile,GATT

  • Section 2.5:Attribute Protocol

  • Section 2.5.1:Overview 前半部分

  • Figure 2.3:Attribute Protocol PDU

上一篇我们学习了 2.4 Profile fundamentals,知道 GATT 不是直接在空口上飞来飞去,而是通过 ATT Bearer 运行。ATT Bearer 本质上是承载 ATT 数据的 L2CAP channel。

这一篇正式进入 ATT,Attribute Protocol

如果用一句话概括:

GATT 负责定义“服务怎么组织、特征值怎么访问”;ATT 负责把这些访问动作变成真正能在设备之间传输的协议包。

也就是说:

GATT 说:我要读取某个 Characteristic Value
ATT 做:发一个 ATT_READ_REQ
GATT 说:我要通知 Client 某个值变化了
ATT 做:发一个 ATT_HANDLE_VALUE_NTF
GATT 说:我要发一个需要确认的重要事件
ATT 做:发 ATT_HANDLE_VALUE_IND,然后等 ATT_HANDLE_VALUE_CFM

规范在 2.5 中明确要求 GATT 实现 Attribute Protocol,并且要支持 GATT 后续过程所需的 ATT PDUs;2.5.1 进一步说明 GATT 使用 ATT 在设备之间传输 command、request、response、indication、notification、confirmation 等数据,这些数据都封装在 Attribute Protocol PDU 中。


2. 本篇核心结论

先把重点打包带走:

ATT 是 GATT 的底层属性协议。GATT 里的发现、读取、写入、通知、指示等操作,最终都要落到 ATT PDU 上。ATT PDU 的基本结构由 Opcode、Attribute Parameters 和可选的 Authentication Signature 组成。

更通俗一点:

GATT 是“我要干什么”,ATT 是“具体发什么包”。
GATT 像业务经理,ATT 像拿着单子跑腿的执行人员。经理可以说“查一下电量”,但真正跑到对面设备那里的,是 ATT 的 ATT_READ_REQ


3. 为什么 GATT 必须依赖 ATT?

前面几篇我们一直说 GATT 是服务框架。它负责定义:

  • Service 怎么组织;

  • Characteristic 怎么声明;

  • Descriptor 怎么挂在 Characteristic 后面;

  • Client 怎么发现远端服务;

  • Client 怎么读写特征值;

  • Server 怎么主动通知或指示 Client。

但这些都还是 语义层 的东西。

举个例子:

Read Characteristic Value

这句话从 GATT 角度看很清楚:读取某个特征值。

但是设备之间真正通信时,不能只发一句“我要读特征值”。对端协议栈需要知道:

  • 这是读请求还是写请求;

  • 要读哪个 Attribute Handle;

  • 是否需要响应;

  • 响应包长什么样;

  • 如果失败,错误怎么返回;

  • 这个包是否带认证签名。

这些细节就是 ATT 负责的。

所以 GATT 和 ATT 的关系可以画成这样:

Application / Higher Layer Profile

GATT
Service / Characteristic / Procedure

ATT
PDU / Opcode / Attribute Parameters

L2CAP

Controller / Link

GATT 往上面对 Application,往下面对 ATT。

Application 看到的是:

readCharacteristic()
writeCharacteristic()
setNotifyValue()

ATT 看到的是:

ATT_READ_REQ
ATT_WRITE_REQ
ATT_HANDLE_VALUE_NTF

这就是蓝牙协议学习中非常重要的一层映射:

应用 API → GATT Procedure → ATT PDU → L2CAP → 空口

4. GATT 是语义层,ATT 是包层

很多 BLE 新手第一次抓包时会困惑:

我明明调用的是 readCharacteristic(),为什么抓包里看到的是 ATT_READ_REQ

原因就是:你在应用层做的是 GATT 操作,但空口附近看到的是 ATT PDU。

例如读取电量:

GATT Server ATT GATT Application GATT Server ATT GATT Application 读取 Battery Level Read Characteristic Value ATT_READ_REQ, Handle = Battery Level Value Handle ATT_READ_RSP, Value = 85 返回 Attribute Value Battery Level = 85%

这里有三层意思:

  1. App 层 说“我要读电量”;

  2. GATT 层 把它理解成 “Read Characteristic Value”;

  3. ATT 层 把它变成 ATT_READ_REQ,并带上具体的 Attribute Handle。

所以后面看抓包,一定不要只背 PDU 名称,要能反推它背后的 GATT 语义。

看到:

ATT_READ_REQ

你要想到:

这里可能是在做 GATT Read Characteristic Value,或者读取某个 Descriptor。

看到:

ATT_WRITE_REQ to CCCD

你要想到:

这里很可能是在打开 Notification 或 Indication。

看到:

ATT_HANDLE_VALUE_NTF

你要想到:

Server 正在主动推送某个 Characteristic Value。

这就是从“看包名”升级到“看协议语义”。


5. ATT 支持哪些通信形式?

GATT 使用 ATT 在设备之间传输多种形式的数据,包括:

  • command;

  • request;

  • response;

  • notification;

  • indication;

  • confirmation。

这些形式都属于 ATT 层的通信方法。规范在 2.5.1 中明确列出了这些数据形式,并说明它们都包含在 ATT PDU 中。

先用一张时序图看整体:

Server Client Server Client 不要求 Response Client 不需要确认 Command Request Response Notification Indication Confirmation

这几个名字非常重要。后面读 GATT feature requirements 时,会反复遇到。


6. Command:写了就跑,不等回信

Command 的典型代表是:

ATT_WRITE_CMD

它常用于 Write Without Response

也就是说,Client 把数据发给 Server,不要求 Server 返回 ATT response。

Server Client Server Client 没有 ATT_WRITE_RSP ATT_WRITE_CMD

它的优点是快。

因为不用等响应,Client 可以更高效地发送数据,适合:

  • 串口透传;

  • 高频小数据;

  • 对实时性要求高、但允许上层自己处理可靠性的场景;

  • 某些控制类数据流。

但缺点也很明显:

Server 到底有没有处理成功,ATT 层不会告诉你。

所以 command 就像:

外卖骑手把饭放门口,拍照走人。
你吃没吃、饭有没有被猫叼走,他不等你确认。

工程上如果你用 Write Without Response 做关键控制命令,就要自己在应用层设计确认机制。否则设备没执行,你还以为“世界很美好”,结果灯没亮、电机没停,Bug 在角落里笑出声。


7. Request / Response:一问一答,规规矩矩

Request 和 Response 是最经典的一问一答模型。

典型例子:

ATT_READ_REQ  → ATT_READ_RSP
ATT_WRITE_REQ → ATT_WRITE_RSP
Server Client Server Client ATT_READ_REQ ATT_READ_RSP ATT_WRITE_REQ ATT_WRITE_RSP

这种模型适合:

  • 读取设备名;

  • 读取电量;

  • 读取固件版本;

  • 写入配置;

  • 写入控制命令;

  • 写 CCCD 打开通知;

  • 需要知道成功或失败的操作。

Request / Response 的特点是清楚:

Client 发起请求,Server 必须给结果。
成功就返回正常 Response,失败就返回 Error Response。

所以它像银行柜台:

你递材料,柜员必须告诉你:办好了,或者哪里不合格。
不会像 Command 那样“材料一扔,人就跑了”。


8. Notification:Server 主动喊一声

Notification 是 Server 主动发给 Client 的数据。

典型 ATT PDU 是:

ATT_HANDLE_VALUE_NTF
Client Server Client Server Client 不需要 ATT 层确认 ATT_HANDLE_VALUE_NTF

Notification 适合高频数据,比如:

  • 心率连续上报;

  • 温度周期上报;

  • IMU 数据流;

  • BLE 串口透传;

  • 传感器实时数据。

它的特点是:

  • Server 主动发;

  • Client 不需要发 ATT confirmation;

  • 吞吐通常比 Indication 更高;

  • 可靠语义较弱。

Notification 就像微信群消息:

Server:温度 26.5。
Server:温度 26.6。
Server:温度 26.7。
Client 不需要每条都回“收到收到收到”。

但注意,Notification 不是 Server 想发就随便发。通常 Client 需要先通过写 CCCD 来启用 Notification。后面讲 CCCD 时会重点拆这个“通知总闸”。


9. Indication:Server 主动发,但 Client 必须签收

Indication 也是 Server 主动发,但它和 Notification 最大区别是:

Indication 需要 Client 返回 Confirmation。

典型 ATT PDU 是:

ATT_HANDLE_VALUE_IND
ATT_HANDLE_VALUE_CFM
Client Server Client Server ATT_HANDLE_VALUE_IND ATT_HANDLE_VALUE_CFM

Indication 更适合:

  • Service Changed;

  • 关键状态变化;

  • 重要控制事件;

  • 低频但不希望悄悄丢掉的数据。

Notification 像群消息,Indication 像挂号信。

Notification:我通知你了,不等你回复。
Indication:我通知你了,你必须签收。

工程上要注意:

Server 发出 Indication 后,需要等 Client 的 Confirmation。
如果 Client 不确认,后续 Indication 可能会被阻塞。

所以 Indication 可靠一些,但吞吐不如 Notification。别拿它当高速数据流通道用,不然就像拿挂号信发实时 IMU 数据,邮局都想辞职。


10. ATT PDU 的基本结构

Figure 2.3 展示了 ATT PDU 的基本结构。它由三部分组成:

  1. Opcode:1 octet;

  2. Attribute Parameters:可变长度;

  3. Authentication Signature:存在时为 12 octets。

转成 Mermaid packet 图:

Opcode - 1 octet 0 7 Attribute Parameters - variable length 8 31 Attribute Parameters - variable length 32 63 Authentication Signature - 12 octets when present 64 95 Authentication Signature - 12 octets when present 96 127 Authentication Signature - 12 octets when present 128 159 Attribute Protocol PDU

这里要注意:图里的 Attribute Parameters 只是示意“可变长度”,不是固定 7 个字节。实际长度取决于具体 PDU 类型和当前 ATT_MTU。


11. Opcode:这包到底是干什么的

Opcode 是 ATT PDU 的第一个字段,长度 1 octet。

它的作用是告诉接收方:

这个包是什么类型?

比如:

ATT PDU 大概含义
ATT_READ_REQ 读请求
ATT_READ_RSP 读响应
ATT_WRITE_REQ 写请求
ATT_WRITE_RSP 写响应
ATT_WRITE_CMD 无响应写
ATT_HANDLE_VALUE_NTF 通知
ATT_HANDLE_VALUE_IND 指示
ATT_HANDLE_VALUE_CFM 指示确认

接收方必须先看 Opcode,才能知道后面的 Attribute Parameters 怎么解析。

同样是一串字节:

03 00 01

如果 Opcode 不同,意义可能完全不同。

Opcode 就像快递单上的业务类型:

  • 查询件;

  • 派送件;

  • 签收确认;

  • 退货单;

  • 加急件。

如果快递单上没有业务类型,后面的地址、电话、备注再详细也没用,因为工作人员不知道你到底要寄、要查、还是要投诉。


12. Attribute Parameters:真正的业务内容

如果 Opcode 表示“这是什么业务”,那么 Attribute Parameters 就表示“业务参数”。

举几个例子。

12.1 Read Request

Opcode = ATT_READ_REQ
Attribute Parameters = Attribute Handle

意思是:

我要读取这个 handle 对应的 Attribute Value。

12.2 Write Request

Opcode = ATT_WRITE_REQ
Attribute Parameters = Attribute Handle + Value

意思是:

我要把这个 value 写到这个 handle 上。

12.3 Notification

Opcode = ATT_HANDLE_VALUE_NTF
Attribute Parameters = Attribute Handle + Value

意思是:

Server 主动告诉 Client:这个 handle 对应的 value 变化了。

画成图:

ATT PDU

Opcode

Attribute Parameters

ATT_READ_REQ

Handle

ATT_WRITE_REQ

Handle + Value

ATT_HANDLE_VALUE_NTF

Handle + Value

工程抓包时,不要只看 Opcode。

看到 ATT_WRITE_REQ 只能说明“这是写请求”,但到底写了什么,要看 Parameters:

Handle = ?
Value = ?

如果 handle 是 CCCD,value 是 01 00,那大概率是在启用 Notification。

如果 handle 是某个控制点,value 是 02 01 00,那就要查对应业务协议,看这个控制命令到底是什么意思。

Opcode 是菜名,Parameters 才是菜里放了什么料。


13. Authentication Signature:可选的防伪封条

ATT PDU 的第三部分是 Authentication Signature

它是可选字段,在存在时长度为 12 octets。规范也说明该字段和安全管理相关,具体机制在安全管理章节中描述。

普通情况下,我们常见的 ATT PDU 可能只有:

Opcode + Attribute Parameters

某些安全相关场景下,后面可能带:

Authentication Signature

可以把它理解成防伪封条。

Normal ATT PDU

Opcode

Attribute Parameters

Signed ATT PDU

Opcode

Attribute Parameters

Authentication Signature

通俗讲:

普通快递:业务类型 + 包裹内容。
签名快递:业务类型 + 包裹内容 + 防伪签名。

这个签名不是每包都有,也不是 App 层平时经常直接处理的东西。但做协议栈、做安全、做底层抓包时,必须知道 ATT PDU 结构里有这个可选尾巴。


14. GATT Procedure 到 ATT PDU 的映射思路

本篇不是完整讲 Section 4.13,但这里可以提前建立映射思路。

GATT 后续很多 procedure,最终都会映射到 ATT opcode。规范后面的映射表会列出例如 Notification 对应 ATT_HANDLE_VALUE_NTF,Indication 对应 ATT_HANDLE_VALUE_INDATT_HANDLE_VALUE_CFM,长写和可靠写会使用 Prepare Write 与 Execute Write 相关 PDU。

先看几个常见映射:

GATT Procedure

ATT PDU

Read Characteristic Value

ATT_READ_REQ / ATT_READ_RSP

Write Characteristic Value

ATT_WRITE_REQ / ATT_WRITE_RSP

Write Without Response

ATT_WRITE_CMD

Notification

ATT_HANDLE_VALUE_NTF

Indication

ATT_HANDLE_VALUE_IND / ATT_HANDLE_VALUE_CFM

Exchange MTU

ATT_EXCHANGE_MTU_REQ / ATT_EXCHANGE_MTU_RSP

理解这个映射后,抓包就不再是一堆陌生缩写。

例如:

ATT_READ_BY_GROUP_TYPE_REQ

你会知道这通常和服务发现相关。

ATT_FIND_INFORMATION_REQ

你会想到 Descriptor Discovery。

ATT_PREPARE_WRITE_REQ
ATT_EXECUTE_WRITE_REQ

你会想到 Long Write 或 Reliable Write。

这就是后面学习 Section 4 的基础。


15. 一次 Read 操作完整拆解

假设 Client 要读取 Battery Level。

从应用到 ATT,大概是这样:

Server Attribute Database ATT GATT App Server Attribute Database ATT GATT App 读取电量 找到 Battery Level Value Handle 构造 Read Characteristic Value ATT_READ_REQ, Handle = 0x0025 ATT_READ_RSP, Value = 85 返回 Attribute Value 电量 = 85%

这里面有几个关键点:

  1. App 关心“电量”;

  2. GATT 关心“Battery Level Characteristic”;

  3. ATT 关心“handle = 0x0025”;

  4. Server 按 handle 找 Attribute Value;

  5. 返回的 value 再被上层解释成 85%。

所以协议学习不能只停在 API 层。

如果你只知道 readCharacteristic(),遇到抓包和协议栈问题就容易懵。

如果你知道它底层是 ATT_READ_REQ,并且参数里是 Attribute Handle,那排查问题就有方向了。


16. 一次 Notification 操作完整拆解

再看 Notification。

假设温度传感器周期性上报温度。

GATT Client ATT GATT Server Sensor App GATT Client ATT GATT Server Sensor App 温度变化 检查 Client 是否启用 Notification 构造 Notification ATT_HANDLE_VALUE_NTF, Handle + Value 上报给应用层 callback

这里有一个重要细节:

Notification 是 Server 主动发,但通常要以 Client 已经配置 CCCD 为前提。

也就是说:

先订阅,再推送。

如果 Client 没写 CCCD,Server 通常不应该随便给它发 Notification。

这也是 Android BLE 开发中经典大坑:

setCharacteristicNotification() 只是本地开关;
真正让远端 Server 发通知,通常还要写 CCCD。

17. 抓包时应该怎么看 ATT?

抓包时建议按三步看:

看到 ATT PDU

先看 Opcode

判断 GATT 语义

再看 Attribute Parameters

结合 Attribute Handle / UUID / Value 判断业务含义

第一步:看 Opcode

例如:

ATT_READ_REQ

说明这是读请求。

第二步:看方向

如果是 Client 到 Server:

Client 正在读 Server 上的某个 Attribute。

如果是 Server 到 Client:

那就不可能是 READ_REQ,而可能是 Notification / Indication / Response。

第三步:看 Parameters

例如:

Handle = 0x0025

然后你要回到 GATT database:

0x0025 是什么?
是 Characteristic Value?
是 Descriptor?
是 CCCD?

第四步:结合业务解释

如果 0x0025 是 Battery Level Value Handle:

这就是读取电量。

如果 0x0025 是 CCCD:

这可能是在读取或写入通知配置。

所以抓包不是看谁缩写多,而是要把包还原成业务动作。


18. Android / iOS / 嵌入式开发视角

18.1 Android

Android 里你经常调用:

discoverServices()
readCharacteristic()
writeCharacteristic()
setCharacteristicNotification()

但底层会映射成 ATT PDU:

Android 操作 GATT 语义 ATT 层常见表现
discoverServices() 服务发现 ATT_READ_BY_GROUP_TYPE_REQ
readCharacteristic() 读取特征值 ATT_READ_REQ
writeCharacteristic() 写特征值 ATT_WRITE_REQATT_WRITE_CMD
写 CCCD 配置通知/指示 ATT_WRITE_REQ
收通知回调 Notification ATT_HANDLE_VALUE_NTF

所以 Android 回调失败时,不要只盯着 Java/Kotlin 层。很多时候真正的案发现场在 ATT:

  • 有没有发出 ATT_READ_REQ

  • Server 有没有返回 ATT_ERROR_RSP

  • 写 CCCD 的 handle 对不对;

  • Notification 有没有从 Server 发出来;

  • Indication 后 Client 有没有 confirmation。

18.2 iOS CoreBluetooth

iOS 也是类似:

iOS 操作 GATT 语义 ATT 层
discoverServices 发现服务 ATT discovery 相关 PDU
readValue 读特征值 ATT_READ_REQ
writeValue 写特征值 ATT_WRITE_REQ / command
setNotifyValue 配置通知或指示 写 CCCD + 接收 NTF/IND

CoreBluetooth 把很多细节封装掉了,但协议底层没有消失。它只是藏到系统栈里了。

18.3 嵌入式协议栈

嵌入式开发更接近 ATT。

你可能会看到:

现象 ATT 视角
read callback 被调用 对端发了 Read Request
write callback 被调用 对端发了 Write Request 或 Write Command
notify API 被调用 本端发送 Notification
indicate API 被调用 本端发送 Indication 并等待 Confirmation
MTU callback Exchange MTU 过程完成
CCCD callback 对端写了 CCCD Descriptor

嵌入式尤其要分清:

Write Request 有响应
Write Command 无响应
Notification 无确认
Indication 有确认

这几个如果混了,状态机很容易写成“蓝牙麻花”。


19. 常见误区

19.1 误区一:GATT 和 ATT 是一个东西

不是。

GATT ATT
Generic Attribute Profile Attribute Protocol
定义 Service / Characteristic / Descriptor 模型 定义 Attribute PDU
定义 GATT procedure 定义 opcode 和参数
偏语义框架 偏协议包格式

GATT 像菜单和点餐规则,ATT 像下单系统和订单格式。

19.2 误区二:PDU 就是 Procedure

不是。

Procedure 是流程,PDU 是流程里发的包。

例如:

GATT Read Characteristic Value procedure
    = ATT_READ_REQ + ATT_READ_RSP
GATT Indication procedure
    = ATT_HANDLE_VALUE_IND + ATT_HANDLE_VALUE_CFM

一个 procedure 可能包含一个或多个 PDU。

19.3 误区三:Notification 和 Indication 都是“通知”,随便用

不行。

Notification 不需要确认,适合高频。

Indication 需要确认,适合关键事件。

项目 Notification Indication
Server 主动发送
Client 是否确认
吞吐 较高 较低
适合 高频数据 关键事件
类比 群消息 挂号信

19.4 误区四:Authentication Signature 每个包都有

不是。

Authentication Signature 是可选字段,只有特定安全相关场景才存在。普通 GATT 读写通知并不一定带它。

19.5 误区五:抓包看到 ATT,就和 GATT 没关系

恰好相反。

抓包看到 ATT,往往就是 GATT procedure 的底层落地形式。

你需要从:

ATT PDU

反推:

GATT 过程

再反推:

应用业务

这才是看 BLE 抓包的正确姿势。


20. 本篇总结

本篇学习了 2.5 Attribute Protocol2.5.1 Overview 前半部分,核心内容可以总结为:

  1. GATT 必须基于 ATT 实现。GATT 负责服务和特征值语义,ATT 负责协议包表达。

  2. GATT 的操作最终会落到 ATT PDU 上。发现、读取、写入、通知、指示,都有对应 ATT PDU。

  3. ATT 支持多种通信形式:command、request、response、notification、indication、confirmation。

  4. ATT PDU 的基本结构包括 Opcode、Attribute Parameters、Authentication Signature

  5. Opcode 决定包类型,Parameters 携带业务参数,Authentication Signature 是可选安全签名

  6. 抓包时看到的是 ATT,但背后对应的是 GATT 语义

一句话总结:

GATT 是蓝牙服务数据库的操作语义,ATT 是这些操作真正上路的协议包。

再土味一点:

GATT 是老板说“去查一下电量”;
ATT 是员工拿着 ATT_READ_REQ 跑去仓库查;
Opcode 是“我要查”;
Parameters 是“查哪个货架”;
Signature 是“这单子有没有防伪章”。


21. 下一篇预告

下一篇学习:

第 10 篇:Attribute 四元组:Handle、Type、Value、Permission

覆盖章节:

  • 2.5.1 Overview 后半部分

  • Figure 2.4:Logical attribute representation

下一篇开始进入 GATT/ATT 的真正数据核心:

Attribute = Handle + Type + Value + Permissions

下一篇要解决的问题是:

  • Attribute 到底是什么;

  • Handle 为什么像门牌号;

  • Type 为什么是 UUID;

  • Value 为什么是实际数据;

  • Permissions 为什么不能通过 ATT 直接读写;

  • 为什么 Attribute Handle 可以有 gaps;

  • 为什么 Service、Characteristic、Descriptor 本质上都存成 Attribute。

下一篇一句话预告:

这一篇我们拆了 ATT 这辆运输车;下一篇进仓库看货架本体:Attribute。Handle 是门牌号,Type 是标签,Value 是货,Permission 是门锁。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐