Linux 序列化与反序列化原理及自定义网络协议实现
在 Linux 网络编程和系统开发中,序列化与反序列化几乎是绕不开的话题。无论是进程间通信,还是基于 Socket 的网络传输,数据最终都需要以字节流的形式在系统中流转。很多初学者在实际开发中往往只会'用协议',却并不清楚协议是如何设计的、数据又是如何被序列化和还原的。今天我们将围绕 Linux 环境下的序列化与反序列化,从基本原理入手,逐步分析自定义协议的设计思路与实现方式。
一、序列化与反序列化基础
在讲解概念之前,先回顾一下直接传递结构体的方式。比如通过网络完成一个简单的计算器,客户端和服务端需要约定好传输的结构体内容。例如两个运算数字和运算符(+ - * /),客户端将结构体发给服务端,服务端通过指针提取数据。
但这种直接传递结构体的方式会面临许多问题:
- 内存对齐问题:不同编译器或操作系统对结构体的填充规则可能不同,导致读取时出现乱码。
- 大小端问题:如果客户端是小端机器,服务端是大端机器,数据读取顺序可能相反。
- 适配性问题:跨语言通信(如 C++ 客户端与 Java 服务端)难以完美模拟 C 内存中的物理布局。
尽管这些问题可以通过特定手段解决,且直接传递结构体效率极高(CPU 占用低),但为了更好的扩展性和维护性,我们通常采用序列化与反序列化。
序列化是将数据由结构体转化为字符串(多变一),方便网络发送;反序列化则是将字符串转化回结构体(一变多),方便上层处理。
相较于直接传递结构体,序列化的优点在于:
- 方便网络发送:统一格式,减少传输复杂度。
- 可扩展性和可维护性:支持新旧版本共存。例如未来增加头像、性别字段,接收方只需忽略未知字段即可,无需强制同步修改结构体定义。
自定义协议的本质就是制定双方都能认识的、符合通信和业务需要的结构化数据。
二、深入理解 IO 函数与 TCP 特性
有了序列化的认识后,我们需要重新审视 read, write, recv, send 这些 IO 函数以及 TCP 的全双工特性。
使用 Socket 编程 API 时,内核中会形成发送缓冲区和接收缓冲区。IO 函数的本质是拷贝:发送数据是将用户数据拷贝到内核缓冲区,读取数据也是从缓冲区拷贝到用户空间。至于何时发送、发多少,则由 TCP 协议决定。
这里涉及几个关键问题:
- 半包问题:发送的数据缺斤少两,导致接收方收到的消息不完整。
- 粘包问题:发送方一次性发送多组小数据,接收方缓冲区较大,一次性读出多组数据,导致无法区分边界。
这两种问题都需要通过自定义协议来解决。TCP 协议像快递员,只负责把包裹送到,不关心包裹内容;自定义协议则规定包裹里具体是什么(如牛仔裤)。
关于全双工通信,是因为每一端都有独立的发送和接收缓冲区,互不影响。每个通过 socket 接口创建的文件描述符,内核都会分配一套独立的缓冲区,确保多客户端通信时互不干扰。
三、网络版计算器实战
3.1 约定方案
为了实现网络版计算器,我们需要约定客户端和服务端的传输规则。这里采用第二种方案:定义结构体来标识交互信息,发送时将结构体按规则转化为字符串,接收时再还原。
3.2 代码实现
3.2.1 准备工作
首先创建必要的文件,包括服务端 (Main.cc) 和客户端 (Client.cc)。我们需要一个封装好的 InetAddr.hpp 类来处理 IP 和端口转换。
#pragma once
{
:
{
_port = (_addr.sin_port);
ipbuffer[];
(AF_INET, &(_addr.sin_addr.s_addr), ipbuffer, (ipbuffer));
_ip = ipbuffer;
}
{
(&_addr, , (_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = (_port);
(AF_INET, _ip.(), &(_addr.sin_addr.s_addr));
}
:
() {}
( sockaddr_in &addr) : _addr(addr) { (); }
( port, std::string &ip = ) : _port(port), _ip(ip) { (); }
{ _addr = addr; (); }
{ _ip; }
{ _port; }
* () { (_addr); }
{ (_addr); }
{ _ip + + std::(_port); }
==( InetAddr &addr) { (_ip == addr._ip && _port == addr._port); }
~() {}
:
_addr;
std::string _ip;
_port;
};


