网络数据传输中的大端与小端:一个BLE和Socket开发者的踩坑笔记

做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/htonlntohs/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
/**
* 把uint16_t按大端写入字节流
*
* 输入:value = 0x1234, buf指向一个至少2字节的缓冲区
* 输出:buf[0]=0x12, buf[1]=0x34(即大端排列:高位在前,低位在后)
*/
void write_uint16_be(uint8_t* buf, uint16_t value) {
buf[0] = (value >> 8) & 0xFF; // value >> 8:把value右移8位,原来高8位的0x12移到低8位
// & 0xFF:位与运算,特点是用0xFF(二进制11111111)做掩码,
// 把高24位全部清零,只保留最低8位,确保结果在0~255范围内
buf[1] = value & 0xFF; // value & 0xFF:直接取value的低8位0x34,高8位被掩码清零
}

/**
* 从字节流按大端读取uint16_t
*
* 输入:buf[0]=0x12, buf[1]=0x34
* 输出:返回 0x1234
*/
uint16_t read_uint16_be(const uint8_t* buf) {
return (buf[0] << 8) | buf[1]; // buf[0] << 8:把第一个字节左移8位,0x12变成0x1200
// | buf[1]:位或运算,特点是按位取或,0x1200 | 0x34 = 0x1234
// 相当于把两个单字节拼回一个双字节整数
}

/**
* 把uint32_t按大端写入字节流
*
* 输入:value = 0x12345678, buf指向一个至少4字节的缓冲区
* 输出:buf[0]=0x12, buf[1]=0x34, buf[2]=0x56, buf[3]=0x78
*/
void write_uint32_be(uint8_t* buf, uint32_t value) {
buf[0] = (value >> 24) & 0xFF; // 右移24位,0x12移到低8位,&0xFF取最低字节
buf[1] = (value >> 16) & 0xFF; // 右移16位,0x34移到低8位,&0xFF取最低字节
buf[2] = (value >> 8) & 0xFF; // 右移8位,0x56移到低8位,&0xFF取最低字节
buf[3] = value & 0xFF; // 直接取最低字节0x78
}

/**
* 从字节流按大端读取uint32_t
*
* 输入:buf[0]=0x12, buf[1]=0x34, buf[2]=0x56, buf[3]=0x78
* 输出:返回 0x12345678
*/
uint32_t read_uint32_be(const uint8_t* buf) {
return (buf[0] << 24) | // 第1字节移到最高8位:0x12 << 24 = 0x12000000
(buf[1] << 16) | // 第2字节移到次高8位:0x34 << 16 = 0x00340000
(buf[2] << 8) | // 第3字节移到次低8位:0x56 << 8 = 0x00005600
buf[3]; // 第4字节保持原位: 0x00000078
// 四个值位或拼合:0x12345678
}

/**
* 方法二:用系统函数(Linux/嵌入式Linux可用)
* 系统函数内部已经处理好了端序转换,不需要自己写移位
*/
#include <arpa/inet.h>

// 主机序转大端(发送前调用)
uint16_t host_to_be = htons(0x1234); // 小端机器上:host_to_be = 0x3412 → 转成 0x1234

// 大端转主机序(接收后调用)
uint16_t be_to_host = ntohs(received_value); // 大端数据received_value=0x1234 → 小端机器上 be_to_host = 0x1234

BLE特征值发送时的典型写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* BLE发送一个int32,按大端
*
* 输入:value = 0xAABBCCDD
* 输出:payload = {0xAA, 0xBB, 0xCC, 0xDD},通过BLE发出
*/
void ble_send_int32(uint32_t value) {
uint8_t payload[4];
payload[0] = (value >> 24) & 0xFF; // 取最高字节0xAA
payload[1] = (value >> 16) & 0xFF; // 取次高字节0xBB
payload[2] = (value >> 8) & 0xFF; // 取次低字节0xCC
payload[3] = value & 0xFF; // 取最低字节0xDD
// 把payload通过BLE characteristic发出去
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
/**
* 从字节流按大端读取Short
*
* 输入:bytes = [0x12, 0x34], offset = 0
* 输出:返回 0x1234 (即十进制 4660)
*/
fun readShortBE(bytes: ByteArray, offset: Int): Short {
return ((bytes[offset].toInt() and 0xFF) shl 8 or // bytes[0].toInt() = 0x12,and 0xFF保持值不变
// shl 8:左移8位变成 0x1200
(bytes[offset + 1].toInt() and 0xFF)).toShort() // bytes[1].toInt() = 0x34,and 0xFF保持值不变
// or:位或拼合 0x1200 | 0x34 = 0x1234
}

/**
* 从字节流按大端读取Int
*
* 输入:bytes = [0x12, 0x34, 0x56, 0x78], offset = 0
* 输出:返回 0x12345678 (即十进制 305419896)
*/
fun readIntBE(bytes: ByteArray, offset: Int): Int {
return (bytes[offset].toInt() and 0xFF) shl 24 or // 第1字节移到最高8位:0x12 << 24
(bytes[offset + 1].toInt() and 0xFF) shl 16 or // 第2字节移到次高8位:0x34 << 16
(bytes[offset + 2].toInt() and 0xFF) shl 8 or // 第3字节移到次低8位:0x56 << 8
(bytes[offset + 3].toInt() and 0xFF) // 第4字节保持原位:0x78
// 四个值位或拼合:0x12345678
}

/**
* ByteBuffer方式(更简洁,推荐)
*
* 输入:bytes = [0x12, 0x34, 0x56, 0x78]
* 输出:返回 0x12345678
*/
fun readIntBEWithBuffer(bytes: ByteArray): Int {
val buffer = ByteBuffer.wrap(bytes)
buffer.order(ByteOrder.BIG_ENDIAN) // 指定按大端解析
return buffer.int // ByteBuffer自动按指定端序拼合4字节返回int
}

/**
* 写入时指定大端
*
* 输入:value = 0x12345678
* 输出:返回 ByteArray [0x12, 0x34, 0x56, 0x78]
*/
fun writeIntBE(value: Int): ByteArray {
val buffer = ByteBuffer.allocate(4)
buffer.order(ByteOrder.BIG_ENDIAN) // 指定大端排列
buffer.putInt(value) // ByteBuffer自动把int拆成4字节按大端写入
return buffer.array()
}

BLE接收数据的典型处理:

1
2
3
4
5
6
7
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val data = characteristic.value // ByteArray,原始字节流
// 假设协议:前2字节是命令ID(大端),后4字节是数据(大端)
val cmdId = readShortBE(data, 0) // 从data[0]开始读2字节,大端拼成short
val payload = readIntBE(data, 2) // 从data[2]开始读4字节,大端拼成int
// 处理数据...
}

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
/**
* 从Data按大端读取UInt16
*
* 输入:data = Data([0x12, 0x34]), offset = 0
* 输出:返回 0x1234
*/
func readUInt16BE(_ data: Data, offset: Int) -> UInt16 {
let value = data.subdata(in: offset..<offset+2).withUnsafeBytes {
$0.load(as: UInt16.self) // 从Data中按内存布局加载一个UInt16
// 在小端机器上,如果原始字节是12 34,load出来是0x3412
}
return UInt16(bigEndian: value) // bigEndian:把value从大端表示转为主机序
// 0x3412 → 0x1234(小端机器上做了字节翻转)
}

/**
* 从Data按大端读取UInt32
*
* 输入:data = Data([0x12, 0x34, 0x56, 0x78]), offset = 0
* 输出:返回 0x12345678
*/
func readUInt32BE(_ data: Data, offset: Int) -> UInt32 {
let value = data.subdata(in: offset..<offset+4).withUnsafeBytes {
$0.load(as: UInt32.self) // 加载4字节,小端机器上原始12 34 56 78 → load出 0x78563412
}
return UInt32(bigEndian: value) // 0x78563412 → 0x12345678
}

/**
* 用Data的API处理
*
* 输入:data = Data([0x12, 0x34, 0x56, 0x78])
* 输出:value = 0x12345678
*/
let data: Data = ... // 从BLE收到的数据
var value: UInt32 = 0
_ = withUnsafeMutableBytes(of: &value) { data.copyBytes(to: $0, from: 0..<4) }
// copyBytes把data的前4字节拷贝到value的内存中
// 小端机器上value内存是 78 56 34 12,所以value = 0x78563412
value = UInt32(bigEndian: value)
// bigEndian把字节翻转:0x78563412 → 0x12345678,得到正确的大端解析值

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
/**
* 从ArrayBuffer按大端读取16位整数
*
* 输入:buffer包含字节 [0x12, 0x34], offset = 0
* 输出:返回 0x1234 (即十进制 4660)
*/
function readInt16BE(buffer, offset) {
const view = new DataView(buffer);
return view.getInt16(offset, false); // false表示大端模式
// DataView自动把第offset和offset+1两个字节按大端拼成int16
}

/**
* 从ArrayBuffer按大端读取32位整数
*
* 输入:buffer包含字节 [0x12, 0x34, 0x56, 0x78], offset = 0
* 输出:返回 0x12345678 (即十进制 305419896)
*/
function readInt32BE(buffer, offset) {
const view = new DataView(buffer);
return view.getInt32(offset, false); // false表示大端模式
// DataView自动把4字节按大端拼成int32
}

/**
* 写入32位整数,按大端
*
* 输入:value = 0x12345678
* 输出:buffer包含字节 [0x12, 0x34, 0x56, 0x78]
*/
function writeInt32BE(value) {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setInt32(0, value, false); // false表示大端排列
// DataView自动把value拆成4字节,按大端顺序写入buffer
return buffer;
}

/**
* uniApp BLE接收示例
*/
uni.onBLECharacteristicValueChange((res) => {
const buffer = res.value; // ArrayBuffer,BLE收到的原始字节流
const view = new DataView(buffer);
const cmdId = view.getUint16(0, false); // 大端读命令ID,如buffer前2字节是[0x00, 0x01] → 返回1
const dataValue = view.getInt32(2, false); // 大端读数据,如buffer接着4字节是[0x00,0x00,0x00,0x64] → 返回100
console.log('cmdId:', cmdId, 'value:', dataValue);
});

六、几个实际建议

  1. 协议文档一定要写清楚字节序。 不写就是埋坑,后面联调的时候双方各猜各的。

  2. 调试时抓原始字节流。 不要只看解析后的值,先看hex dump。看到 34 12 你才知道对方发的是小端,看到 12 34 才知道是大端。

  3. 封装统一的读写函数。 别在业务代码里到处写移位操作,封装一个 ByteUtils 之类的工具类,所有地方统一调用。

  4. 用DataView/ByteBuffer这类标准API。 比手写移位安全,不容易出off-by-one的错误。

  5. 端序问题不只影响整数。 float和double也有端序问题,处理方式和整数一样,按协议规定的端序做转换就行。


字节序这个问题本身不复杂,但第一次碰到的时候确实容易懵。记住一个原则就够了:协议定端序,代码做转换,两端统一。剩下的就是写代码的时候别偷懒,老老实实按协议来。


网络数据传输中的大端与小端:一个BLE和Socket开发者的踩坑笔记
https://jycpp.github.io/2026/26-08-29-网络数据传输中的大端与小端.html
作者
Jet Yan
发布于
2026年8月29日
许可协议