2、MTU交换与数据吞吐量

聊GATT性能,MTU是个绕不开的坎儿。我见过不少团队,花大把精力优化算法、压缩数据,结果吞吐量死活上不去。一查,MTU还是默认的23字节。说白了,你连“车道宽度”都没调,车跑得再快也没用。

这一节,咱们就把MTU掰开揉碎。从概念到流程,再到怎么调最优,一次说透。

2.1 MTU的概念

MTU,全称Maximum Transmission Unit,最大传输单元。在BLE里,它指的是一个ATT数据包能承载的最大字节数。

默认值是多少?23字节。其中3字节是ATT头(操作码+句柄),真正能放数据的只有20字节。你想想看,传一个100字节的传感器数据,得拆成5包。每包都有协议开销,效率能高才怪。

MTU协商成功之后,这个值可以往上提。BLE 4.2之后,理论上最大能到247字节(实际受LL层限制)。数据包大了,同样数据需要的包数就少了,吞吐量自然就上去了。

核心结论:MTU越大,单包承载数据越多,有效吞吐量越高。但MTU不是越大越好,后面会讲。

2.2 MTU交换流程

MTU交换发生在连接建立之后。它不是一个“设置”操作,而是一个“协商”过程。双方各自报一个自己能接受的最大值,取小的那个作为最终MTU。

流程其实很简单:

  1. Client发起请求:Client发送MTU Request,带上自己的MTU值。
  2. Server响应:Server回复MTU Response,带上自己的MTU值。
  3. 取最小值:双方取两个值中较小的那个,作为本次连接的MTU。

举个例子。Client报247,Server报512。最终MTU就是247。反过来,Client报100,Server报200,最终就是100。

我的习惯:在初始化代码里,连接建立后立刻发起MTU交换。别等要发数据了才做,那时候已经慢了。

下面这张图,把MTU交换的完整流程画出来了:

MTU交换流程图 GATT Client GATT Server 连接建立 ① MTU Request (247) ② MTU Response (512) ③ 最终MTU = min(247, 512) = 247

2.3 MTU对吞吐量的影响

MTU对吞吐量的影响,可以用一个公式来算:

有效吞吐量 = (MTU - 3) × 包数 / 总时间

这里的3是ATT头开销。MTU越大,单包有效载荷越大,同样数据需要的包数越少,总时间就越短。

我拿一个实际项目的数据给你看。当时传一个512字节的固件升级包:

MTU值 单包有效载荷 所需包数 理论吞吐量提升
23(默认) 20字节 26包 基准
100 97字节 6包 约4.3倍
247 244字节 3包 约8.7倍

看到没?从23提到247,包数从26降到了3。吞吐量直接翻了将近9倍。这就是为什么我说MTU是性能调优的第一道门。

注意:MTU提升后,吞吐量不会线性增长。因为还有连接间隔、延迟、CPU处理能力等瓶颈。但MTU是基础,基础不牢,后面全白搭。

2.4 如何协商最优MTU

最优MTU不是越大越好。我踩过这个坑。有一次把MTU设到247,结果对端芯片处理不过来,频繁丢包。后来降到180,反而更稳定。

协商最优MTU,我一般按这几步走:

  1. 查芯片手册:先看自己芯片支持的最大MTU。比如nRF52840支持247,CC2640R2F支持255。
  2. 发起协商:连接建立后,立刻发起MTU Request,带上自己的最大值。
  3. 读取对端响应:拿到对端的MTU值,取最小值。
  4. 实际测试:用最终MTU跑数据,观察丢包率和重传率。如果丢包率高,降一档再试。

代码实现也很简单。以Zephyr为例:

// 发起MTU交换
int err = bt_gatt_exchange_mtu(conn, NULL, NULL);
if (err) {
    printk("MTU exchange failed: %d\n", err);
}

// 获取协商后的MTU
uint16_t mtu = bt_gatt_get_mtu(conn);
printk("Negotiated MTU: %d\n", mtu);

我的经验:如果对端是手机,iOS和Android的MTU行为不一样。iOS一般支持247,Android碎片化严重,有的只支持100。我习惯在App端先查一下,再决定怎么协商。

最后说一个避坑点。我曾经在一个项目里,把MTU协商放在数据发送前才做。结果每次连接都要等几百毫秒才能发数据。后来改成连接建立后立刻协商,用户体验好多了。

嗯,MTU这块就这些。记住一句话:MTU是吞吐量的天花板。天花板太低,下面再怎么优化也有限。