后面打算写Qt关于网络编程的博客,网络编程就绕不开字节流数据传输,字节流数据的传输一般是根据协议来定义对应的报文该如何组包,那这就必然牵扯到了大端字节序和小端字节序的问题了。不清楚的大小端的可以看一下相关资料:大小端模式_百度百科 (baidu.com)。
这里看一个具体的例子比如某个报文协议是这样定的:
|------|-----------|
| 报头 | ... |
| 设备编号 | U16(两个字节) |
| 设备温度 | U16(两个字节) |
| 设备湿度 | U16(两个字节) |
| 设备状态 | U16(两个字节) |
| 报尾 | ... |
那么传输过程的报文结构,除去头尾之外应该是这样的:
代码报文结构体这样定义
cpp
struct DeviceData {
quint16 number;
quint16 temperature;
quint16 humidness;
quint16 status;
};
然后简单测试一下
cpp
QByteArray data;
DeviceData d;
d.number = 0x1234;
d.temperature = 0x5678;
d.humidness = 0x4321;
d.status = 0x8756;
data.append(reinterpret_cast<char *>(&d), sizeof(DeviceData));
qDebug() << data.toHex();
编译运行查看一下打印结果:
如果看了前面关于大小端的资料应该就会明白为什么这里打印结果是"3412785621435687",这里也有一个参考的文章:大小端格式由编译器,操作系统还是CPU决定的?答案是CPU_大端cpu采用小端编译链-CSDN博客我的机器cpu,如果不采取任何处理,这里输出的确实是小端数据,如果是大端那么就会输出对应的"1234567843218756"。如果上文中举例的协议定的就是小端数据传输那么就是这样写,无需做任何处理。如果是大端数据传输则需要做对应的处理。同样上文中还存在一个问题,比如设备编号协议里面定的是无符号一个字节即uint8类型的。那么结构体将会这样定义:
cpp
struct DeviceData {
quint8 number;
quint16 temperature;
quint16 humidness;
quint16 status;
};
测试代码:
cpp
QByteArray data;
DeviceData d;
d.number = 0x12;
d.temperature = 0x5678;
d.humidness = 0x4321;
d.status = 0x8756;
data.append(reinterpret_cast<char *>(&d), sizeof(DeviceData));
qDebug() << data.toHex();
这里将结构体转QByteArray使用了reinterpret_cast,可以自己查一下static_cast、dymatic_cast、reinterpret_cast以及Qt的qobject_cast有什么区别。同样还可以使用QByteArray的setRawData方法:
QByteArray data;
data.setRawData(reinterpret_cast<char *>(&d), sizeof(DeviceData));
结果也是一样的。
将对应QByteArray转回结构体直接使用memcpy即可。例如上面的例子:
DeviceData dd;
memcpy(&dd, data.constData(), sizeof(DeviceData));
打印输出:
为什么打印是这样,可以先看看这个结构体的大小,打印 sizeof(DeviceData)可以看到是8个字节,结构体成员一个quint8,三个quint16,大小:1+2+2+2为什么是8而不是7, 这个就需要了解关于字节对齐的知识了:字节对齐_百度百科 (baidu.com)
直接采用1字节对齐:
cpp
#pragma pack(push, 1);
struct DeviceData {
quint8 number;
quint16 temperature;
quint16 humidness;
quint16 status;
};
#pragma pack(pop);
再查看打印:
然后sizeof(DeviceData)也是7了(这里使用1字节对齐会影响效率)。·
回到关于大小端的问题,代码里面采用的是结构体转QByteArray,这样就牵扯到了依靠系统自己的大小端来处理了,代码不灵活。可以写一个通用的方法来根据需求转换对应需要的字节序。
方法一:
思路是一个字节一个字节进行拷贝使用QByteArray的append方法以及位移操作。
比如一个无符号四字节的quint32 u32=0x12345678(大端就是12345678,小端就是78563412),先执行下列代码:
cpp
quint32 u32 = 0x12345678;
QByteArray data;
data.append(u32);
qDebug() << data.toHex();
查看打印:
也就是说QByteArray的 append方法在这种情况下并不会将整数 u32
转换为字节流,而是将整数的低字节(最低有效字节)追加到 QByteArray中。
转化为小端数据:
|----------|-------|----------|----------------------|
| 原生数据 | 操作 | 原生数据 | QByteArray进行append追加 |
| 12345678 | 右移0位 | 12345678 | 78 |
| 12345678 | 右移8位 | 00123456 | 7856 |
| 00123456 | 右移16位 | 00001234 | 785634 |
| 00001234 | 右移24位 | 00000012 | 78563412 |
转化为大端数据:
|----------|-------|----------|----------------------|
| 原生数据 | 操作 | 原生数据 | QByteArray进行append追加 |
| 12345678 | 右移24位 | 00000012 | 12 |
| 12345678 | 右移16位 | 00001234 | 1234 |
| 00123456 | 右移8位 | 00123456 | 123456 |
| 00001234 | 右移0位 | 12345678 | 12345678 |
对应代码:
cpp
quint32 u32 = 0x12345678;
//输出小端数据
QByteArray littleEndian;
littleEndian.append(u32);
littleEndian.append(u32 >> 8);
littleEndian.append(u32 >> 16);
littleEndian.append(u32 >> 24);
qDebug() << "little:" << littleEndian.toHex();
//输出大端数据
QByteArray bigEndian;
bigEndian.append(u32 >> 24);
bigEndian.append(u32 >> 16);
bigEndian.append(u32 >> 8);
bigEndian.append(u32);
qDebug() << "bigEndian:" << bigEndian.toHex();
编译运行查看打印:
对应转回同理,下面是完整代码:
cpp
quint32 u32 = 0x12345678;
//输出小端数据
QByteArray littleEndian;
littleEndian.append(u32);
littleEndian.append(u32 >> 8);
littleEndian.append(u32 >> 16);
littleEndian.append(u32 >> 24);
qDebug() << "littleEndian:" << littleEndian.toHex();
quint32 u32x = 0;
u32x |= static_cast<quint8>(littleEndian[0]);
u32x |= (static_cast<quint8>(littleEndian[1]) << 8);
u32x |= (static_cast<quint8>(littleEndian[2]) << 16);
u32x |= (static_cast<quint8>(littleEndian[3]) << 24);
qDebug() << "ori data:" << u32x;
//输出大端数据
QByteArray bigEndian;
bigEndian.append(u32 >> 24);
bigEndian.append(u32 >> 16);
bigEndian.append(u32 >> 8);
bigEndian.append(u32);
qDebug() << "bigEndian:" << bigEndian.toHex();
quint32 u32y = 0;
u32y |= (static_cast<quint8>(bigEndian[0]) << 24);
u32y |= (static_cast<quint8>(bigEndian[1]) << 16);
u32y |= (static_cast<quint8>(bigEndian[2]) << 8);
u32y |= static_cast<quint8>(bigEndian[3]);
qDebug() << "ori data:" << u32y;
编译运行查看打印:
305419896对应的16进制就是0x12345678:
其他数据类型同理,这里写成模板函数:
cpp
template <typename T>
static QByteArray toData(const T &value, bool isLittle) {
QByteArray data;
for (int i = 0; i < sizeof(T); ++i) {
int bitOffset = (isLittle) ? i : sizeof(T) - i - 1;
data.append(value >> bitOffset * 8);
}
return data;
}
template <typename T>
static void fromData(const QByteArray &data, bool isLittle, T &value) {
for (int i = 0; i < sizeof(T); ++i) {
int bitOffset = (isLittle) ? i : sizeof(T) - i - 1;
value |= (static_cast<quint8>(data[i]) << bitOffset * 8);
}
}
上面例子代码改为:
cpp
quint32 u32 = 0x12345678;
//输出小端数据
QByteArray littleEndian = toData(u32, true);
qDebug() << "littleEndian:" << littleEndian.toHex();
quint32 u32x = 0;
fromData(littleEndian, true, u32x);
qDebug() << "ori data:" << u32x;
//输出大端数据
QByteArray bigEndian = toData(u32, false);
qDebug() << "bigEndian:" << bigEndian.toHex();
quint32 u32y = 0;
fromData(bigEndian, false, u32y);
qDebug() << "ori data:" << u32y;
编译运行查看打印:
跟前面的一致。
方法二:
思路是使用QDataStream的读写数据,然后借助QDataStream的setByteOrder方法,具体就不多细讲,直接看模板函数:
cpp
template <typename T>
QByteArray toData1(T value, bool isLittle) {
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
if (isLittle)
stream.setByteOrder(QDataStream::LittleEndian);
else
stream.setByteOrder(QDataStream::BigEndian);
stream << value;
return data;
}
template <typename T>
void fromData1(const QByteArray &data, bool isLittle, T &value) {
QDataStream stream(data);
if (isLittle)
stream.setByteOrder(QDataStream::LittleEndian);
else
stream.setByteOrder(QDataStream::BigEndian);
stream >> value;
}
将上面例子改为使用这两个方法:
cpp
quint32 u32 = 0x12345678;
//输出小端数据
QByteArray littleEndian = toData1(u32, true);
qDebug() << "littleEndian:" << littleEndian.toHex();
quint32 u32x = 0;
fromData1(littleEndian, true, u32x);
qDebug() << "ori data:" << u32x;
//输出大端数据
QByteArray bigEndian = toData1(u32, false);
qDebug() << "bigEndian:" << bigEndian.toHex();
quint32 u32y = 0;
fromData1(bigEndian, false, u32y);
qDebug() << "ori data:" << u32y;
编译运行查看打印:
与方法一结果一致。
方法三:
借助Qt的QtEndian:
首先qt有判断当前CPU是大端还是小端的宏:
例如:
cpp
#if Q_BYTE_ORDER == Q_BIG_ENDIAN
qDebug() << "current endian is big";
#endif
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
qDebug() << "current endian is little";
#endif
编译运行查看打印:
因为我的是x86是小端。
对应Qt也有一些大小端转换的方法:
具体使用有兴趣的可以探究一下,我没有过多研究Qt的这个。