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。
流程其实很简单:
- Client发起请求:Client发送MTU Request,带上自己的MTU值。
- Server响应:Server回复MTU Response,带上自己的MTU值。
- 取最小值:双方取两个值中较小的那个,作为本次连接的MTU。
举个例子。Client报247,Server报512。最终MTU就是247。反过来,Client报100,Server报200,最终就是100。
我的习惯:在初始化代码里,连接建立后立刻发起MTU交换。别等要发数据了才做,那时候已经慢了。
下面这张图,把MTU交换的完整流程画出来了:
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,我一般按这几步走:
- 查芯片手册:先看自己芯片支持的最大MTU。比如nRF52840支持247,CC2640R2F支持255。
- 发起协商:连接建立后,立刻发起MTU Request,带上自己的最大值。
- 读取对端响应:拿到对端的MTU值,取最小值。
- 实际测试:用最终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是吞吐量的天花板。天花板太低,下面再怎么优化也有限。