跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Linux 序列化与反序列化原理及自定义协议实战

本文深入探讨了 Linux 网络编程中的序列化与反序列化技术。首先分析了直接传递结构体面临的内存对齐、大小端及跨语言兼容性问题,引出序列化方案的优势。接着重新解读了 read/write 等 IO 函数的底层机制,解释了 TCP 半包与粘包问题的成因及解决方案。最后通过实战案例,演示了如何使用 C++ 封装 Socket 类、利用 Jsoncpp 库实现自定义协议,并构建了一个支持并发处理的网络版计算器,涵盖了从协议设计到代码落地的完整流程。

Elasticer发布于 2026/3/20更新于 2026/10/24104 浏览
Linux 序列化与反序列化原理及自定义协议实战

Linux 序列化与反序列化原理及自定义协议实现

在 Linux 网络编程和系统开发中,序列化与反序列化几乎是绕不开的话题。无论是进程间通信,还是基于 Socket 的网络传输,数据最终都需要以字节流的形式在系统中流转。很多初学者在实际开发中往往只会'用协议',却并不清楚协议是如何设计的、数据又是如何被序列化和还原的。今天我们将围绕 Linux 环境下的序列化与反序列化,从基本原理入手,逐步分析自定义协议的设计思路与实现方式。

一、序列化与反序列化

在深入概念之前,先回顾一下直接传递结构体的局限性。比如通过网络完成一个简单的计算器,客户端和服务端需要约定好传输的结构体内容。虽然这种方式效率极高,CPU 占用低,但存在明显隐患:

  1. 内存对齐问题:不同编译器或操作系统对结构体的填充规则可能不同,导致接收方读取乱码。
  2. 大小端问题:如果客户端是小端机器,服务端是大端机器,数据读取顺序会相反。
  3. 适配性问题:跨语言通信(如 C++ 客户端与 Java 服务端)时,很难完美模拟对方内存中的物理布局。

为了解决这些问题,我们引入序列化与反序列化。

文章配图

将数据由结构体转化为字符串(多变一),方便网络发送的过程叫序列化;反之,将字符串按规则还原为结构体(一变多),方便上层处理的过程叫反序列化。

相较于直接传递结构体,序列化的优势在于:

  • 方便网络发送:统一格式,减少传输开销。
  • 可扩展性与维护性:支持新旧版本共存。例如未来增加字段(如头像、性别),接收方只需忽略未知字段即可,无需强制同步修改结构体定义。

自定义协议的本质,就是制定双方都能识别的、符合业务需求的数据结构(struct 或 class)。

二、重新理解 IO 函数与 TCP 全双工

有了基础认知后,我们需要重新审视 read, write, recv, send 这些 IO 函数。

在使用 Socket API 时,内核会为每个连接形成发送缓冲区和接收缓冲区。IO 函数的本质是拷贝:发送是将用户数据拷贝到内核缓冲区,读取是从内核缓冲区拷贝到用户空间。至于何时发送、发多少,则由 TCP 协议自行决定,这也是它被称为'传输控制协议'的原因。

这里常遇到两个经典问题:半包和粘包。

  • 半包问题:发送的数据量超过接收方缓冲区容量,导致一次只能读取部分数据。
  • 粘包问题:发送方多次发送小数据,接收方一次性读出了多组数据,无法区分边界。

解决这两个问题的关键在于自定义协议。TCP 协议像快递员,只负责把包裹安全送达,不关心包裹里是什么;而自定义协议规定了包裹里的具体内容格式,确保收发双方能正确解析。

关于 TCP 全双工特性,核心原因在于每端都有独立的发送和接收缓冲区,互不影响。每个通过 socket 创建的文件描述符,内核都会分配一套独立的缓冲区,确保多路复用时的隔离性。

三、网络版计算器实战

3.1 约定方案

为了实现一个可靠的网络计算器,我们需要约定通信规则。这里采用第二种方案:定义结构体标识交互信息,发送前序列化为字符串,接收后反序列化回结构体。

3.2 代码实现

为了简化 Socket 操作,我们封装了一套 Socket 接口类,采用模板方法模式设计。

3.2.1 准备工作

首先创建必要的头文件,其中 InetAddr.hpp 用于管理 IP 和端口信息,处理主机序与网络序的转换。

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define Conv(addr) ((struct sockaddr*)&addr)

class InetAddr {
private:
    void Net2Host() {
        _port = ntohs(_addr.sin_port);
        char ipbuffer[64];
        inet_ntop(AF_INET, &(_addr.sin_addr.s_addr), ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }
    void Host2Net() {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &(_addr.sin_addr.s_addr));
    }
public:
    InetAddr() {}
    InetAddr(const struct sockaddr_in &addr) : _addr(addr) { Net2Host(); }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0") : _port(port), _ip(ip) { Host2Net(); }
    void Init(const struct sockaddr_in &addr) { _addr = addr; Net2Host(); }
    std::string Ip() { return _ip; }
    uint16_t Port() { return _port; }
    struct sockaddr* Addr() { return Conv(_addr); }
    socklen_t Length() { return sizeof(_addr); }
    std::string ToString() { return _ip + "-" + std::to_string(_port); }
    bool operator==(const InetAddr &addr) { return (_ip == addr._ip && _port == addr._port); }
    ~InetAddr() { }
private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

这个类封装了套接字信息,便于后续管理地址转换。

3.2.2 Socket 封装

我们定义一个基类 Socket,包含纯虚函数 create, bind, listen,子类 TcpSocket 负责具体实现。基类提供 Build 函数整合流程,利用多态调用。

对于错误处理,我们定义了枚举类型,让代码更直观。sockfd 作为成员变量,backlog 设为默认值。IP 默认为 0.0.0.0。

Accept 函数不仅返回新连接的 fd,还通过 peer 获取客户端 IP 和端口信息,存入 addr 对象中。

3.2.3 TcpServer.hpp 的实现

服务端使用多进程模型处理并发请求。父进程负责监听,子进程处理具体连接。

为了避免父进程阻塞等待子进程结束,我们利用信号机制处理僵尸进程。子进程关闭监听套接字,父进程关闭已连接套接字,确保资源释放。

关键逻辑在于 Recv 和 Send 函数。由于 TCP 是流式协议,接收数据时必须处理半包和粘包问题。我们通过累加字符串的方式暂存数据,直到判断报文完整后再交给上层处理。

3.2.4 Protocol 协议的实现

自定义协议的核心是 Request 和 Response 类。Request 包含操作数和运算符,Response 包含结果和状态码(用于标识除零等错误)。

我们使用 Jsoncpp 库进行序列化和反序列化。

  • 序列化:使用 Json::Value 构建对象,通过 FastWriter 转为紧凑字符串。
  • 反序列化:使用 Json::Reader 将字符串解析回 Value 对象。

为了解决粘包和半包问题,我们在序列化后的 JSON 字符串前添加了一个报头,记录有效载荷长度。这样接收方可以先读取固定长度的报头,确定剩余数据的长度,从而精准截取完整报文。

此外,Unpack 函数会将已处理的完整报文从输入缓冲区中移除,避免重复处理。

3.2.5 客户端与服务端整合

服务端通过回调函数处理解析后的请求,将任务委托给 Calculator 类执行计算。Parser 类负责解析协议,Protocol 类负责打包响应。

客户端代码相对简单,主要负责发起连接、构造请求并循环接收响应。

运行测试后,可以看到计算器功能正常,包括除零异常也能通过 _code 字段正确反馈。

通过以上步骤,我们完成了一个具备基本容错能力的网络计算器,同时也掌握了 Linux 下序列化、反序列化及自定义协议设计的核心要点。

目录

  1. Linux 序列化与反序列化原理及自定义协议实现
  2. 一、序列化与反序列化
  3. 二、重新理解 IO 函数与 TCP 全双工
  4. 三、网络版计算器实战
  5. 3.1 约定方案
  6. 3.2 代码实现
  7. 3.2.1 准备工作
  8. 3.2.2 Socket 封装
  9. 3.2.3 TcpServer.hpp 的实现
  10. 3.2.4 Protocol 协议的实现
  11. 3.2.5 客户端与服务端整合

更多推荐文章

查看全部
  • C++ vector 容器:底层原理、扩容机制与实战用法详解
  • LeetCode 热题 100 快速通关指南(附模板)
  • 基于 SpringBoot 的医院住院管理系统设计与实现
  • C++ STL 常用容器 vector、set 与 string 用法详解
  • Python IDLE 汉化方法
  • 30dayMakeCppServer:构建高性能C++网络服务器实践
  • 利用 AI 快速开发 Microsoft Visual C++ 应用
  • Stable Diffusion 图生视频实战:从一张图到一段动画
  • C++ 容器适配器:从接口到实现,理解 stack、queue 和 priority_queue
  • Diffusion Transformer (DiT):U-Net 换 ViT,应用于视频生成与机器人动作预测
  • 中国人工智能大模型技术白皮书核心内容总结
  • 自然语言处理在金融领域的应用与实战
  • AI Agent 架构:基础组成模块深度解析
  • 小智 AI 完整的 MCP 交互流程
  • WebDriverAgent 技术深度解析
  • Qwen3-VL 结合 Dify 实现 OCR 增强与空间感知实战
  • 基于指数预定义时间控制的固定翼无人机轨迹跟踪控制(Matlab 仿真)
  • 5 款设计师必备的 Stable Diffusion 模型,涵盖 3D、电商与 B 端场景
  • DeepSeek-R1 大模型基于 MS-Swift 框架部署与微调实践
  • 金仓 KingbaseES 融合架构实践:从多库并存到一库多能

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online