【Bluetooth-SIG】【CoreV6.2】【Vol3 Part G】【GATT】【九】【Attribute Protocol Overview:GATT 的运输车 ATT】
第 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 的关系可以画成这样:
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。
例如读取电量:
这里有三层意思:
-
App 层 说“我要读电量”;
-
GATT 层 把它理解成 “Read Characteristic Value”;
-
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 中。
先用一张时序图看整体:
这几个名字非常重要。后面读 GATT feature requirements 时,会反复遇到。
6. Command:写了就跑,不等回信
Command 的典型代表是:
ATT_WRITE_CMD
它常用于 Write Without Response。
也就是说,Client 把数据发给 Server,不要求 Server 返回 ATT response。
它的优点是快。
因为不用等响应,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
这种模型适合:
-
读取设备名;
-
读取电量;
-
读取固件版本;
-
写入配置;
-
写入控制命令;
-
写 CCCD 打开通知;
-
需要知道成功或失败的操作。
Request / Response 的特点是清楚:
Client 发起请求,Server 必须给结果。
成功就返回正常 Response,失败就返回 Error Response。
所以它像银行柜台:
你递材料,柜员必须告诉你:办好了,或者哪里不合格。
不会像 Command 那样“材料一扔,人就跑了”。
8. Notification:Server 主动喊一声
Notification 是 Server 主动发给 Client 的数据。
典型 ATT PDU 是:
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
Indication 更适合:
-
Service Changed;
-
关键状态变化;
-
重要控制事件;
-
低频但不希望悄悄丢掉的数据。
Notification 像群消息,Indication 像挂号信。
Notification:我通知你了,不等你回复。
Indication:我通知你了,你必须签收。
工程上要注意:
Server 发出 Indication 后,需要等 Client 的 Confirmation。
如果 Client 不确认,后续 Indication 可能会被阻塞。
所以 Indication 可靠一些,但吞吐不如 Notification。别拿它当高速数据流通道用,不然就像拿挂号信发实时 IMU 数据,邮局都想辞职。
10. ATT PDU 的基本结构
Figure 2.3 展示了 ATT PDU 的基本结构。它由三部分组成:
-
Opcode:1 octet;
-
Attribute Parameters:可变长度;
-
Authentication Signature:存在时为 12 octets。
转成 Mermaid packet 图:
这里要注意:图里的 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 变化了。
画成图:
工程抓包时,不要只看 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
可以把它理解成防伪封条。
通俗讲:
普通快递:业务类型 + 包裹内容。
签名快递:业务类型 + 包裹内容 + 防伪签名。
这个签名不是每包都有,也不是 App 层平时经常直接处理的东西。但做协议栈、做安全、做底层抓包时,必须知道 ATT PDU 结构里有这个可选尾巴。
14. GATT Procedure 到 ATT PDU 的映射思路
本篇不是完整讲 Section 4.13,但这里可以提前建立映射思路。
GATT 后续很多 procedure,最终都会映射到 ATT opcode。规范后面的映射表会列出例如 Notification 对应 ATT_HANDLE_VALUE_NTF,Indication 对应 ATT_HANDLE_VALUE_IND 和 ATT_HANDLE_VALUE_CFM,长写和可靠写会使用 Prepare Write 与 Execute Write 相关 PDU。
先看几个常见映射:
理解这个映射后,抓包就不再是一堆陌生缩写。
例如:
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,大概是这样:
这里面有几个关键点:
-
App 关心“电量”;
-
GATT 关心“Battery Level Characteristic”;
-
ATT 关心“handle = 0x0025”;
-
Server 按 handle 找 Attribute Value;
-
返回的 value 再被上层解释成 85%。
所以协议学习不能只停在 API 层。
如果你只知道 readCharacteristic(),遇到抓包和协议栈问题就容易懵。
如果你知道它底层是 ATT_READ_REQ,并且参数里是 Attribute Handle,那排查问题就有方向了。
16. 一次 Notification 操作完整拆解
再看 Notification。
假设温度传感器周期性上报温度。
这里有一个重要细节:
Notification 是 Server 主动发,但通常要以 Client 已经配置 CCCD 为前提。
也就是说:
先订阅,再推送。
如果 Client 没写 CCCD,Server 通常不应该随便给它发 Notification。
这也是 Android BLE 开发中经典大坑:
setCharacteristicNotification() 只是本地开关;
真正让远端 Server 发通知,通常还要写 CCCD。
17. 抓包时应该怎么看 ATT?
抓包时建议按三步看:
第一步:看 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_REQ 或 ATT_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 Protocol 和 2.5.1 Overview 前半部分,核心内容可以总结为:
-
GATT 必须基于 ATT 实现。GATT 负责服务和特征值语义,ATT 负责协议包表达。
-
GATT 的操作最终会落到 ATT PDU 上。发现、读取、写入、通知、指示,都有对应 ATT PDU。
-
ATT 支持多种通信形式:command、request、response、notification、indication、confirmation。
-
ATT PDU 的基本结构包括 Opcode、Attribute Parameters、Authentication Signature。
-
Opcode 决定包类型,Parameters 携带业务参数,Authentication Signature 是可选安全签名。
-
抓包时看到的是 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 是门锁。
更多推荐

所有评论(0)