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字节。
分包发送的逻辑其实不复杂,但要注意几个关键点:
- 包序号:每个分包需要带序号,方便接收方重组。
- 总包数:第一个包或最后一个包需要告知总包数。
- 超时重传:如果某个包丢失,需要重传。
// 分包发送示例
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不是越大越好,分包要考虑流控,写操作要根据场景选择。这些经验都是我在实际项目中踩坑踩出来的,希望对你有帮助。