12、MTU协商与大数据传输

各位同学,今天我们来聊聊蓝牙开发中一个非常实际的问题——MTU协商与大数据传输。

说实话,我刚入行那会儿,觉得蓝牙传输就是简单的“发数据、收数据”。直到第一次遇到传输大文件时,数据莫名其妙丢失,设备还频繁断连……嗯,那时候我才意识到,MTU这个东西,不搞明白真的会翻车。

12.1 MTU的概念

MTU,全称是Maximum Transmission Unit,最大传输单元。说白了,就是蓝牙协议栈在一次数据包中能承载的最大字节数。

在BLE(低功耗蓝牙)中,默认的MTU是23字节。注意,这23字节里,有3个字节是协议头(操作码、句柄等),真正能传的数据只有20字节。你想想看,传一个几百KB的文件,每次只能塞20字节,效率得多低?

核心要点:MTU越大,单次传输的数据量越大,传输效率越高。但MTU也不是越大越好,受限于设备内存和射频性能。

我在项目中遇到过一款老式蓝牙芯片,MTU协商到512字节后,设备直接死机。后来发现是它的接收缓冲区只有256字节。所以,MTU协商不是“越大越好”,而是“合适才好”。

12.2 requestMtu协商

在Android中,发起MTU协商的API是BluetoothGatt.requestMtu(int mtu)。这个调用是客户端主动发起的,服务端(外设)会响应一个onMtuChanged回调。

// 发起MTU协商
BluetoothGatt gatt = device.connectGatt(context, false, gattCallback);
gatt.requestMtu(512);

// 回调处理
BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
    @Override
    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            Log.d("MTU", "协商成功,MTU = " + mtu);
            // 实际协商结果可能小于请求值
        } else {
            Log.e("MTU", "协商失败,status = " + status);
        }
    }
};

这里有个坑,我必须要提醒你:requestMtu必须在连接建立之后、发现服务之前调用?还是之后?

我个人习惯是在onServicesDiscovered回调之后发起协商。为什么?因为有些外设需要在服务发现完成后才能正确处理MTU请求。我曾经在onConnectionStateChange里直接调requestMtu,结果外设直接返回错误——嗯,后来查了芯片手册才知道,它要求先发现服务。

小技巧:协商MTU时,建议先请求一个较大的值(比如512),然后根据回调结果确定实际MTU。不要假设请求值一定会被接受。

12.3 分包发送与重组

当你要发送的数据超过MTU时,就需要分包。比如MTU是100字节,你要发500字节的数据,那就得拆成5个包,每个包不超过100字节。

分包发送的逻辑其实不复杂,但要注意几个关键点:

  1. 包序号:每个分包需要带序号,方便接收方重组。
  2. 总包数:第一个包或最后一个包需要告知总包数。
  3. 超时重传:如果某个包丢失,需要重传。
// 分包发送示例
private void sendLargeData(BluetoothGatt gatt, byte[] data, int mtu) {
    int maxPayload = mtu - 3; // 减去协议头
    int totalPackets = (int) Math.ceil((double) data.length / maxPayload);
    
    for (int i = 0; i < totalPackets; i++) {
        int offset = i * maxPayload;
        int length = Math.min(maxPayload, data.length - offset);
        byte[] packet = new byte[length + 4]; // 4字节包头
        
        // 包头:包序号(2字节) + 总包数(2字节)
        packet[0] = (byte) (i >> 8);
        packet[1] = (byte) (i & 0xFF);
        packet[2] = (byte) (totalPackets >> 8);
        packet[3] = (byte) (totalPackets & 0xFF);
        
        // 数据
        System.arraycopy(data, offset, packet, 4, length);
        
        // 发送
        gatt.writeCharacteristic(characteristic, packet, WRITE_TYPE_DEFAULT);
    }
}

接收方的重组逻辑就是反过来:收到包后,根据序号排序,等所有包到齐后拼接成完整数据。

注意:分包发送时,一定要考虑流控。如果发送速度太快,接收方来不及处理,会导致丢包。我建议每发送一个包,等待onCharacteristicWrite回调后再发下一个。

12.4 写操作:Write Without Response vs Write Request

这是蓝牙开发中另一个容易混淆的点。两种写操作的区别,我用一张表来说明:

特性 Write Request Write Without Response
确认机制 需要接收方确认 不需要确认
可靠性 高(有重传机制) 低(可能丢包)
速度 慢(等待确认) 快(无需等待)
应用场景 关键数据、控制指令 大量数据、实时性要求高

我个人习惯是:控制类指令用Write Request,确保对方收到;大数据传输用Write Without Response,配合应用层的确认机制。

为什么?你想想看,如果你传一个大文件,每个包都要等确认,那速度会慢得让人抓狂。但如果你完全不确认,丢包了也不知道。所以折中方案是:用Write Without Response快速发送,然后在应用层做批量确认。

// Write Request
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
gatt.writeCharacteristic(characteristic);

// Write Without Response
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
gatt.writeCharacteristic(characteristic);

避坑指南:我曾经在一个项目中,用Write Without Response发送固件升级包,结果因为射频干扰导致中间几个包丢失,设备变砖了。后来我加了一个应用层的ACK机制——每发送50个包,等待接收方回复一个确认包,丢失的包重传。这样既保证了速度,又保证了可靠性。

12.5 知识体系总览

下面这张图总结了本章的核心内容,你可以对照着梳理思路:

MTU协商与大数据传输知识体系 MTU协商与大数据传输 MTU概念 默认23字节 有效载荷20字节 requestMtu协商 客户端主动发起 服务发现后调用 分包发送与重组 包序号+总包数 超时重传机制 写操作类型 Write Request 可靠、慢 控制指令 Write Without Response 不可靠、快 大数据传输 图例 核心概念 关键操作

好了,关于MTU协商与大数据传输的内容就讲到这里。记住,MTU不是越大越好,分包要考虑流控,写操作要根据场景选择。这些经验都是我在实际项目中踩坑踩出来的,希望对你有帮助。


公众号:蓝海资料掘金营,微信deep3321