4、MTU优化实战:MTU协商机制、ATT_MTU与L2CAP_MTU的关系、如何通过代码提升MTU以增加吞吐量

MTU优化,说白了就是让蓝牙一次能多传点数据。

我刚开始做BLE项目时,发现传输速率总是上不去。查了半天,原来是MTU没调。默认的23字节,传个几百字节的数据要拆成几十包,效率低得可怜。

这一章,咱们就把MTU这件事彻底讲透。

4.1 什么是MTU?为什么它这么重要?

MTU全称是Maximum Transmission Unit,最大传输单元。在BLE里,它决定了你一次能往对端发多少字节的有效数据。

核心概念:默认的ATT_MTU是23字节。其中3字节被ATT协议头占用,留给应用层的有效载荷只有20字节。如果你不主动协商,永远只能发20字节一包。

我见过不少开发者,代码里写了一大堆分包逻辑,却不知道改一下MTU就能省掉这些麻烦。你想想看,20字节和512字节,吞吐量差了多少倍?

MTU大小 有效载荷(每包) 传输1000字节所需包数 相对吞吐量提升
23(默认) 20字节 50包 1x(基准)
100 97字节 11包 约4.5x
247 244字节 5包 约10x
512 509字节 2包 约25x

看到这个表你就明白了。MTU从23提到512,包数从50降到了2。这不只是数字游戏,实际体验上,传输时间从秒级降到了毫秒级。

4.2 ATT_MTU与L2CAP_MTU的关系

这两个概念容易搞混。我刚开始学的时候也绕了一阵子。

ATT_MTU是应用层看到的MTU。它决定了你一次Write或Notify能带多少数据。

L2CAP_MTU是链路层的MTU。它包含了L2CAP的协议头(4字节)。

它们的关系很简单:

L2CAP_MTU = ATT_MTU + 4

为什么?因为ATT协议跑在L2CAP之上。每一包ATT数据,外面要套一层L2CAP的头。这4字节就是L2CAP的通道ID(2字节)和长度字段(2字节)。

我的经验:在代码里配置MTU时,通常只需要关心ATT_MTU。底层L2CAP的MTU由协议栈自动计算。但如果你在做协议栈移植或者底层驱动开发,这两个值都要确认。

举个例子。你调用API设置MTU为247,协议栈内部会做两件事:

  • 把ATT_MTU设为247
  • 把L2CAP_MTU设为251(247 + 4)

嗯,这里要注意。有些低端蓝牙芯片的L2CAP_MTU有硬件上限。比如某款芯片的L2CAP_MTU最大只支持255,那你ATT_MTU最多只能设到251。设高了也没用,底层会截断。

4.3 MTU协商机制:谁说了算?

MTU不是单方面决定的。它需要双方协商。

流程是这样的:

  1. 客户端(通常是手机)发起MTU请求,告诉服务端“我能接受的最大MTU是X”
  2. 服务端(通常是设备)收到请求后,比较自己的最大MTU和X
  3. 服务端回复一个较小的值,作为最终的MTU

说白了,就是取双方的最小值。

我曾经踩过的坑:有一次我设备端支持MTU 512,手机端也支持512,但协商结果只有247。查了半天,发现是手机蓝牙协议栈的版本问题。某些旧版Android手机,虽然声称支持512,但实际协商时只给了247。所以,不要完全相信规格书,要以协商结果为准。

协商完成后,双方都会收到一个回调,告诉你最终的MTU值。这个值在整个连接生命周期内有效,除非断开重连。

4.4 如何通过代码提升MTU

理论说完了,上代码。

我以Android和iOS两个平台为例,展示如何主动发起MTU协商。

Android端(客户端)

// 连接成功后,发起MTU请求
BluetoothGatt gatt = device.connectGatt(context, false, gattCallback);

// 在连接回调中请求MTU
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    if (newState == BluetoothProfile.STATE_CONNECTED) {
        // 请求MTU为512
        boolean success = gatt.requestMtu(512);
        if (success) {
            Log.d("MTU", "MTU请求已发送");
        }
    }
}

// 接收MTU协商结果
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d("MTU", "协商成功,当前MTU = " + mtu);
        // 这里的mtu就是最终的ATT_MTU
        // 后续发送数据时,每包最大可以发 mtu - 3 字节
    } else {
        Log.e("MTU", "MTU协商失败,status = " + status);
    }
}

iOS端(客户端)

// 连接成功后,请求MTU
- (void)centralManager:(CBCentralManager *)central 
  didConnectPeripheral:(CBPeripheral *)peripheral {
    
    // 请求最大MTU(iOS会自动协商到双方支持的最大值)
    [peripheral maximumWriteValueLengthForType:CBCharacteristicWriteWithResponse];
    
    // 或者显式请求
    [peripheral requestMaximumWriteValueLength:512];
}

// 接收MTU变更回调
- (void)peripheral:(CBPeripheral *)peripheral 
  didWriteValueForCharacteristic:(CBCharacteristic *)characteristic 
                           error:(NSError *)error {
    
    // 获取当前最大写入长度
    NSInteger mtu = [peripheral maximumWriteValueLengthForType:
                     CBCharacteristicWriteWithResponse];
    NSLog(@"当前MTU = %ld", (long)mtu);
}

我的建议:不要一上来就请求最大MTU。我习惯先请求一个中间值(比如247),确认协商成功后,再逐步增大。这样能避免某些设备因为MTU过大而崩溃。

设备端(服务端)

设备端通常不需要主动发起协商,但需要正确响应客户端的请求。以Nordic nRF5 SDK为例:

// 在GATT事件回调中处理MTU请求
static void on_ble_evt(ble_evt_t const * p_ble_evt) {
    switch (p_ble_evt->header.evt_id) {
        case BLE_GAP_EVT_CONNECTED:
            // 连接建立,准备接收MTU请求
            break;
            
        case BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST:
            // 收到客户端的MTU请求
            uint16_t client_mtu = p_ble_evt->evt.gatts_evt.params.
                                  exchange_mtu_request.client_rx_mtu;
            uint16_t server_mtu = NRF_SDH_BLE_GATT_MAX_MTU_SIZE;
            
            // 取最小值
            uint16_t final_mtu = MIN(client_mtu, server_mtu);
            
            // 回复协商结果
            sd_ble_gatts_exchange_mtu_reply(
                p_ble_evt->evt.gatts_evt.conn_handle, 
                final_mtu
            );
            
            // 更新应用层的MTU
            app_mtu = final_mtu;
            break;
    }
}

4.5 MTU优化后的实际效果

我拿一个实际项目的数据说话。

之前做一款数据采集设备,每次上传2000字节的传感器数据。默认MTU 23时,需要发100包。加上包间间隔和ACK,耗时约800ms。

优化后MTU提到512,只需要4包。耗时降到40ms。吞吐量提升了20倍。

关键结论:MTU优化是BLE性能优化里投入产出比最高的操作。改几行代码,吞吐量翻几倍甚至几十倍。没有理由不做。

4.6 避坑指南

最后,分享几个我实际踩过的坑。

  • 不要假设对方支持大MTU。有些老旧设备或低功耗芯片,最大只支持23。协商失败后要优雅降级。
  • MTU协商要在数据传输前完成。我见过有人边传数据边协商,结果数据包和协商包冲突,导致连接断开。
  • 注意MTU和连接间隔的关系。MTU大了,每包数据多了,但连接间隔没变,吞吐量提升有限。这两个参数要配合优化。
  • Android碎片化问题。不同厂商的Android手机,MTU协商行为不一样。有些手机协商成功但实际写入长度受限。建议协商后主动测试一次最大写入。

我曾经遇到的一个奇葩问题:某款手机协商MTU为512成功,但实际写入超过200字节就丢包。排查后发现是手机蓝牙驱动有bug。最后只能限制写入长度为200字节,虽然MTU是512。所以,信任但验证。

好了,MTU优化就讲到这里。下一节咱们聊连接间隔和连接事件,这两个参数和MTU配合好了,吞吐量还能再上一个台阶。

MTU协商与数据流关系图 客户端(手机) 服务端(设备) ① 请求MTU=512 ② 回复MTU=247(取最小值) 最终ATT_MTU = 247 数据流传输 每包有效载荷 = ATT_MTU - 3 = 244字节 L2CAP_MTU = ATT_MTU + 4 = 251字节 ATT_MTU:应用层可见的最大数据单元 L2CAP_MTU:链路层实际传输单元(含4字节头)