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图来直观感受一下:

GATT数据模型结构图 GATT 数据模型结构 Profile Service 1 Service 2 Characteristic 1 Characteristic 2 Value Descriptor 说明: • Profile:应用规范,包含多个Service • Service:功能集合,如心率服务 • Characteristic:数据单元,含Value+Descriptor • Descriptor:描述属性,如CCC配置通知开关 Characteristic 3 Characteristic 4 每个Characteristic有唯一的UUID,Value存放实际数据,Descriptor提供配置信息

在实际开发中,我建议你把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协商的过程是这样的:

  1. Client发送MTU请求,告诉Server自己支持的最大MTU
  2. Server回复MTU响应,告诉Client自己支持的最大MTU
  3. 双方取较小值,作为本次连接的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没协商好,后面再怎么调参数也是白搭。