目录
三、htonl 与序列化的本质区别:简单数据 vs 结构化数据
序言
我们之前已经学过了序列化,例如Json,而且我们知道TCP面向字节流的,数据在网络中传输需要进行序列化,以及了解到了网络字节序等关键词,但是他们之间究竟有什么关系呢?似乎我们都有些许疑惑;我们来梳理一下
序列化是什么?
序列化其实就是将我们代码中的结构化的数据转化成另外一种格式,比如Json序列化,我们知道转化到Json格式的key-value格式的数据结构,其类型其实是字符串。
当然不止这一种序列化方式,我们还可以序列化成其他的格式,例如二进制、XML格式等,而不同的格式其特点也是不同的,后序在场景中需要的操作也是不同的。(网络字节序/大小端存储的问题)
好,现在我们知道了什么是序列化,序列化就是将结构化的对象,转化为另外一种特殊的格式,比如Json串,二进制,XML;
但其实这里还可以分类,其中Json和XML都属于是文本类型的数据格式;
这是AI给出的几种序列化目标对象,总结的很好。
那么接下来我们就要探讨为什么要进行序列化了?也就是序列化要解决的是什么问题?
为什么要序列化?
--->解决发送方和接收方在网络中对数据存储顺序的认识不一致的问题。
也就是大小端存储的问题。
-->什么是大小端存储?
我们先来看一下常见的几种数据类型,比如char,int,double等;其中char字符是只有一个字节,而其他的类型是多字节类型,数据的存储顺序可分为两种,一个是小端存储,一个是大端存储。
内存地址与字节分布
假设整数存储在内存地址 0x1000
开始的 4 字节空间:
地址 | 大端序存储(高位在前) | 小端序存储(低位在前) |
---|---|---|
0x1000 |
0x12 (最高位字节) |
0x78 (最低位字节) |
0x1001 |
0x34 |
0x56 |
0x1002 |
0x56 |
0x34 |
0x1003 |
0x78 (最低位字节) |
0x12 (最高位字节) |
如果网络通信中,发送方数据是大端存储,而接收方是小端存储,那么就会造成双方解析错误;
所以序列化的第一个目的就是解决不同平台的大小端存储的差异
如果我们将数据序列化成Json字符串,那么是不是就不存在字节序的问题了,因为字符串是字符组成的,每一个字符一个字节存储一个数据,没有字节顺序。
但是如果序列化成二进制格式,而二进制格式的数据仍然有大小端存储的区别,所以序列化后还需要进行转化成网络字节序。
什么?什么是网络字节序?
咬文嚼字,网络字节序就是在数据在网络中的存储顺序,其实还是大小端存储的问题,只不过网络字节序指的是规定好的字节序,TCP协议是面向字节流的,其规定的网络字节序就是大端存储,也就是如果使用TCP协议进行通信,那么就必须要将数据转化为网络字节序,才可以正确的在网络中传输。
好!回到刚才的问题,我们说到序列化成二进制格式的数据依旧存在大小端存储的问题,那么就需要将序列化的数据转化成网络字节序。这没问题。
但是我有个问题,我们知道转化成网络字节序可以使用接口htonl,但是我们之前写socket服务端的时候,端口号也需要转化为网络字节序,可是确实直接将变量作为实参传递,这不是没有进行序列化也可以吗?
NO! 这里的区别在于端口号要么是int要么是char[],没有结构化的数据,何谈序列化!
但是如果对象是一个结构体,那个是包含了几个多个字节的类型对象的,在场景中,序列化的目的不再是解决大小端存储的差异性问题了,而是保证接收方可以正确的识别那几个字节的数据是哪个对象的!
所以!这就是序列化的第二个目的--->保证接收方正确识别和解析结构化的对象;
如果传递的是结构体类型的数据时,如果不是序列化成文本类型(字符串),
拓展:
一、JSON 序列化为何天然规避字节序?
核心原理:字符流无多字节序问题
数据表示形式: JSON 将数值转为字符串(如
1234
→"1234"
),每个字符按编码(如 UTF-8)转为单字节或多字节字符编码。例:数字
1234
在 JSON 中是字符串"1234"
,存储为 UTF-8 字符流0x31 0x32 0x33 0x34
,每个字节独立表示字符,无多字节数据的顺序问题。
跨平台一致性: 接收方按字符串解析后再转为数值,无论发送方是大端还是小端平台,结果一致。
二、二进制序列化的大小端困境与网络字节序
1. 二进制序列化的 "端序隐患"
问题本质
二进制序列化直接存储数据的内存二进制表示,不同平台的字节序差异会导致解析错误。
例:小端平台序列化
int32 0x12345678
为字节流0x78 0x56 0x34 0x12
,大端平台解析为0x78563412
,与原值完全不同。
2. 网络字节序:二进制数据的 "统一语言"
定义: 网络字节序即大端序,是 TCP/IP 协议强制要求的传输格式,要求所有多字节数据在网络传输时转为大端。
与序列化的配合
发送方序列化二进制数据时,对每个多字节整数调用
htonl()
/htons()
转大端;接收方用
ntohl()
/ntohs()
转回本地字节序。
三、htonl 与序列化的本质区别:简单数据 vs 结构化数据
1. 简单数据(如端口号)的处理
为何无需序列化: 端口号(2 字节 int)是单一数据类型,通信双方提前约定 "这是一个端口号,需用大端解析",本质是 "极简序列化"(隐含结构编码:"单一 int 类型,无其他字段")。
代码示例
uint16_t port = 8080; // 本地端口(小端存储为0x80 0x20) uint16_t net_port = htons(port); // 转大端为0x20 0x80(0x2080=8320) send(sockfd, &net_port, 2, 0);
2. 结构体的序列化需求
必须序列化的原因
结构体包含多个字段,需解决:
字段身份绑定:如何区分
int age
和int score
的字节流;解析顺序:先读
age
还是score
;类型标识:如何区分
int
和float
的字节流。
示例:结构体序列化流程
struct User { char name[20]; // 字符串 int age; // 年龄 float height; // 身高 }; // 序列化(伪代码) void serialize(User* u, uint8_t* buf) { // 1. 写入name长度(解决字段长度问题) uint32_t name_len = strlen(u->name); uint32_t net_name_len = htonl(name_len); memcpy(buf, &net_name_len, 4); // 2. 写入name内容 memcpy(buf+4, u->name, name_len); // 3. 写入age(转大端) uint32_t net_age = htonl(u->age); memcpy(buf+4+name_len, &net_age, 4); // 4. 写入height(转大端,需按float格式处理) // ...(省略float字节序转换) }
四、序列化的两大核心目的总结
1. 消除平台差异(大小端、数据类型)
处理对象:多字节数值(int、float 等)的字节序统一。
技术手段:通过
htonl
转网络字节序,或文本序列化规避端序。
2. 结构化标识(字段解析规则)
处理对象:结构体、对象等复杂数据结构。
技术手段
约定字段顺序(如先
name
后age
);添加字段标签(如 Protobuf 的
Tag
);声明字段类型(如 "4 字节表示 int,8 字节表示 double")。
五、类比理解:序列化如同 "数据的快递单"
大小端处理:相当于要求快递包裹内的多件物品(多字节数据)必须按从大到小排列(大端);
结构化标识:快递单上的 "收件人姓名"" 地址 ""电话" 等字段,确保快递员(接收方)能按规则分拣包裹(解析数据)。
关键:若没有快递单(序列化规则),即使包裹内物品排列整齐(处理了大小端),也无法知道该送给谁(字段映射)。
六、实践建议:不同场景的序列化策略
场景 | 序列化策略 | 示例代码 |
---|---|---|
简单数值传输 | 直接转网络字节序(极简序列化) | net_port = htons(local_port) |
结构体跨平台通信 | 二进制序列化 + 网络字节序 | Protobuf 生成代码 +htonl 转换 |
异构系统集成 | 文本序列化(JSON/XML) | json.dumps(obj) → 字符串传输 |
通过将技术概念与生活场景类比,可以更直观地理解序列化的本质 —— 它不仅是字节序的转换,更是数据结构的跨平台 "翻译规则"。在实际开发中,根据数据复杂度和性能需求选择合适的序列化方式,是保障系统稳定性的关键。