9、网络传输中的序列化:Socket编程基础、序列化数据在网络中的发送与接收
好,咱们今天聊点实在的。
前面几章我们一直在本地打转——文件读写、内存缓冲、结构体打包。但说真的,C语言做序列化,最大的用武之地在哪?网络通信。
你想想看,两个进程,可能不在同一台机器上,甚至隔着半个地球。它们要交换数据,怎么办?把结构体指针直接传过去?别闹了,地址空间都不一样。这时候,序列化就是唯一的桥梁。
9.1 为什么网络传输必须序列化?
我刚开始做嵌入式网络编程时,犯过一个低级错误。当时要把一个温度传感器结构体从ARM板发到PC端,我直接用了send()把结构体二进制扔过去。结果PC端解析出来全是乱码。
为什么?两个原因:
- 字节序不同:ARM板是小端,PC的x86也是小端,但万一对面是个PowerPC呢?
- 内存对齐:结构体里有填充字节,不同编译器、不同平台的对齐规则不一样。
- 指针无效:结构体里如果含指针,传过去的是地址值,不是数据本身。
说白了,网络传输的底层是字节流。你必须在发送端把数据“拍平”成一串连续的字节,接收端再按约定好的格式“还原”回来。这就是序列化与反序列化的本质。
核心原则:网络传输中,永远不要直接发送结构体的内存镜像。一定要经过明确的序列化步骤。
9.2 Socket编程基础——你只需要记住这几个API
网络编程的API其实不多。我个人的习惯是,把Socket通信拆成三步:创建、连接、收发。咱们用TCP为例,因为TCP是流式协议,跟序列化天然搭配。
9.2.1 服务端流程
// 1. 创建socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定地址
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(8888);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
// 3. 监听
listen(server_fd, 5);
// 4. 接受连接
int client_fd = accept(server_fd, NULL, NULL);
// 5. 收发数据
recv(client_fd, buffer, sizeof(buffer), 0);
send(client_fd, response, sizeof(response), 0);
// 6. 关闭
close(client_fd);
close(server_fd);
9.2.2 客户端流程
// 1. 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 2. 连接服务端
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 3. 收发数据
send(sock, data, data_len, 0);
recv(sock, buffer, sizeof(buffer), 0);
// 4. 关闭
close(sock);
小提示:htons()和htonl()这两个函数,就是把主机字节序转成网络字节序(大端)。我建议你不管本地是什么字节序,发数据前一律转成大端。这叫“网络字节序约定”,大家都遵守,就不会乱。
9.3 序列化数据在网络中的发送
好,现在Socket通了,怎么把序列化后的数据发出去?
这里有个关键点:TCP是流协议,没有消息边界。你发两次send(),接收方可能一次recv()就全收了,也可能分三次收。所以,我们必须自己定义消息格式。
9.3.1 常用的消息格式:TLV
TLV是Type-Length-Value的缩写。我个人非常喜欢这种格式,简单、可靠、容易解析。
| 字段 | 长度 | 说明 |
|---|---|---|
| Type | 2字节 | 消息类型,比如0x01表示温度数据 |
| Length | 2字节 | Value部分的长度(字节数) |
| Value | 可变 | 序列化后的实际数据 |
发送端代码示例:
// 假设我们要发送一个序列化后的传感器数据
uint8_t serialized_data[256];
int data_len = serialize_sensor_data(sensor, serialized_data);
// 构造TLV包头
uint16_t type = htons(0x01);
uint16_t len = htons(data_len);
// 先发包头,再发数据
send(sock, &type, 2, 0);
send(sock, &len, 2, 0);
send(sock, serialized_data, data_len, 0);
注意:千万不要把TLV包头和Value放在一个结构体里直接send()。因为结构体可能有填充字节。正确的做法是:分别发送,或者手动打包到一个连续的缓冲区里。
9.4 序列化数据的接收与解析
接收端就稍微麻烦一点了。因为TCP是流,你不知道对方一次发了多少。我曾经在项目里踩过这个坑——假设一次recv()就能收完整个消息,结果丢数据了。
正确的做法是:先收固定长度的包头,解析出长度,再收变长的Value。
// 接收TLV包头
uint16_t type, len;
recv_all(sock, &type, 2);
recv_all(sock, &len, 2);
type = ntohs(type);
len = ntohs(len);
// 根据长度接收Value
uint8_t *value = malloc(len);
recv_all(sock, value, len);
// 反序列化
if (type == 0x01) {
SensorData data;
deserialize_sensor_data(value, len, &data);
printf("温度: %.2f\n", data.temperature);
}
free(value);
这里我写了一个recv_all()辅助函数,确保收够指定字节数:
int recv_all(int sock, void *buf, int len) {
int total = 0;
while (total < len) {
int n = recv(sock, (char*)buf + total, len - total, 0);
if (n <= 0) return n; // 连接断开或出错
total += n;
}
return total;
}
经验之谈:recv()返回0表示对方关闭连接,返回-1表示出错。这两个情况一定要处理,否则程序会卡死。我见过太多新手写网络程序,recv()返回值都不检查,一跑就崩。
9.5 完整示例:发送序列化结构体
咱们把前面几章的知识串起来。假设有一个Person结构体,我们要通过网络发送:
// 定义结构体
typedef struct {
uint32_t id;
char name[32];
float score;
} Person;
// 序列化函数
int serialize_person(const Person *p, uint8_t *out) {
uint32_t id = htonl(p->id);
float score = p->score; // 浮点数序列化需注意,这里简化处理
memcpy(out, &id, 4);
memcpy(out+4, p->name, 32);
memcpy(out+36, &score, 4);
return 40; // 固定长度
}
// 发送
Person p = {1001, "Alice", 95.5f};
uint8_t buffer[40];
serialize_person(&p, buffer);
uint16_t type = htons(0x02);
uint16_t len = htons(40);
send(sock, &type, 2, 0);
send(sock, &len, 2, 0);
send(sock, buffer, 40, 0);
接收端反序列化:
Person p;
uint32_t id;
recv_all(sock, &id, 4);
p.id = ntohl(id);
recv_all(sock, p.name, 32);
recv_all(sock, &p.score, 4);
// 浮点数不需要字节序转换?不一定,如果两端浮点格式不同就麻烦了
// 实际项目中建议用固定点或字符串表示浮点数
避坑指南:浮点数的字节序和表示格式在不同平台可能不同。我曾经在ARM和x86之间传float,结果数值对不上。后来我改用整数表示(比如放大100倍),或者直接用字符串传。安全第一。
9.6 本章知识体系
下面这张图,是我梳理的网络传输中序列化的核心流程。你看一遍,应该能对整个链路有个全局认识。
9.7 总结
网络传输中的序列化,说白了就是两件事:
- 发送前:把结构体拍平,加上TLV包头,转成网络字节序。
- 接收后:先收包头,再收数据,按约定格式还原。
嗯,这里要注意:不要偷懒。我见过有人图省事,直接把结构体指针强转成char*就发出去。短期跑没问题,一换平台就崩。序列化这个步骤,省不得。
下一章我们会聊更高级的话题——如何处理变长数据、如何设计可扩展的序列化协议。但今天的内容,已经足够你写出一个能跑通网络通信的序列化模块了。
一句话记住本章:网络传输 = 序列化 + TLV + 字节序转换 + recv_all()。