2、GATT协议基础回顾:GATT角色、Service与Characteristic结构、MTU与PDU
各位好,欢迎来到第二章。在咱们正式聊信号弱化场景下的优化之前,我觉得有必要先把GATT协议的基础再过一遍。为什么?因为很多性能问题,说白了就是基础没打牢。我见过不少工程师,一上来就调参数、改代码,结果发现连角色都搞反了——那优化自然无从谈起。
这一章,咱们就踏踏实实地把GATT的骨架理清楚。嗯,内容不复杂,但很重要。你想想看,如果连Service和Characteristic都分不清,怎么去设计一个高效的GATT服务?
2.1 GATT角色:Server与Client
GATT定义了两个角色:Server和Client。这个关系其实很简单——Server提供数据,Client消费数据。但我在项目中遇到过不少新手,把这两个角色跟蓝牙经典模式里的主从设备搞混了。
咱们用个表格来对比一下:
| 角色 | 职责 | 典型设备 | 数据流向 |
|---|---|---|---|
| GATT Server | 存储并暴露Service、Characteristic | 传感器、心率带、温湿度计 | 响应Client的读写请求,主动Notify |
| GATT Client | 发现Service、读写Characteristic | 手机App、网关、手表 | 发起请求,接收Notification |
这里有个关键点:一个设备可以同时是Server和Client。比如你的手机,它既可以是Server(暴露自己的电量服务),也可以是Client(读取手环的心率数据)。我个人习惯在设计初期,先画一张角色关系图,避免后面逻辑混乱。
核心原则:谁提供数据,谁就是Server;谁消费数据,谁就是Client。跟主从关系没有必然联系。
2.2 Service与Characteristic结构
GATT的数据模型是分层的。最顶层是Profile,下面包含多个Service,每个Service包含多个Characteristic,每个Characteristic又包含Value和Descriptor。说白了,这就是一个树形结构。
咱们用一张SVG图来直观感受一下:
在实际开发中,我建议你把Service设计得尽量内聚。比如一个温度传感器,就把温度相关的所有数据放在同一个Service里,别把温度值和电量值混在一起。这样做的好处是——Client发现服务时,可以一次性获取完整的功能集合,减少交互次数。
Characteristic的四个关键属性
每个Characteristic都有属性,决定了Client能对它做什么。咱们用表格列一下:
| 属性 | 含义 | 典型场景 |
|---|---|---|
| Read | Client可以读取Value | 读取设备序列号 |
| Write | Client可以写入Value | 设置目标温度 |
| Notify | Server主动推送数据,无需Client确认 | 实时心率数据 |
| Indicate | Server主动推送数据,需要Client确认 | 关键告警,确保送达 |
我的经验:在信号弱化场景下,Notify比Indicate更高效。因为Indicate需要等待确认,一旦丢包重传,延迟会急剧增加。我曾经在一个项目中,把Indicate改成Notify,配合合理的重传机制,吞吐量提升了将近40%。
2.3 MTU与PDU
好,接下来咱们聊聊MTU和PDU。这两个概念,是GATT性能优化的核心。说白了,它们决定了你一次能传多少数据。
MTU:最大传输单元
MTU(Maximum Transmission Unit)指的是ATT层一次能发送的最大数据包大小。默认值是23字节,其中3字节是ATT头,所以实际有效载荷只有20字节。
但你可以通过MTU协商,把这个值变大。比如协商到247字节,有效载荷就是244字节。你想想看,一次传244字节和一次传20字节,效率差了多少?
MTU协商的过程是这样的:
- Client发送MTU请求,告诉Server自己支持的最大MTU
- Server回复MTU响应,告诉Client自己支持的最大MTU
- 双方取较小值,作为本次连接的MTU
注意:MTU协商只能在连接建立后进行一次。如果你错过了这个时机,就只能用默认的23字节。我曾经见过一个产品,因为MTU协商失败,导致固件升级速度慢得离谱——传一个100KB的固件,花了将近10分钟。
PDU:协议数据单元
PDU(Protocol Data Unit)是ATT层的数据包格式。每个PDU包含操作码、参数和认证签名(可选)。不同的操作,PDU的结构也不同。
咱们看看最常见的几种PDU:
| 操作 | PDU结构 | 说明 |
|---|---|---|
| Read Request | Opcode (1B) + Handle (2B) | Client请求读取某个Handle的值 |
| Read Response | Opcode (1B) + Value (可变) | Server返回读取到的数据 |
| Write Request | Opcode (1B) + Handle (2B) + Value (可变) | Client写入数据,需要Server确认 |
| Write Command | Opcode (1B) + Handle (2B) + Value (可变) | Client写入数据,不需要确认 |
| Notification | Opcode (1B) + Handle (2B) + Value (可变) | Server主动推送数据,无确认 |
| Indication | Opcode (1B) + Handle (2B) + Value (可变) | Server主动推送数据,需要确认 |
这里有个细节:PDU的大小不能超过MTU。如果你要发送的数据很大,就需要拆分成多个PDU。在信号弱化场景下,拆分越少,成功率越高。所以,尽量协商一个大的MTU,是优化第一步。
实战建议:在代码初始化阶段,主动发起MTU协商。以Android为例,使用BluetoothGatt.requestMtu()方法,传入设备支持的最大值。iOS则需要在peripheral(_:didWriteValueFor:error:)回调中处理。我个人习惯把MTU协商放在连接成功后的第一个操作,确保后续所有数据传输都能受益。
2.4 小结
这一章咱们回顾了GATT的基础:角色分工、数据模型、MTU和PDU。这些东西看似简单,但它们是后续所有优化的基石。你想想看,如果连Server和Client都搞反了,或者MTU没协商好,后面再怎么调参数也是白搭。