3. GATT 操作与流程:Read、Write、Notify、Indicate 详解

GATT 操作,说白了就是蓝牙设备之间怎么交换数据。我刚开始做蓝牙开发时,总觉得 GATT 就是几个读写函数调一调。后来被坑了几次才明白——操作类型选不对,数据就丢了;MTU 没协商好,吞吐量上不去;Connection Interval 设错了,功耗和速度全乱套。

这一章,咱们就把这些核心操作掰开揉碎讲清楚。

3.1 四大操作:Read、Write、Notify、Indicate

GATT 定义了四种数据交互方式。每种都有自己的适用场景,选错了会出大问题。

操作类型 方向 是否需要应答 典型场景
Read Client → Server 是(Server 回复数据) 读取设备信息、状态查询
Write Client → Server 可选(Write Request/Command) 下发配置、控制指令
Notify Server → Client 否(无应答) 实时数据推送(传感器、心率)
Indicate Server → Client 是(Client 回复确认) 关键事件上报(报警、状态变更)

3.1.1 Read 操作

Read 是最基础的操作。Client 发一个 Read Request,Server 把 Attribute Value 返回。

这里有个坑——一次 Read 最多能读多少字节? 答案是:取决于 MTU 大小。默认 MTU 是 23 字节,减去 3 字节的 ATT 头部,实际有效载荷只有 20 字节。如果你要读的数据超过 20 字节,就得用 Read Blob Request 分片读取。

我的习惯: 对于超过 20 字节的数据,我一般直接用 Notify 推送,或者先做 MTU 交换把 MTU 撑大。分片读取虽然能用,但效率低,代码也容易出 bug。

3.1.2 Write 操作

Write 分两种:Write Request 和 Write Command。

  • Write Request:Client 发数据,Server 必须回复 Write Response。可靠,但慢。
  • Write Command:Client 发数据,Server 不回复。快,但不可靠。

我遇到过一个问题:用 Write Command 下发固件升级数据,结果丢包了,设备直接变砖。后来我改成 Write Request,虽然慢了点,但至少能保证每一包都到了。

注意: 写操作同样受 MTU 限制。一次 Write 最多能写 (MTU - 3) 字节。超过这个长度,必须分片或者用 Prepare Write + Execute Write 组合。

3.1.3 Notify 与 Indicate

这两个是 Server 主动给 Client 推数据的操作。区别在于:

  • Notify:Server 发完就不管了,Client 不需要回复。速度快,但可能丢包。
  • Indicate:Server 发完,Client 必须回复确认。可靠,但慢。

你想想看,心率传感器每秒出 10 个数据,用 Indicate 的话,每个数据都要等确认,吞吐量直接腰斩。所以心率数据一般用 Notify。但如果是报警信号——比如温度超限——那就必须用 Indicate,确保 App 收到了。

核心原则:
  • 数据量大、允许偶尔丢包 → Notify
  • 数据量小、必须可靠 → Indicate

3.2 MTU 交换流程

MTU(Maximum Transmission Unit)决定了单次 ATT 包能传多少数据。默认 MTU 是 23 字节,但大部分现代蓝牙芯片都支持更大的 MTU,比如 247 字节甚至 512 字节。

MTU 交换的流程很简单:

  1. Client 发送 MTU Request,告诉 Server:“我能支持的最大 MTU 是 X”。
  2. Server 回复 MTU Response,告诉 Client:“我能支持的最大 MTU 是 Y”。
  3. 双方取 min(X, Y) 作为本次连接的 MTU。

举个例子:Client 说我能到 247,Server 说我能到 128,那最终 MTU 就是 128。有效载荷就是 128 - 3 = 125 字节。

我建议: 连接建立后,第一时间做 MTU 交换。不要等到传数据时才发现 MTU 太小。我在一个项目中吃过这个亏——默认 MTU 传固件包,一包 20 字节,传了 10 分钟。后来改成 247 MTU,3 分钟搞定。

MTU 交换的时机也很重要。有些芯片在连接后会自动协商,有些需要手动触发。我的习惯是:连接建立后 100ms 内发起 MTU Request,避免后续操作因为 MTU 太小而失败。

3.3 Connection Interval 对吞吐量的影响

Connection Interval 是蓝牙连接中两个连接事件之间的时间间隔。单位是 1.25ms,范围从 7.5ms 到 4s。

Connection Interval 越小,吞吐量越高,但功耗也越大。

咱们算一笔账:

  • 假设 MTU 是 247 字节,有效载荷 244 字节(减去 L2CAP 头部)。
  • 每个连接事件可以传 4 个包(取决于芯片和协议栈)。
  • Connection Interval 是 7.5ms,那一秒就有 133 个连接事件。

理论吞吐量 = 244 字节 × 4 包 × 133 事件/秒 ≈ 130 KB/s。

但如果 Connection Interval 是 50ms,那一秒只有 20 个事件,吞吐量就掉到 244 × 4 × 20 ≈ 19.5 KB/s。

Connection Interval 事件数/秒 理论吞吐量(MTU=247, 4包/事件) 典型场景
7.5ms 133 ~130 KB/s 音频流、大数据传输
15ms 66 ~64 KB/s 传感器数据流
30ms 33 ~32 KB/s 周期性状态上报
50ms 20 ~19 KB/s 低功耗设备
注意: 实际吞吐量会比理论值低 20%-30%,因为还有协议开销、CPU 处理延迟、射频干扰等因素。我在项目中实测过,7.5ms 间隔下,实际吞吐量大概在 90-100 KB/s 左右。

Connection Interval 不是你想设多少就设多少。它需要 Client 和 Server 协商。Client 发起 Connection Update Request,Server 决定是否接受。如果 Server 不接受,连接就会断开。

我的经验是:不要一上来就设 7.5ms。先设一个中间值,比如 15ms,看看连接稳定性。如果稳定,再逐步减小。有些老手机不支持 7.5ms,强行设会导致断连。

3.4 知识体系总览

下面这张图把 GATT 操作、MTU 交换、Connection Interval 的关系串起来了。你看完应该能对整个流程有个全局认识。

GATT 操作与流程知识体系 GATT 操作与流程知识体系 GATT 操作 Read Write Notify Indicate MTU 交换 Connection Interval 关键关系: • MTU 决定单包数据量,Connection Interval 决定发包频率 • 吞吐量 = 单包数据量 × 包数/事件 × 事件数/秒 • Notify 无应答,适合高吞吐;Indicate 有应答,适合高可靠

3.5 实战建议

讲了这么多理论,最后给几条实战建议:

  • 连接后先做 MTU 交换,别等传数据时才发现不够用。
  • 根据数据特性选操作类型:大数据流用 Notify,关键事件用 Indicate。
  • Connection Interval 从中间值开始调,稳定后再往小改。别一上来就 7.5ms。
  • 实测吞吐量:理论值只是参考,实际跑一下才知道真实性能。

一句话总结: GATT 操作选型看可靠性需求,MTU 决定单包大小,Connection Interval 决定发包频率。三者配合好了,吞吐量才能上去。