做BLE蓝牙和Socket通信的同学,大概率都遇到过这种情况:设备端和应用端对着协议文档联调,两边代码都觉得没问题,但数据一跑起来就是不对。最后抓包一看,发现是字节序反了——你按大端发的,我按小端收的,或者反过来。这种问题不复杂,但第一次遇到的时候确实挺坑。
这篇文章把我踩过的坑和查过的资料整理一下,主要讲清楚大端和小端在BLE和Socket开发里到底怎么回事,以及设备端(C++)和App端(Kotlin/Swift/uniApp)各自怎么处理。
一、什么时候会碰到字节序问题
BLE蓝牙开发里,特征值(Characteristic)的数据交互本质上是字节流传输。Socket通信也是一样,TCP/UDP传的就是一串byte。只要你传的数据不止一个字节——比如int16、int32、float这些多字节类型——字节的排列顺序就成了一个必须明确的事情。
大多数设备端(尤其是嵌入式MCU)采用的是大端模式(Big
Endian),网络协议(TCP/IP)也规定使用大端,也就是常说的”网络字节序”。但也不是所有设备都这样。比如苹果的设备,从硬件层面就是小端模式(Little
Endian)。所以如果你在做iOS和某个嵌入式设备之间的BLE通信,两端字节序不一致是大概率事件。
二、大端和小端到底是什么意思
说白了就是:一个多字节的数据,在内存里哪个字节放前面。
拿一个具体的例子。假设我们要传一个16位整数 0x1234:
1 2 3 4 5 6 7 8 9
| 大端模式(Big Endian): 字节流:12 34 低地址 → 高地址:高位字节在前,低位字节在后 人读的顺序:0x12 0x34
小端模式(Little Endian): 字节流:34 12 低地址 → 高地址:低位字节在前,高位字节在后 人读的顺序:0x34 0x12
|
再来看一个32位的例子,0x12345678:
1 2
| 大端:12 34 56 78 小端:78 56 34 12
|
就是这么简单。大端符合人的阅读习惯,从左往右看就是高位到低位。小端反过来,低位在前。
三、为什么会有两种模式,苹果为什么用小端
大端的优势很明显:符合直觉,调试的时候抓包看数据,一眼就能看出数值是多少。网络传输用大端也是这个原因——路由器、交换机处理包头的时候,按字节顺序读就行,不用转换。
小端的优势在硬件层面。CPU做算术运算的时候,从低位开始算,进位自然往高位走。小端模式下低地址就是低位字节,CPU取数直接按顺序读,不需要做地址偏移。对于频繁做数值运算的场景,小端在硬件实现上更高效。
苹果的设备(iPhone、Mac)用的都是小端,这不是苹果自己选的,而是沿用了Intel
x86和ARM架构的默认选择。x86是小端,ARM默认也是小端(虽然可以切换)。所以本质上不是”苹果采用小端”,而是苹果用的芯片架构决定了它是小端。
四、数据传输时的处理过程
实际开发中,处理流程是这样的:
发送端: 1.
确定协议规定的字节序(一般协议文档会写,没写的话默认按大端) 2.
如果本机字节序和协议规定的不一致,发送前做转换 3.
把转换后的字节流发出去
接收端: 1. 收到字节流 2.
如果本机字节序和协议规定的不一致,接收后做转换 3. 按转换后的数据解析
关键原则就一条:协议定字节序,代码做适配。
协议说大端,所有人都按大端组包和解包,不管你本机是什么端。
五、代码实例
下面给几个实际可用的代码片段。每个函数都标注了输入输出示例,方便对照理解。
5.1 设备端(C++)
C++里可以用移位操作来处理,也可以用系统提供的
htons/htonl 和
ntohs/ntohl。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
void write_uint16_be(uint8_t* buf, uint16_t value) { buf[0] = (value >> 8) & 0xFF; buf[1] = value & 0xFF; }
uint16_t read_uint16_be(const uint8_t* buf) { return (buf[0] << 8) | buf[1]; }
void write_uint32_be(uint8_t* buf, uint32_t value) { buf[0] = (value >> 24) & 0xFF; buf[1] = (value >> 16) & 0xFF; buf[2] = (value >> 8) & 0xFF; buf[3] = value & 0xFF; }
uint32_t read_uint32_be(const uint8_t* buf) { return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; }
#include <arpa/inet.h>
uint16_t host_to_be = htons(0x1234);
uint16_t be_to_host = ntohs(received_value);
|
BLE特征值发送时的典型写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
void ble_send_int32(uint32_t value) { uint8_t payload[4]; payload[0] = (value >> 24) & 0xFF; payload[1] = (value >> 16) & 0xFF; payload[2] = (value >> 8) & 0xFF; payload[3] = value & 0xFF; ble_characteristic_notify(payload, 4); }
|
5.2 App端 - Kotlin(Android)
Android设备也是小端架构,如果协议规定大端,需要做转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
fun readShortBE(bytes: ByteArray, offset: Int): Short { return ((bytes[offset].toInt() and 0xFF) shl 8 or (bytes[offset + 1].toInt() and 0xFF)).toShort() }
fun readIntBE(bytes: ByteArray, offset: Int): Int { return (bytes[offset].toInt() and 0xFF) shl 24 or (bytes[offset + 1].toInt() and 0xFF) shl 16 or (bytes[offset + 2].toInt() and 0xFF) shl 8 or (bytes[offset + 3].toInt() and 0xFF) }
fun readIntBEWithBuffer(bytes: ByteArray): Int { val buffer = ByteBuffer.wrap(bytes) buffer.order(ByteOrder.BIG_ENDIAN) return buffer.int }
fun writeIntBE(value: Int): ByteArray { val buffer = ByteBuffer.allocate(4) buffer.order(ByteOrder.BIG_ENDIAN) buffer.putInt(value) return buffer.array() }
|
BLE接收数据的典型处理:
1 2 3 4 5 6 7
| override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val data = characteristic.value val cmdId = readShortBE(data, 0) val payload = readIntBE(data, 2) }
|
5.3 App端 - Swift(iOS)
iOS是小端,处理方式和Android类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
func readUInt16BE(_ data: Data, offset: Int) -> UInt16 { let value = data.subdata(in: offset..<offset+2).withUnsafeBytes { $0.load(as: UInt16.self) } return UInt16(bigEndian: value) }
func readUInt32BE(_ data: Data, offset: Int) -> UInt32 { let value = data.subdata(in: offset..<offset+4).withUnsafeBytes { $0.load(as: UInt32.self) } return UInt32(bigEndian: value) }
let data: Data = ... var value: UInt32 = 0 _ = withUnsafeMutableBytes(of: &value) { data.copyBytes(to: $0, from: 0..<4) }
value = UInt32(bigEndian: value)
|
5.4 App端 - uniApp
uniApp做BLE开发时,数据通常以ArrayBuffer或Uint8Array形式处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
function readInt16BE(buffer, offset) { const view = new DataView(buffer); return view.getInt16(offset, false); }
function readInt32BE(buffer, offset) { const view = new DataView(buffer); return view.getInt32(offset, false); }
function writeInt32BE(value) { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setInt32(0, value, false); return buffer; }
uni.onBLECharacteristicValueChange((res) => { const buffer = res.value; const view = new DataView(buffer); const cmdId = view.getUint16(0, false); const dataValue = view.getInt32(2, false); console.log('cmdId:', cmdId, 'value:', dataValue); });
|
六、几个实际建议
协议文档一定要写清楚字节序。
不写就是埋坑,后面联调的时候双方各猜各的。
调试时抓原始字节流。 不要只看解析后的值,先看hex
dump。看到 34 12 你才知道对方发的是小端,看到
12 34 才知道是大端。
封装统一的读写函数。
别在业务代码里到处写移位操作,封装一个 ByteUtils
之类的工具类,所有地方统一调用。
用DataView/ByteBuffer这类标准API。
比手写移位安全,不容易出off-by-one的错误。
端序问题不只影响整数。
float和double也有端序问题,处理方式和整数一样,按协议规定的端序做转换就行。
字节序这个问题本身不复杂,但第一次碰到的时候确实容易懵。记住一个原则就够了:协议定端序,代码做转换,两端统一。剩下的就是写代码的时候别偷懒,老老实实按协议来。