概念
ATT是什么
ATT(属性协议)定义了BLE中客户端(Client)和服务器(Server)之间如何发现、读取、写入和通知数据,
ATT是一种轻量级的,无状态的,基于属性的数据交换协议
属性(Attribute)构成
每个属性都由以上四部分组成
- 属性句柄 (Attribute Handle)
定义:一个 16 位(2 字节)的无符号整数,取值范围为 0x0001 到 0xFFFF。
作用:作为属性在属性表中的唯一标识符和地址。它类似于数据库中的主键 (Primary Key)。
特性:- 唯一性:在一个设备(GATT 服务器)的属性表中,每个句柄都是唯一的。
- 有序性:句柄值是严格递增的。
- 高效性:在设备连接后,客户端和服务器之间的所有数据读写操作都通过这个简短的句柄来指定目标属性,而不是使用冗长的 UUID,从而极大地提高了通信效率和降低了功耗。
- 属性类型 (Attribute Type)
定义:一个 UUID (Universally Unique Identifier),用于说明该属性中“值 (Value)”代表的是什么。它类似于数据库中的列类型或模式 (Schema)。
作用:为属性赋予语义。客户端通过识别类型 UUID 来理解一个属性的用途。
分类:- 蓝牙官方定义 (SIG-defined) UUID:16 位短 UUID,用于表示标准的服务、特征和描述符。例如:
- 0x2800: 主服务声明
- 0x2803: 特征声明
- 0x2A37: 心率测量特征
- 自定义 (Vendor-specific) UUID:128 位长 UUID,用于厂商自定义的私有服务和特征。
- 蓝牙官方定义 (SIG-defined) UUID:16 位短 UUID,用于表示标准的服务、特征和描述符。例如:
- 属性值 (Attribute Value)
定义:属性所承载的实际数据,是一个长度可变的字节序列。
作用:存放应用程序的数据或协议的结构化信息。
特性:- 格式自由:ATT/GATT 协议本身不关心值的具体内容,它只负责传输字节流。如何解析这些字节(例如,解析成一个整数、一个字符串或一个复杂的结构体)完全由应用层负责。
- 长度可变:值的长度可以从 0 字节到 ATT_MTU (最大传输单元) 限制的字节数。
- 属性权限 ( Attribute Permissions)
定义:一组标志位,用于规定客户端可以对该属性执行哪些操作。
作用:为属性提供安全和访问控制。
常见的权限:- 读 (Read):属性值可被读取。
- 写 (Write):属性值可被写入。
- 无响应写 (Write Without Response):可被写入,且服务器不发送响应。
- 认证读/写 (Authenticated Read/Write):需要经过加密和认证的安全连接才能读/写。
- 授权读/写 (Authorization Read/Write):服务器应用层需要对每次读/写请求进行授权。
注意:权限本身不是属性值的一部分,而是与句柄关联的、由服务器协议栈管理的元数据。
Attribute table 属性表
属性表是由一条一条的属性构成的,如:
构建层次
- 服务声明 (Service Declaration)
- Type:0x2800 (主服务) 或 0x2801 (次服务)
- Value: 所声明的服务的 UUID (例如,0x180D 代表心率服务)
- 特征声明 (Characteristic Declaration)
- Type:0x2803
- Value:分为3部分
- 特征属性:1字节
- 特征值句柄,是个指针,2字节
- 特征UUID:2或16字节
- 特征值 (Characteristic Value)
- Type:该特征的UUID
- Value:真实的应用数据
- 描述符 (Descriptor)
- Type:描述自己的UUID,如果是0x2902那就是CCCD
- Value:描述数据
service/characteristic是attribute的逻辑表现形式,而attribute是service/characteristic具体实现方式
ATT和Attribute的关系
是协议和数据的关系
ATT 协议定义了一系列命令(请求和响应),这些命令的唯一操作对象,就是服务器上的一个个 Attribute
1 客户端 (Client) 的意图:我想知道这个设备的制造商是谁。通过服务发现,客户端已经知道“制造商名称”这个 Attribute 的句柄 (Handle) 是 0x0003。
2 客户端使用 ATT 协议:客户端会构建一个 ATT Read Request PDU。这个数据包的内容很简单,就是:[Opcode: ATT_READ_REQUEST, Handle: 0x0003]。然后通过底层的 L2CAP 发送出去。
3 服务器 (Server) 收到 ATT 命令:服务器的 BLE 协议栈收到了这个 ATT 数据包。它解析后知道:
这是一个读请求 (因为 Opcode 是 ATT_READ_REQUEST)。
请求的目标是句柄 0x0003。
4 服务器操作 Attribute:服务器会在自己的“属性表”中查找句柄为 0x0003 的那个 Attribute。
它找到了这个 Attribute。
它检查这个 Attribute 的权限 (Permissions),发现是可读的。
它读取这个 Attribute 的值 (Value),得到 [ "G", "o", "o", "g", "l", "e" ]。
5 服务器使用 ATT 协议响应:服务器会构建一个 ATT Read Response PDU。数据包内容为:[Opcode: ATT_READ_RESPONSE, Value: [ "G", "o", "o", "g", "l", "e" ]]。然后发送回客户端。
6 客户端收到 ATT 响应:客户端收到响应包,解析后就知道 0x0003 的值是 "Google"。任务完成。
ATT与GATT关系
- ATT(Attribute Protocol)是 BLE 的底层协议,负责点对点地封装 Attribute Table 的读写、通知、发现等操作。
- GATT(Generic Attribute Profile)则是在 ATT 之上,定义了 Attribute Table 的逻辑组织方式(service、characteristic、descriptor),以及标准化的服务发现流程和访问规则
GATT是对Attribute Table的逻辑组织,一般开发不需要面对ATT,只要面对GATT就行。
如果没有GATT的话,可以开发BLE程序吗,是可以的。你需要手动管理和维护Attribute Table,直接操作对应的句柄,来进行读写或通知。因为没有GATT,所以客户端和服务端需要互相约定好Attribute Table的内容,你想暴露的服务的句柄是多少,有什么权限,然后就可以正常通信了。可以看到,虽然能正常通信,但是失去了扩展性和维护性,如果双方互不知情的情况下,仅靠Attribute Table来通信是十分困难的
ATT 操作
ATT PDU结构
+--------+--------+-----------------+
| Opcode | Handle | Value/Data |
+--------+--------+-----------------+
| 1 byte | 2 byte | N bytes |
+--------+--------+-----------------+
- Opcode:PDU承载的命令
- Handle:PDU要操作的句柄(请求包包含Handle,回应包没有Handle)
- parameter:参数
Opcode 类型
主要分六类
错误响应 (Error Response)
| 命令名称 | Opcode | 方向 | 描述 |
|---|---|---|---|
| Error Response | 0x01 | S→C | 用于响应一个失败的请求。值中会包含:1. 失败的请求的Opcode。2. 失败的属性的Handle。 3. 一个具体的错误码(如“属性未找到”、“权限不足”等)。 |
交换MTU(Exchange MTU)
| 命令名称 | Opcode | 方向 | 描述 |
|---|---|---|---|
| Exchange MTU Request | 0x02 | C→S | 客户端发起,告知服务器自己能够支持的ATT_MTU大小 |
| Exchange MTU Response | 0x03 | S→C | 服务器响应,告知客户端自己能够支持的ATT_MTU大小。最终生效的MTU是两者支持值中的较小者 |
查找信息(Find Information)
| 命令名称 | Opcode | 方向 | 描述 |
|---|---|---|---|
| Find Information Request | 0x04 | C→S | 客户端请求在一个给定的句柄范围内(例如0x0001-0xFFFF),查找所有属性的句柄及其类型UUID。是服务发现的核心命令 |
| Find Information Response | 0x05 | S→C | 服务器返回找到的句柄和类型UUID对的列表 |
| Find By Type Value Request | 0x06 | C→S | 在句柄范围内,查找具有特定类型UUID和特定值的属性。主要用于查找服务声明 |
| Find By Type Value Response | 0x07 | S→C | 服务器返回找到的属性的起始和结束句柄 |
读操作 (Reading Attributes)
| 命令名称 | Opcode | 方向 | 描述 |
|---|---|---|---|
| Read By Type Reques | 0x08 | C→S | 在句柄范围内,读取所有具有特定类型UUID的属性的值。例如,读取一个服务中所有的特征声明 |
| Read By Type Response | 0x09 | S→C | 服务器返回一个包含(句柄,值)对的列表 |
| Read Request | 0x0A | C→S | 最常用的读命令。通过一个精确的句柄来读取一个属性的值 |
| Read Response | 0x0B | S→C | |
| Read Blob Request | 0x0C | C→S | 用于读取一个很长(超过ATT_MTU)的属性值的一部分。请求中会包含一个偏移量(Offset) |
| Read Blob Response | 0x0D | S→C | 服务器返回从指定偏移量开始的那部分属性值 |
| Read Multiple Request | 0x0E | C→S | 一次性读取多个句柄对应的属性值(前提是这些值加起来不能超过ATT_MTU) |
| Read Multiple Response | 0x0F | S→C | 服务器返回一个包含所有请求的值的集合 |
| Read By Group Type Request | 0x10 | C→S | 在句柄范围内,根据分组类型UUID(如主服务0x2800)来读取属性。用于发现服务 |
| Read By Group Type Response | 0x11 | S→C | 服务器返回一个列表,包含每个服务的(起始句柄,结束句柄,服务UUID) |
写操作 (Writing Attributes)
| 命令名称 | Opcode | 方向 | 描述 |
|---|---|---|---|
| Write Request | 0x12 | C→S | 需要响应的写操作。客户端向指定句柄写入一个值,并期望服务器返回一个Write Response确认写入成功 |
| Write Response | 0x13 | S→C | 服务器在成功处理Write Request后发送的确认响应 |
| Write Command | 0x52 | C→S | 无响应的写操作。客户端向指定句柄写入一个值,但不要求服务器确认。速度更快,但可靠性较低 |
| Prepare Write Request | 0x16 | C→S | 用于写入长属性值的第一步。发送要写入的数据的一部分(片段)到服务器的准备队列中 |
| Prepare Write Response | 0x17 | S→C | 服务器确认已收到该数据片段,并返回收到的内容供客户端校验 |
| Execute Write Request | 0x18 | C→S | 当所有数据片段都发送完毕后,客户端发送此命令,要求服务器执行或取消所有在队列中的写入操作 |
| Execute Write Response | 0x19 | S→C | 服务器确认已执行或取消写入操作 |
| Signed Write Command | 0x20 | C→S | 一种带签名的无响应写操作,用于在未加密的连接上验证数据来源,增强安全性 |
服务器发起的操作 (Server Initiated)
| 命令名称 | Opcode | 方向 | 描述 |
|---|---|---|---|
| Handle Value Notification | 0x1B | S→C | 通知 (Notification)。服务器主动将一个句柄的最新值发送给客户端。这是一种“发后不理”的操作,服务器不期望收到响应 |
| Handle Value Indication | 0x1D | S→C | 指示 (Indication)。服务器主动将一个句柄的最新值发送给客户端,并期望客户端返回一个确认 |
| Handle Value Confirmation | 0x1E | C→S | 确认 (Confirmation)。客户端在收到一个Indication后,必须向服务器发送此命令,以确认数据已收到 |
响应类型
上面所有的操作,按照响应类型来区分,可以分为三类
- 同步命令,需要一对一的句柄,回复PDU不需要句柄,因为上下文是明确的
- 异步命令、服务器主动发起的通知、返回列表、发现结果响应,必须包含句柄,客户端需要知道数据条目来自哪里
- 错误响应,客户端的某个请求无法被正常处理,必须包含句柄
无句柄响应
ATT_READ_RSP(读响应)- Opcode:
0x0B - 场景: 客户端发送
ATT_READ_REQ(带有句柄) 后,服务器返回此响应。 - 原因: 这是一个严格的同步“问-答”模式,客户端知道这个响应就是对自己刚刚那个读请求的回答。
- Opcode:
ATT_WRITE_RSP(写响应)- Opcode:
0x13 - 场景: 客户端发送
ATT_WRITE_REQ(带有句柄) 后,服务器返回此响应以确认写入成功。 - 原因: 同上,是同步的确认操作。
- Opcode:
ATT_PREPARE_WRITE_RSP(准备长写响应)- Opcode:
0x17 - 场景: 在分包写入长数据时,服务器对每一个数据包的响应。
- 原因: 同样是同步确认,但它会回显收到的部分数据以供校验,而不是回显句柄。
- Opcode:
ATT_EXECUTE_WRITE_RSP(执行长写响应)- Opcode:
0x19 - 场景: 客户端发送执行写入命令后,服务器的最终确认。
- 原因: 对事务的最终同步确认。
- Opcode:
ATT_READ_BLOB_RSP(读长属性响应)- Opcode:
0x0D - 场景: 读取一个属性值的一部分。
- 原因: 和
ATT_READ_RSP类似,是同步的,所以它本身不包含句柄。
- Opcode:
ATT_HANDLE_VALUE_CFM(指示确认)- Opcode:
0x1E - 场景: 这是客户端用来回应服务器的
ATT_HANDLE_VALUE_IND的。 - 原因: 客户端收到指示后,发送这个PDU来告诉服务器“我收到了”。服务器知道这个确认是针对它刚刚发送的那个指示,所以无需句柄。
- Opcode:
有句柄响应或通知
这类PDU要么是服务器主动发起的,要么是返回一个“列表”结果,因此必须明确指出每个数据项对应的句柄。
ATT_HANDLE_VALUE_NOTI(通知)- Opcode:
0x1B - 场景: 服务器主动向客户端发送数据。
- 原因: 异步操作。客户端没有预先请求,所以服务器必须用句柄来告知“这个数据来自心率特征”或“这个数据来自电量特征”。
- Opcode:
ATT_HANDLE_VALUE_IND(指示)- Opcode:
0x1D - 场景: 与通知类似,但需要客户端确认。
- 原因: 同上,是异步的、服务器发起的通信。
- Opcode:
ATT_READ_BY_TYPE_RSP(按类型读取响应)- Opcode:
0x09 - 场景: 客户端请求“给我所有UUID为
0x2A37的属性值”。 - 原因: 响应是一个列表,可能包含多个符合条件的属性。每个条目都必须包含自己的句柄和对应的值,否则客户端无法区分。
- Opcode:
ATT_READ_BY_GROUP_TYPE_RSP(按组类型读取响应)- Opcode:
0x11 - 场景: 客户端进行服务发现时,请求“给我所有的主要服务”。
- 原因: 响应是一个服务列表。每个条目都需要包含服务的起始句柄、结束句柄和服务的UUID。
- Opcode:
ATT_FIND_INFORMATION_RSP(查找信息响应)- Opcode:
0x05 - 场景: 客户端请求“请告诉我句柄范围
0x0001到0xFFFF内所有属性的句柄和类型”。 - 原因: 响应是属性的列表,每个条目自然包含句柄和其类型UUID。
- Opcode:
错误响应
ATT_ERROR_RSP(错误响应)- Opcode:
0x01 - 场景: 客户端的某个请求无法被正确处理。
- 原因: 这个响应包含句柄。它的载荷里包含了导致错误的请求的操作码和该请求中使用的属性句柄。这样做是为了让客户端能够明确地知道是“哪一个请求”的“哪一个句柄”出了问题,这对于调试至关重要。
- Opcode:
总结
| PDU 名称 | 包含句柄? | 为什么? |
|---|---|---|
| 读/写/执行 响应 | 否 | 同步的“一对一”确认,上下文明确。 |
| 通知/指示 | 是 | 异步的、服务器发起的事件,必须指明数据来源。 |
| 按类型/组/信息 查找响应 | 是 | 响应内容是一个列表,每个条目都必须有自己的句柄标识。 |
| 错误响应 | 是 | 为了精确诊断,需要指明是哪个请求的哪个句柄出错了。 |
| 指示确认 (客户端->服务器) | 否 | 对服务器指示的同步确认,上下文明确。 |
ATT操作细节
场景设定:
客户端 (Client): 智能手机
服务器 (Server): 一个包含标准“设备信息服务”(Device Information Service)的BLE外设。
目标: 手机App希望读取该外设的“制造商名称”。
前提: 客户端已经完成了连接和GATT服务发现。它通过解析服务器的属性表,已经知道了“制造商名称字符串”这个特征值的句柄(Handle)是 0x0012。
| 句柄 (Handle) | 属性类型 (UUID) | 权限 | 属性值 (Value) |
|---|---|---|---|
0x0011 | 0x2803 (Characteristic Declaration) | Read Only | 声明:这是一个只读特征,其值在句柄 0x0012,UUID为 0x2A29 |
0x0012 | 0x2A29 (Manufacturer Name String) | Read Only | "MyBLEDevice" (ASCII编码) |
详细过程
C-> S
OpCode: 0A(ATT_READ_REQ)
Handle: 0x0012
S->C
Opcode: 0B(ATT_READ_RSP)
Value: 4D 79 42 4C 45 44 65 76 69 63 65(MyBLEDevice)