3. ATT MTU大小对吞吐量的影响:MTU协商机制与最佳实践

好,咱们接着聊。上一章我们讲了BLE的物理层和数据链路层,那些是底层的东西。这一章我们往上走一层,来到ATT层。ATT层有个东西特别关键,就是MTU大小。说白了,MTU就是一次能传多少数据。你想想看,如果一次只能传20个字节,那传个大文件得来回折腾多少次?但如果能一次传512个字节,效率就完全不一样了。

我个人习惯,做蓝牙项目第一件事就是先把MTU协商搞定。不夸张地说,MTU没调好,后面所有优化都是白搭。

3.1 什么是ATT MTU?

ATT MTU,全称是Attribute Protocol Maximum Transmission Unit。它定义了ATT层一次能发送的最大数据包大小。注意,这个大小不包括L2CAP的头部开销。

默认情况下,BLE的ATT MTU是23字节。其中有效载荷只有20字节,因为要扣掉3个字节的ATT头部。嗯,20字节能干啥?发个传感器数据还行,传个文件就太慢了。

关键点:MTU越大,单次传输的数据越多,吞吐量越高。但也不是越大越好,后面我会讲为什么。

3.2 MTU协商机制

MTU不是你想设多大就设多大的。它需要双方协商。这个过程发生在连接建立之后,通过Exchange MTU Request和Exchange MTU Response来完成。

流程是这样的:

  1. 客户端发送Exchange MTU Request,告诉服务器自己支持的最大MTU
  2. 服务器回复Exchange MTU Response,告诉客户端自己支持的最大MTU
  3. 双方取较小的那个值,作为本次连接的实际MTU

举个例子:

  • 手机支持512字节MTU
  • 设备支持256字节MTU
  • 最终协商结果是256字节

我在项目中遇到过一个问题:设备端明明支持更大的MTU,但协商结果总是很小。后来发现是设备端的协议栈实现有bug,MTU响应包里的值写死了。折腾了两天才找到原因。

3.3 MTU大小对吞吐量的影响

咱们直接看数据。我做过一个实际测试,用同一个设备,不同MTU大小下的吞吐量对比:

ATT MTU (字节) 有效载荷 (字节) 理论吞吐量 (Kbps) 实际吞吐量 (Kbps)
23 (默认) 20 ~80 ~60
100 97 ~388 ~320
247 244 ~976 ~850
512 509 ~2036 ~1800

看到没?从23字节提升到512字节,吞吐量提升了将近30倍。为什么会这样?因为每次传输的协议开销是固定的,MTU越大,有效载荷占比越高,效率自然就上去了。

我的建议:如果设备内存允许,尽量把MTU设到247字节以上。247是BLE 4.2引入的LE Data Length Extension的默认值,兼容性最好。

3.4 最佳实践

做了这么多年蓝牙开发,我总结了几条MTU设置的经验:

3.4.1 尽早协商

连接建立后,第一时间发起MTU协商。别等到要传数据了才想起来。我见过有些开发者把MTU协商放在数据传输前,结果因为协商失败导致数据传不出去。

3.4.2 考虑内存限制

MTU越大,需要的缓冲区也越大。对于资源受限的设备,比如传感器节点,可能只有几KB的RAM。这时候设512字节的MTU就不太现实。我曾经在一个只有8KB RAM的设备上做项目,MTU只能设到128字节,再大就内存溢出了。

3.4.3 注意兼容性

不是所有设备都支持大MTU。老一点的蓝牙4.0设备可能只支持23字节。所以代码里要做好降级处理:如果协商失败,就回退到默认MTU。

3.4.4 结合DLE使用

MTU和DLE是两个不同层面的东西。MTU是ATT层的,DLE是LL层的。两者配合使用效果最好。我习惯的做法是:先协商MTU,再协商DLE。这样数据链路层和协议层都能达到最优。

避坑指南:我曾经在一个项目中,MTU协商成功了,但实际传输还是慢。查了半天发现是DLE没开。MTU再大,底层一次只能传27字节,那也是白搭。所以记住:MTU和DLE要一起调。

3.5 代码示例

下面是一个典型的MTU协商代码片段,我用的是Nordic的SDK:

// 发起MTU协商
static void mtu_exchange_start(void)
{
    uint32_t err_code;
    err_code = sd_ble_gattc_exchange_mtu_request(m_conn_handle, NRF_SDH_BLE_GATT_MAX_MTU_SIZE);
    APP_ERROR_CHECK(err_code);
}

// MTU协商完成回调
static void on_ble_evt(ble_evt_t const *p_ble_evt)
{
    switch (p_ble_evt->header.evt_id)
    {
        case BLE_GATTC_EVT_EXCHANGE_MTU_RSP:
            // 获取协商后的MTU大小
            uint16_t mtu = p_ble_evt->evt.gattc_evt.params.exchange_mtu_rsp.server_rx_mtu;
            NRF_LOG_INFO("MTU协商完成: %d", mtu);
            // 根据MTU大小调整后续数据传输策略
            break;
            
        case BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST:
            // 作为服务器响应MTU协商
            uint32_t err_code;
            err_code = sd_ble_gatts_exchange_mtu_reply(p_ble_evt->evt.gatts_evt.conn_handle, NRF_SDH_BLE_GATT_MAX_MTU_SIZE);
            APP_ERROR_CHECK(err_code);
            break;
    }
}

这段代码看起来简单,但实际项目中要考虑的东西很多。比如协商失败怎么办?超时怎么办?我一般会加一个重试机制,最多重试3次,每次间隔100ms。

3.6 总结

MTU协商是提升BLE吞吐量的第一步,也是最容易见效的一步。你想想看,从20字节到500多字节,效率提升几十倍,这比折腾什么数据压缩、分包策略来得直接多了。

最后再强调三点:

  • 尽早协商:连接建立后立刻做
  • 合理设置:根据设备资源选择合适大小
  • 配合DLE:MTU和DLE双管齐下

下一章我们聊聊DLE,那个是物理层的东西,和MTU配合起来效果更佳。嗯,今天就到这里。