序列化与非序列化

【Linux网络编程】04 · 应用层自定义协议与序列化(Jsoncpp + 长度前缀彻底解决粘包)

摘要:本文围绕"网络版计算器"这一经典案例,把应用层协议是怎么被设计出来的 这件事从头掰开讲:为什么 TCP 一定要自己定协议、为什么"报文必须自带边界"、Encode/Decode 这对函数到底在干什么、Jsoncpp 如何完成结构化数据与字符串的互转、send/recv 的真实语义(它们其实是拷贝函数而不是收发函数)、以及 TCP 为什么天然支持全双工。最后横向对比 JSON / Protobuf / MessagePack 等序列化方案,并给出面试高频追问。

关键词:应用层协议、序列化、反序列化、Jsoncpp、粘包、拆包、Encode/Decode、全双工、模板方法模式、工厂模式


目录


一、引言:从"能通"到"能用"

前面几篇我们已经能用 socket 把字符串从一台主机发到另一台主机了,但那只是回显(echo):客户端发什么,服务器原样吐回来。

回显式的代码有一个致命特点:它不需要理解数据 。服务器收到 "hello" 也不知道这是什么意思,原样发回去就完事了。而真实的业务场景不是这样的:

  • 客户端要告诉服务器"帮我算 10+20";
  • 服务器要告诉客户端"结果是 30,或者你的除数是 0,出错了"。

这就引出了本篇的三件大事:

  1. 约定报文格式(应用层协议)
  2. 把结构化数据变成字符串 / 再把字符串还原成结构化数据(序列化和反序列化)
  3. 在 TCP 这条没有边界的字节流里,把一个个报文切出来(解决粘包)

二、应用层:我们写的程序就在这一层

在 TCP/IP 五层模型里,应用层是最上面的一层:

复制代码
┌──────────────────────────────────────┐
│  应用层     ← 我们写的网络程序就在这里     │  HTTP / FTP / SMTP / 我们自己定的协议
├──────────────────────────────────────┤
│  传输层                                │  TCP / UDP  (由内核实现)
├──────────────────────────────────────┤
│  网络层                                │  IP        (由内核实现)
├──────────────────────────────────────┤  socket API 就夹在这儿
│  数据链路层                            │  以太网 / 驱动
├──────────────────────────────────────┤
│  物理层                                │  光缆 / 网线 / 无线电磁波
└──────────────────────────────────────┘

⭐ 下四层由操作系统内核帮我们做完了 ,我们调用 socket / bind / listen / accept / connect / send / recv 这些 API,本质上是在借用内核的传输层能力。我们写的那些解决实际问题的程序(网盘、聊天室、游戏服务器、RPC 框架)通通跑在应用层。

一个重要推论:TCP/UDP 只负责把"字节"搬过去,它完全不关心你字节里装的什么。 那么"这段字节代表什么意思"就必须由我们自己在应用层约定 ------ 这就是应用层协议存在的唯一理由。


三、再谈"协议":协议就是双方约定好的结构化数据

3.1 痛点:socket API 只会收发字符串

socket 的读写接口(read/write/recv/send)传的都是 void*/char 缓冲区,也就是一串没有结构的字节 。但实际业务要传的往往是结构化数据:一个订单、一条聊天消息、一个计算任务......

📌 协议(protocol)本质上就是一种"约定":约定好这段字节流里,第几个字节到第几个字节是什么含义、用什么符号分隔、值用什么格式表示。

一句话:协议 = 双方约定好的结构化数据。

3.2 以"网络版计算器"为例

需求:客户端把两个加数发给服务器,服务器算完把结果返回。

约定方案一:纯字符串约定
  • 客户端发送一个形如 "1+2" 的字符串;
  • 字符串中有两个操作数,都是整型;
  • 两个数字之间有一个字符是运算符 ,运算符只能是 +;
  • 数字和运算符之间没有空格;
  • ......
cpp 复制代码
// 服务端朴素写法
recv(sockfd, buf, sizeof(buf), 0);   // buf = "1+2"
// 自己去找 '+' 的位置,拆成左操作数和右操作数

✅ 优点:简单、直观、肉眼可读(用 telnet/nc 就能调试)。

❌ 缺点:扩展性极差 。哪天要加个乘法、加个浮点数、加个"错误码"字段,就得重写一遍字符串解析逻辑,而且很容易写出 atoi 满天飞、sscanf 到处硬编码的垃圾代码。

约定方案二:结构体 + 序列化(本篇采用)
  • 定义一个结构体 来表示需要交互的信息:

    cpp 复制代码
    class Request  { int _data_x; int _data_y; char _oper; };  // 请求
    class Response { int _result; int _code; };                // 应答
  • 发送时 把这个结构体按照某种规则转换成字符串;

  • 接收时 再按照相同规则把字符串还原成结构体;

  • 这个"结构 ↔ 字符串"的转换过程,就叫 序列化(Serialize) 和 反序列化(Deserialize)。

3.3 到底什么是"应用层协议"

⭐ 无论采用方案一、方案二还是什么别的方案,只要保证:一端发送时构造的数据,在另一端能够被正确地解析,就是 OK 的。这种约定,就是应用层协议。

也就是说,协议没有标准答案。你完全可以这样约定:

复制代码
"protocol_code\r\nlen\r\nx op y\r\n"

也可以那样约定。只要收发两端对得上就行。为了让大家真正理解协议的细节,我们才"手搓"一遍协议定制的全过程。

环节 做法 目的
协议格式 采用方案二(结构体) 体现协议定制的细节与字段的自描述能力
序列化 直接用现成轮子 ------ Jsoncpp 库 避免把时间浪费在字符串切割上,同时也贴近工程实践
传输层 对 socket 做字节流读取处理 直面 TCP 的粘包问题

四、序列化与反序列化

复制代码
        发送方                                          接收方
   ┌───────────────┐                              ┌───────────────┐
   │  结构化数据     │                              │  字符串/字节流  │
   │  Request 对象  │                              │  "...json..."     │
   └───────┬───────┘                              └───────┬───────┘
           │  Serialize() 序列化                          │  Deserialize() 反序列化
           ▼                                              ▲
   ┌───────────────┐       网络/磁盘传输          ┌────────┴──────┐
   │  字符串/字节流  │  ───────────────────────▶  │  结构化数据     │
   └───────────────┘                              └──────────────┘
概念 定义 场景
序列化 (Serialize) 把内存中的结构化对象 转换成可传输的字符串/字节序列 发送前、写文件前、存 Redis 前
反序列化 (Deserialize) 把字符串/字节序列还原成内存中的结构化对象 收到数据后、读文件后、从 Redis 读出后

💡 为什么不能直接把结构体内存 memcpy 出去? 这是初学者的经典疑问,答案见 第十二节(涉及内存对齐、大小端、版本兼容三大坑)。

4.1 关键:"序列化"在本项目里其实做了两层

初学时最容易混淆的一点:Serialize() 和 Encode() 好像都在做"变成字符串",它俩到底什么关系?

⭐ 它们的分工不同、层次不同:

层次 负责的函数 处理的对象 产出
第一层(向内:业务的表示) Serialize() / Deserialize() 业务对象 ↔ 消息体字符串 {"datax":10,"datay":20,"oper":43}
第二层(向外:网络的表示) Encode() / Decode() 消息体字符串 ↔ 完整网络报文 36\r\n{"datax":10,...}\r\n

两次处理都属于"序列化",只是粒度不同。 业务层构建出字符串之后,还需要经过第二次加工,才能变成真正能上线的报文。

为什么要分两层?因为它们解决的问题完全不同:

  • Request / Response 这两个结构只描述"消息里有什么字段" ,它并不规定这些字段怎么变成字节流发到网络上;
  • 而 TCP 是字节流,Encode 负责的事情是给已经有内容的消息体套上一层"能在线上被识别出来的壳"。

💡 一句话串起来 :Serialize 决定"消息长什么样",Encode 决定"这条消息在线上从哪里开始、到哪里结束"。两者叠起来,才是完整的应用层协议。

4.2 为什么收发双方要各持有一份 Request / Response

⭐ 序列化和反序列化要求双方约定好完全相同的字段名。 所以服务端与客户端的代码里都有一份 Request 和 Response (即 Protocol.hpp 是两边共有的头文件)。

它们的使用方向恰好相反,务必记牢这张表:

角色 对 Request 对 Response
客户端 Serialize() 发送 Deserialize() 接收
服务端 Deserialize() 接收 Serialize() 发送
复制代码
客户端 ── Serialize(Request) ──▶ 网络 ──▶ Deserialize(Request) ── 服务端
       ◀─ Deserialize(Response) ── 网络 ◀── Serialize(Response) ──

如果两边字段名不一致会怎样? 举个真实的坑:一边写 root["datax"],另一边读 root["x"]。JSON 解析不会报错(因为 root["x"] 在 Jsoncpp 里会自动创建一个空的新元素 ,见附录 A.5),但取出来的是 0 ------ 发出去也白搭,数据永远是默认值。这就是注释里说的"必须约定同一套字段,否则发了也白搭"。


五、重新理解 read/write/recv/send:它们其实是拷贝函数

这是本篇最重要的认知升级之一。

5.1 数据到底怎么流动的

复制代码
【主机 A】客户端进程                                【主机 B】服务器进程

  用户空间                                           用户空间
 ┌───────────┐                                     ┌───────────┐
 │ 应用层缓冲  │                                     │ 应用层缓冲  │
 │ "1+2"     │                                     │           │
 └─────┬─────┘                                     └─────▲─────┘
       │ send()/write()                       recv()/read()│
       │ ★把用户数据"拷贝"进内核★        ★把内核数据"拷贝"到用户★│
 ──────┼──────────────── 用户态/内核态分界 ───────────────┼──────
       ▼                                                 │
 ┌───────────┐                                     ┌─────┴─────┐
 │ 发送缓冲区  │                                     │ 接收缓冲区  │
 └─────┬─────┘                                     └─────▲─────┘
       │          (TCP 决定何时发、发多少、出错怎么办)      │
       └──────────────────▶  网络  ───────────────────────┘

⭐ 关键结论:send/recv 本质上是"拷贝函数",而不是"收发函数"!

  • send() 只负责把用户空间的字节拷贝 到内核的发送缓冲区,然后就返回了;
  • recv() 只负责把内核接收缓冲区 里的字节拷贝到用户空间,缓冲区空了就阻塞等你;
  • 数据什么时候真正发出去、一次发多少、丢包了怎么重传、拥塞了怎么降速 ------ 全是 TCP 协议自己说了算。

正因为 TCP 全权接管了"何时发、发多少、出错了怎么办"这套控制逻辑,所以 TCP 才叫 传输控制协议(Transmission Control Protocol)。

5.2 这个认知带来的直接推论

  1. send() 返回成功 ≠ 对方收到了数据。 它只代表数据被拷进了你的发送缓冲区。对方进程崩溃与否、网络是否通畅,此刻都不知道。
  2. 你可能要循环调用 recv() 才能拿全一个业务包。 因为对方那一次业务逻辑可能很大,而 TCP 分片由不得应用层控制。
  3. 这就是为什么必须自己处理"报文边界"。 TCP 只保证字节流的顺序,不保留"消息"的边界。

六、为什么 TCP 支持全双工

6.1 全双工、半双工、单工

术语 含义 生活类比
单工 只能单向传输 广播、电视
半双工 双方都能收能发,但同一时刻只能一方发 对讲机(要按着说话键)
全双工 双方可以同时收发 电话、TCP 连接

6.2 TCP 全双工的物理来源

⭐ 在任意一台主机上,一个 TCP 连接同时拥有发送缓冲区和接收缓冲区。两者互相独立、互不干扰。所以在内核层面,"发消息"和"收消息"是两条完全独立的通道,可以同时进行 ------ 这就是全双工。

这一点要和我们熟悉的**管道(pipe)**做个对比:

对象 fd 数量 缓冲区 是否全双工
pipe(匿名管道) 需要 2 个 fd(fd[0] 读、fd[1] 写) 一个单向缓冲区 ❌ 单向,双向通信要开两根管道
TCP socket 1 个 sockfd 独立的收发两个缓冲区 ✅ 全双工

💡 这也就是为什么一个 TCP sockfd 既能用来 read 又能用来 write 的原因:同一个 fd 背后挂着两个方向相反的缓冲区,你读就读接收缓冲区,你写就写发送缓冲区,互不打架。

复制代码
           同一个 sockfd
      ┌──────────┴──────────┐
      ▼                     ▼
 ┌─────────┐           ┌─────────┐
 │ 接收缓冲区│           │ 发送缓冲区│
 │  recv ← │           │ → send  │
 └─────────┘           └─────────┘
    两者独立 → 可同时工作 → 全双工

七、Socket 封装:模板方法模式小试牛刀

项目文件结构:

复制代码
Calculate.hpp    Makefile        Socket.hpp       TcpServer.hpp
Daemon.hpp       Protocol.hpp    TcpClientMain.cc TcpServerMain.cc

为了把注意力集中在协议本身,服务器采用简单的自定义线程处理(不为每个连接 fork 多进程) ,客户端也不用读用户输入,直接自动构造请求和服务器来回通信,省掉一堆没营养的 IO 代码。

7.1 抽象基类 Socket:定义"骨架"

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define Convert(addrptr) ((struct sockaddr *)addrptr)

namespace Net_Work
{
    const static int defaultsockfd = -1;
    const int backlog = 5;

    enum
    {
        SocketError = 1,
        BindError,
        ListenError,
    };

    // 封装一个基类,Socket接口类
    // 设计模式:模版方法类
    class Socket
    {
    public:
        virtual ~Socket() {}
        virtual void CreateSocketOrDie() = 0;
        virtual void BindSocketOrDie(uint16_t port) = 0;
        virtual void ListenSocketOrDie(int backlog) = 0;
        virtual Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) = 0;
        virtual bool ConnectServer(std::string &serverip, uint16_t serverport) = 0;
        virtual int GetSockFd() = 0;
        virtual void SetSockFd(int sockfd) = 0;
        virtual void CloseSocket() = 0;
        virtual bool Recv(std::string *buffer, int size) = 0;
        virtual void Send(std::string &send_str) = 0;
        // TODO

    public:
        void BuildListenSocketMethod(uint16_t port, int backlog)
        {
            CreateSocketOrDie();
            BindSocketOrDie(port);
            ListenSocketOrDie(backlog);
        }
        bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport)
        {
            CreateSocketOrDie();
            return ConnectServer(serverip, serverport);
        }
        void BuildNormalSocketMethod(int sockfd)
        {
            SetSockFd(sockfd);
        }
    };
}

7.2 什么是模板方法模式(Template Method)

⭐ 模板方法模式 :在一个抽象父类中定义好算法的执行骨架(步骤顺序) ,而把其中某些具体步骤延迟到子类实现。

在本例中:

  • "模板"(骨架) = BuildListenSocketMethod():先 create → 再 bind → 再 listen ,这个顺序是所有监听类套接字都必须遵守的、不可变的流程,所以写在父类里;
  • "可变步骤" = CreateSocketOrDie() 等纯虚函数:TCP 要 SOCK_STREAM,将来如果加 UDP 子类就要 SOCK_DGRAM,各不相同,所以留给子类 override。

带来的好处:

  1. 上层代码只需要调 BuildListenSocketMethod(port, backlog) 一行,不用再关心"监听套接字怎么搭起来"这种样板细节;
  2. 将来新增 UdpSocket 子类,只需继承并实现纯虚函数,上层完全不用改(对修改封闭、对扩展开放 = 开闭原则)。

💡 注意函数后缀的 OrDie 命名习惯:表示"这一步失败就直接让进程退出(exit())",不做错误处理。这是许多开源项目(如陈硕 muduo、Google 内部代码规范)常见的约定,用于把"不可能失败的场景"的代码写短。

7.3 具体实现 TcpSocket

cpp 复制代码
    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd) {}
        ~TcpSocket() {}

        void CreateSocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
                exit(SocketError);
        }

        void BindSocketOrDie(uint16_t port) override
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(port);
            int n = ::bind(_sockfd, Convert(&local), sizeof(local));
            if (n < 0)
                exit(BindError);
        }

        void ListenSocketOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
                exit(ListenError);
        }

        Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int newsockfd = ::accept(_sockfd, Convert(&peer), &len);
            if (newsockfd < 0)
                return nullptr;
            *peerport = ntohs(peer.sin_port);
            *peerip = inet_ntoa(peer.sin_addr);
            Socket *s = new TcpSocket(newsockfd);
            return s;
        }

        bool ConnectServer(std::string &serverip, uint16_t serverport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(serverip.c_str());
            server.sin_port = htons(serverport);
            int n = ::connect(_sockfd, Convert(&server), sizeof(server));
            if (n == 0)
                return true;
            else
                return false;
        }

        int GetSockFd() override { return _sockfd; }
        void SetSockFd(int sockfd) override { _sockfd = sockfd; }

        void CloseSocket() override
        {
            if (_sockfd > defaultsockfd)
                ::close(_sockfd);
        }

        bool Recv(std::string *buffer, int size) override
        {
            char inbuffer[size];
            ssize_t n = recv(_sockfd, inbuffer, size - 1, 0);
            if (n > 0)
            {
                inbuffer[n] = 0;
                *buffer += inbuffer;  // 故意拼接的
                return true;
            }
            else if (n == 0)
                return false;
            else
                return false;
        }

        void Send(std::string &send_str) override
        {
            // 多路转接我们统一再说
            send(_sockfd, send_str.c_str(), send_str.size(), 0);
        }

    private:
        int _sockfd;
    };
} // namespace Net_Work

7.4 这里值得留意的两个细节

① AcceptConnection 为什么要返回一个新的 Socket*?

因为 accept() 返回的是一个全新的 fd (用于和这个特定客户端通信),而监听 socket 还要继续 accept 别人。所以这里用新 fd new 出一个 TcpSocket 对象 返回给上层,让上层拿它去和客户端收发数据。注意它返回的是父类指针 Socket*,上层面向抽象编程,不依赖具体实现。

② Recv 里为什么要 *buffer += inbuffer(拼接)?

这是我们处理 TCP 字节流的第一处伏笔。recv 每次读到的不一定是一个完整报文 ,可能只有半个。所以这里先把读到的内容追加到调用者的缓冲区尾部 ,由上层再去循环解析,直到凑出完整报文为止。这就是所谓的用户态缓冲区。

⚠️ 代码小坑提醒(面试加分项) :

char inbuffer[size]; 是 VLA(变长数组) ,属于 C99 特性、并非标准 C++ ,只是 GCC 扩展。工程写法建议改成 std::vector<char> inbuffer(size); 或 std::string。

另外 *buffer += inbuffer; 依赖 inbuffer[n] = 0 补的 '\0',如果收到的报文中含有 '\0' 二进制数据就会被截断 ------ 这也是为什么本协议最终选择了纯文本的 JSON,而不是二进制结构体。


八、定制协议:Request / Response 的报文格式

8.1 基本结构

⭐ 定制结构化字段本身,就是在定协议。

cpp 复制代码
class Request
{
private:
    // _data_x _oper _data_y
    // 报文的自描述字段
    // "len\r\nx op y\r\n" : \r\n不属于报文的一部分,约定
    int _data_x;   // 第一个参数
    int _data_y;   // 第二个参数
    char _oper;    // + - * / %
};

class Response
{
private:
    // "len\r\n_result _code\r\n"
    int _result;   // 运算结果
    int _code;     // 运算状态
};

为什么要设计一个 _code(状态码)字段?

因为网络请求是有可能失败的,光有一个返回值不足以表达全部语义:

_code 取值建议 含义 _result 的含义
0 成功 真正的计算结果
1 除数为 0 无意义(置 0)
2 模 0(非法取模) 无意义
3 运算符非法 无意义
4 结果溢出 无意义

💡 这其实就是 HTTP 状态码的思想:用"状态码 + 数据载荷"的组合来描述一次请求的结果,而不是只返回一个值。

8.2 期望的完整报文格式

复制代码
┌──────────┬────────┬───────────────────────────┬────────┐
│  len     │  \r\n  │  {"datax":1,...}          │  \r\n  │
│ (报文字节数)│ (分隔符)│  (报文正文 / payload)      │ (分隔符) │
└──────────┴────────┴───────────────────────────┴────────┘
   ▲ 自描述头部           ▲ 自描述正文                 ▲ 结束边界

一个真实的例子(假设正文是 x op y 形式):

复制代码
9\r\n
10 + 20\r\n

📌 \r\n 不属于报文正文的一部分 ,它只是我们约定的分隔符/边界。这是 HTTP、FTP、SMTP、Redis(RESP) 等众多文本协议的通用做法。

注:课件里还有一个 protocol_code 的可选扩展版协议头 ------ "protocol_code\r\nlen\r\nx op y\r\n"。加了一个"协议编号"字段后,同一个端口上就可以同时跑多套协议 (比如 1 表示计算器、2 表示聊天消息、3 表示文件传输),收到报文后先读 protocol_code,再分派给对应的处理函数。这正是 RPC 框架里的常见设计。


九、Jsoncpp 落地:序列化到底怎么写

9.1 安装

⚠️ Jsoncpp 是第三方库,不是系统自带的 ,必须手动安装到本地才能 #include <jsoncpp/json/json.h>。它就是 C++ 版的 JSON 序列化/反序列化库。

bash 复制代码
# Ubuntu / Debian
sudo apt-get install libjsoncpp-dev

# CentOS / RockyLinux
sudo yum install jsoncpp-devel

编译时注意链接库:g++ -o server *.cc -ljsoncpp -lpthread

装完后两个常见检查点:

bash 复制代码
ls /usr/include/jsoncpp/json/        # 头文件是否到位
ls /usr/lib/x86_64-linux-gnu/libjsoncpp*   # 库文件是否到位

9.2 Protocol.hpp 全貌

📌 下面这份代码附上了学习过程中总结的行内注释,基本把你会在阅读/二开时产生的疑问都答了。原版注释里有一句特别值得单独拎出来:

"每段 JSON 字符串是自己构建的,所以一定有边界;但一旦把这些字符串都传入传输层,就无法分辨它们了。"

这正是 Encode 存在的全部理由 ------ 序列化(此处指第二层的 Encode)的本质,就是给每一段 JSON 消息封装出明确的边界,每次找到边界就能实现分包。

cpp 复制代码
#pragma once

#include <iostream>
#include <memory>
#include <jsoncpp/json/json.h>

namespace Protocol
{
    // 问题:
    // 1. 结构化数据的序列和反序列化
    // 2. 还要解决用户区分报文边界 ------ 数据包粘报问题

    const std::string ProtSep = " ";
    const std::string LineBreakSep = "\r\n";

    // ==================== 一、加报头 / 去报头 ====================

    // "len\r\nx op y\r\n"  ← 期望的最终报文格式
    // Encode 的过程就是"拼接":
    //     最终报文 = 协议头 (Header) + 业务数据 (JSON字符串)
    // 这样解决 TCP 粘包问题
    // 注意:Request/Response 结构只表示"消息里有什么字段",
    //       但不规定"这些字段怎么变成字节流发到网络上"------那是这里的事
    std::string Encode(const std::string &message)
    {
        std::string len = std::to_string(message.size());
        std::string package = len + LineBreakSep + message + LineBreakSep;
        return package;
    }

    // 解包:message 是输出型参数(函数内部写结果,调用方取值)
    // 最终应用层报文的格式完全由自己定义;当然也可以采用互联网上其他标准协议,
    // 只要收发双方一致就行
    bool Decode(std::string &package, std::string *message)
    {
        auto pos = package.find(LineBreakSep);
        if (pos == std::string::npos)
            return false;
        std::string lens = package.substr(0, pos);          // substr(pos,len):从 pos 开始,截取 len 个字符
        int messagelen = std::stoi(lens);
        int total = lens.size() + messagelen + 2 * LineBreakSep.size();
        if (package.size() < total)
            return false;
        // 至少 package 内部一定有一个完整的报文了!
        *message = package.substr(pos + LineBreakSep.size(), messagelen);
        package.erase(0, total);
        return true;
    }

    // ==================== 二、请求(序列化/反序列化必读注释在这里) ====================

    class Request
    {
    public:
        Request() : _data_x(0), _data_y(0), _oper(0) {}
        Request(int x, int y, char op) : _data_x(x), _data_y(y), _oper(op) {}

        void Debug()
        {
            std::cout << "_data_x: " << _data_x << std::endl;
            std::cout << "_data_y: " << _data_y << std::endl;
            std::cout << "_oper: " << _oper << std::endl;
        }
        void Inc() { _data_x++; _data_y++; }

        // Json::Value 就像一个通用的"键值对容器",底层其实是一个 std::map
        //                        ↓
        //   你往里面塞什么 key-value 由你决定(这就是协议设计)
        //                        ↓
        //   但通信双方必须约定好同一套字段,否则发了也白搭

        // 序列化:结构化数据 -> 字符串
        // out 为输出型参数:函数内部把序列化好的 JSON 字符串写进 out 指向的对象,
        // 调用方通过它拿到结果
        bool Serialize(std::string *out)
        {
            Json::Value root;
            // 库的能力:能往 root 里塞任意 key-value
            root["datax"] = _data_x;
            root["datay"] = _data_y;
            root["oper"] = _oper;

            Json::FastWriter writer;   // 旧版接口,紧凑输出(无多余空格换行)
            // JSON 的本质:把一个内存中的哈希映射(字典/Map 对象),
            // 序列化(扁平化)成了一个普通的 std::string
            // 例:{"datax":10,"datay":20,"oper":43}
            *out = writer.write(root);
            return true;
        }

        // 反序列化:字符串 -> 结构化数据。in 是输入型参数
        bool Deserialize(std::string &in)
        {
            Json::Value root;
            Json::Reader reader;

            // parse(解析):判断这个字符串能否被 Json::Value 解析,返回布尔值;
            // 只有 true 才进行安全的字段取值(防止对残缺报文乱取值)
            bool res = reader.parse(in, root);
            if (res)
            {
                _data_x = root["datax"].asInt();   // 靠 "datax" 这个 key 找到值,赋给 _data_x
                _data_y = root["datay"].asInt();   // 靠 "datay" 找到值,赋给 _data_y
                _oper = root["oper"].asInt();      // 靠 "oper"  找到值,赋给 _oper
            }
            return res;
        }

        int GetX() { return _data_x; }
        int GetY() { return _data_y; }
        char GetOper() { return _oper; }

    private:
        int _data_x;
        int _data_y;
        char _oper;
    };

    // ==================== 三、应答(服务器返回给客户端的"响应") ====================

    class Response
    {
    public:
        Response() : _result(0), _code(0) {}
        Response(int result, int code) : _result(result), _code(code) {}

        bool Serialize(std::string *out)
        {
            Json::Value root;
            root["result"] = _result;
            root["code"] = _code;
            Json::FastWriter writer;
            *out = writer.write(root);
            return true;
        }

        bool Deserialize(std::string &in)
        {
            Json::Value root;
            Json::Reader reader;
            bool res = reader.parse(in, root);
            if (res)
            {
                _result = root["result"].asInt();
                _code = root["code"].asInt();
            }
            return res;
        }

        void SetResult(int res) { _result = res; }
        void SetCode(int code) { _code = code; }
        int GetResult() { return _result; }
        int GetCode() { return _code; }

    private:
        int _result;   // 运算结果
        int _code;     // 运算状态
    };

    // ==================== 四、工厂 ====================

    // ==================== 四、工厂 ====================

    // 对象工厂:封装 Request 和 Response 的创建
    class Factory
    {
    public:
        std::shared_ptr<Request> BuildRequest()
        {
            return std::make_shared<Request>();
        }
        std::shared_ptr<Request> BuildRequest(int x, int y, char op)
        {
            return std::make_shared<Request>(x, y, op);
        }
        std::shared_ptr<Response> BuildResponse()
        {
            return std::make_shared<Response>();
        }
        std::shared_ptr<Response> BuildResponse(int result, int code)
        {
            return std::make_shared<Response>(result, code);
        }
    };

} // namespace Protocol

9.3 为什么要用 Factory(工厂模式)

工厂模式 的核心价值:把对象的创建过程封装起来,尤其是当创建逻辑比较复杂(要初始化多个字段、要做资源分配、可能返回多种派生类型)时,调用方只需要 BuildXxx() 一句话拿到 shared_ptr。

这里搭配 std::shared_ptr 使用还有个额外好处:自动管理生命周期 。请求/应答对象会在网络层、业务层之间传递,用裸指针容易内存泄漏或悬空,交给 shared_ptr 就省心了。

9.4 序列化后长什么样?

json 复制代码
{"datax":10,"datay":20,"oper":43}

💡 注意 oper 是 char,塞进 JSON 后被当成了整数 43 (字符 '+' 的 ASCII 码)。反序列化时我们用 root["oper"].asInt() 取出来再隐式转回 char,收发两端是一致的,所以没毛病。

但这也提醒我们:JSON 的类型系统很简单(只有 null/bool/number/string/array/object),自定义类型要先达成转换约定。

9.5 Json::Value 到底是个什么容器?

⭐ Json::Value 可以理解为一个"万能键值对容器",底层就是一个 map(哈希/有序映射)。

复制代码
Json::Value root;                          // 一个空的 map<string, Value>
        │
        ├─ root["datax"] = 10;             // 塞入 key="datax", value=10
        ├─ root["datay"] = 20;             // 塞入 key="datay", value=20
        └─ root["oper"]  = 43;             // 塞入 key="oper",  value=43
                    │
                    ▼   write()
        {"datax":10,"datay":20,"oper":43}   // 一个扁平的 std::string

由此得出两个重要认知:

① JSON 的本质 = 把 map 扁平化成字符串

内存里的 map 是有结构的(树/哈希表),而网络上只能传一维字节。所谓序列化,就是把这个键值对结构按约定写法"拍扁"成一个 std::string ;反序列化则是反向把这串字符还原成一张 map。所以 JSON 天生只能表达"对象/数组/字符串/数字/布尔/null"这几种结构,复杂的自定义类型必须先降维。

② 塞什么 key 完全是"协议设计",必须两端对齐

环节 谁决定 风险
字段名(datax/result/code) 协议设计者(你) 两端不一致 → 取值全是 0,且不报错
字段类型(int/string) 序列化时决定 类型不匹配时 asInt()/asString() 会做隐式转换或抛异常

⚠️ Jsoncpp 的一个隐蔽坑 :root["typo_key"] 当键不存在时,不会报错、也不会返回空,而是自动新建一个 null 元素 ,此时 .asInt() 得到 0。所以"字段名写错"在本项目里表现为静默地得到 0 ,非常难查。想要严格检查,请先用 isMember("datax") 判断,或使用 at()(键不存在会抛异常)。

9.6 顺带复习:输出型参数 vs 输入型参数

本项目大量使用指针/引用传参,代码注释里特别标了这一点:

写法 语义 本项目中的例子
std::string *out 输出型参数:函数往里写结果,调用方事后读取 Serialize(std::string *out)
const std::string &in / std::string &in 输入型参数:函数从这里读数据 Deserialize(std::string &in)
std::string &package 输入输出型参数 :既读又改(Decode 会 erase 掉已消费的报文) Decode(std::string &package, ...)
std::string *message 输出型参数:切出来的报文正文 Decode(..., std::string *message)

💡 Decode 用引用接收 package 正是因为它需要就地删除已处理的那一段 (package.erase(0, total)),剩下的半包留着下次继续用 ------ 这正是"流式处理"的精髓。


十、Encode / Decode:给字节流加上边界(核心)

10.1 为什么需要它们

前面铺垫了那么多,真正的难点来了。TCP 是面向字节流 的,没有消息边界:

复制代码
发送方调用 3 次 send:  [AAAA]  [BBBB]  [CCCC]
                              │
接收方的接收缓冲区里可能是: [AA] [AABBB] [BCC] [CCC]   ← 完全无序、无从对应!

这就是著名的 粘包 / 拆包 问题。Encode/Decode 的任务就是人为地、显式地给报文加上边界。

10.2 Encode:加长度前缀(Length-Prefixed Frame)

cpp 复制代码
std::string Encode(const std::string &message)
{
    std::string len = std::to_string(message.size());
    std::string package = len + LineBreakSep + message + LineBreakSep;
    return package;
}

输入 "10 + 20" → 输出 "9\r\n10 + 20\r\n"

⭐ 记住这个分解式:最终报文 = 协议头(Header) + 业务数据(JSON字符串)

复制代码
len\r\n            +   {"datax":10,"datay":20,"oper":43}   +  \r\n
─────────────          ──────────────────────────────────      ────
协议头(长度 + 分隔)        业务数据(由 Serialize 产出)            结束分隔

Encode 干的事情本质上就是"字符串拼接" ,没有调用任何底层 socket API ------ 它纯粹是在应用层做字符串加工,这也再次印证了:协议 ≈ 字符串处理(课件原话:"很多工作都是在做字符串处理!")。
💡 为什么非得包这一层?(这段很关键)

每段 JSON 字符串是自己构建出来的,所以在内存里它当然是有边界的(std::string 自带 size);但一旦把这些字符串依次丢给传输层,它们就被首尾相连地灌进同一条 TCP 字节流,到时候就再也分辨不出谁是谁了。

所以第二层序列化(Encode)的本质,就是为每一段 JSON 消息封装出明确边界 ------ 每次找到边界,就能实现分包。

另外,这个格式还带了一点冗余性设计:

  • 头部 len\r\n:告诉你正文有多长 → 用于精确切分;
  • 尾部 \r\n:作为一个额外的边界标记 → 用于确认一个报文确实收完了。

实际上只靠 len 也能切(这是纯长度前缀法),只靠 \r\n 也能切(这是分隔符法,但当正文中出现 \r\n 时会误判)。两者结合更健壮,也更贴近 HTTP 等成熟文本协议的习惯。

10.3 Decode:从缓冲区里"切"出一个完整报文

cpp 复制代码
bool Decode(std::string &package, std::string *message)
{
    // 除了解包,我还想判断报文的完整性,能否正确处理具有"边界"的报文
    auto pos = package.find(LineBreakSep);          // ① 找第一个 \r\n
    if (pos == std::string::npos)
        return false;                                //    连长度行都没收全,继续等
    std::string lens = package.substr(0, pos);      // ② 取出长度字符串
    int messagelen = std::stoi(lens);               //    转成数字(正文长度)
    int total = lens.size() + messagelen + 2 * LineBreakSep.size(); // ③ 计算整个报文总长
    if (package.size() < total)
        return false;                                //    还不够一个完整报文,继续等
    // 至少 package 内部一定有一个完整的报文了!
    *message = package.substr(pos + LineBreakSep.size(), messagelen); // ④ 摘出正文
    package.erase(0, total);                        // ⑤ 从缓冲区头部删掉已处理的报文
    return true;
}

10.4 逐帧推演:缓冲区里那些"半包"的情况

课件里非常形象地列出了缓冲区内容的所有中间状态,这里整理成表:

缓冲区 package 当前内容 find(\r\n) len 需要总长 结果
"l" npos --- --- ❌ 返回 false,继续 recv
"len" npos --- --- ❌ 继续 recv
"len\r\n" pos=3 解析失败 --- ❌ 继续 recv
"len\r\nx" pos len total > 现长 ❌ 继续 recv
"len\r\nx op" pos len total > 现长 ❌ 继续 recv
"len\r\nx op y" pos len total > 现长 ❌ 只差一个边界符!继续 recv
"len\r\nx op y\r\n" pos len total == 现长 ✅ 切出一个完整报文!
"len\r\nx op y\r\n""len" pos len total ✅ 切出第1个;残留 "len"
"len\r\nx op y\r\n""len\r\nx op y\r\n" pos len total ✅ 切出第1个;残留第2个(粘包被正确拆解)

⭐ 核心思想:Decode 不只是"解包",它还同时回答了一个问题 ------ "你给的这段数据里,现在有没有至少一个完整报文?"

  • 返回 false:还不够 ,调用方继续 recv 往尾部追加;
  • 返回 true:成功取出一个完整报文,并且已经把它从缓冲区头部删掉了,剩余部分留着下次再处理。

10.5 UDP 为什么不用这套?

特性 TCP UDP
传输单位 字节流,无边界 数据报,天然带边界
一次发送对应 可能多次接收、也可能合并 一次 sendto 必然对应一次 recvfrom
是否需要 Encode/Decode 必须 recvfrom 拿到的就是一个完整报文,不需要切包
是否需要处理粘包 ✅ 需要 ❌ 不需要

💡 UDP 自带长度字段(8 字节首部里就有 length),内核会帮你把一个个数据报切好。代价是 UDP 不可靠。这就是"可靠性带来的复杂度债务"。


十一、流式数据的完整处理流程

有了上面的铺垫,服务端的处理循环就水到渠成了:

cpp 复制代码
void HandlerRequest(Socket *sock)
{
    std::string inbuffer;                 // ★ 用户态缓冲区,跨 recv 调用持久存在
    std::string clientip;
    uint16_t clientport;
    // ...
    while (true)
    {
        // ---------- 1. 读 ----------
        if (!sock->Recv(&inbuffer, 1024))  // 内部是 *buffer += inbuffer(拼接)
            break;                          // 对端关闭或出错

        // ---------- 2. 解包(可能要循环,因为可能粘了多个包)----------
        std::string message;
        while (Decode(inbuffer, &message)) // ★ 只要还切得出完整报文就继续切
        {
            // ---------- 3. 反序列化 ----------
            auto req = factory.BuildRequest();
            if (!req->Deserialize(message))
                continue;                   // 报文坏了,丢弃这一个,继续下一个

            // ---------- 4. 业务处理 ----------
            auto resp = cal.Calculator(req); // 真正的业务逻辑:计算 + 设置 _code

            // ---------- 5. 序列化 ----------
            std::string resp_str;
            resp->Serialize(&resp_str);

            // ---------- 6. 加报头 ----------
            resp_str = Encode(resp_str);

            // ---------- 7. 发送 ----------
            sock->Send(resp_str);
        }
    }
    sock->CloseSocket();
    delete sock;
}

⭐ 新手最常见的两个错误写法,务必对照自查:

  1. 把 inbuffer 定义在 while 循环内部 ------ 那每次循环半个包残留就丢了,永远解不出完整报文。它必须定义在循环外面。
  2. 只调用一次 Decode ------ 如果一次 recv 收到了 3 个包粘在一起,只 Decode 一次就丢 2 个。必须 while(Decode(...))。

一句话总结整个处置策略:先拷到用户缓冲区 → 再看够不够一个包 → 够了才解析。 网络 IO 和业务处理彻底解耦。


十二、深入理解:为什么选择文本协议而不是直接发结构体

很多同学会问:既然 Request 本身就是个结构体,直接 send(sockfd, &req, sizeof(req), 0) 不行吗?绝大多数情况下:不行。 原因有三大坑:

坑 1:内存对齐规则可能不同

cpp 复制代码
struct Request { int x; char oper; };
sizeof(Request) == ?     // 32位 gcc 默认 4 字节对齐 → 8,不是 5!

客户端和服务端如果是不同的编译器/编译选项/语言 (-fpack-struct、#pragma pack、Java/Go 写的客户端),结构体在内存里的排布就不一样,你 sizeof 出来的字节数不同,解析必然错位。

坑 2:大小端(字节序)问题

cpp 复制代码
int x = 1;
// x86/x64(小端)内存:  01 00 00 00
// 网络字节序(大端):    00 00 00 01

如果直接把内存里的 int 发出去,跨平台必然读反 。二进制协议就必须到处写 htonl/ntohl,漏一个就出玄学 bug。

⭐ 而 JSON 是文本协议 :"{"datax":10}" 里的 '1','0' 是两个 ASCII 字符,无论在什么机器上、什么大小端、什么编译器下,含义都完全一致。文本天然免疫字节序与内存对齐问题。

坑 3:版本兼容性与可读性

诉求 二进制结构体 JSON
加一个新字段 ❌ 老客户端全崩(布局变了) ✅ 老字段照读,新字段忽略即可
抓包能看懂吗 ❌ 一堆十六进制 ✅ nc/Wireshark 肉眼可读
跨语言(C++/Java/Python) ❌ 要各自实现布局 ✅ 所有语言都有 JSON 库

💡 结论 :牺牲一点点性能(文本解析比 memcpy 慢、体积更大),换来可读性、可调试性、跨语言性和向后兼容性。对绝大多数业务而言这笔交易是血赚的。这也是 HTTP/1.x、Redis RESP、绝大部分 RPC 框架选择文本协议的原因。


十三、再深入:这个自定义协议还有哪些可以改进的地方

课件里的实现作为教学版本非常清晰,但站在工程的角度,它还留下了几个可以打磨的点:

问题 现状 改进建议
std::stoi 可能抛异常 收到 "abc\r\n..." 就直接抛 std::invalid_argument,进程崩溃 用 strtol + 检查 endptr,或 try/catch,或先 isdigit 校验
FastWriter 已废弃 新版 jsoncpp 中 Json::FastWriter/Reader 被标记 deprecated 改用推荐的 Json::StreamWriterBuilder + Json::CharReaderBuilder
没有最大值保护 messagelen 可能被伪造成巨大值,导致内存耗尽 加一个 MAX_PACKAGE_SIZE 上限,超过就断开连接并计入黑名单
报文无校验字段 网络传输中数据被篡改无法察觉 加一个 CRC32 / MD5 校验和字段
Protocol.hpp 里还留着未使用的 ProtSep = " " 这是约定"正文内部字段分隔符"时的遗留痕迹 既然正文用 JSON 自描述,ProtSep 可以删掉;若正文是 "x op y" 这种空白分隔形式,则它会被使用并在两边保持一致
Recv 用 += 拼接二进制不安全 遇 '\0' 会截断 二进制协议请改用 std::string::append(ptr, len) 指定长度追加
单条 Buffer + 多条请求 目前一个连接串行处理 可加线程池/IO多路复用进一步提升并发

📌 关于 protocol_code 的扩展 :在 len 之前再加一个协议编号字段,就演化成 "protocol_code\r\nlen\r\nx op y\r\n"。服务端读到 protocol_code 后查一张"协议号 → 处理函数"的分发表即可支持多套协议共存 ------ 这就是 RPC 框架协议分派的最小雏形。 所以"今天定义了几组协议?可以同时存在多个协议吗?"答案是:可以同时存在多组,靠编号区分即可。


十四、序列化方案横向对比

方案 格式 可读性 体积 速度 IDL/schema 典型应用
JSON (Jsoncpp) 文本 ✅ 极好 大 中 ❌ 无(弱类型) Web API、配置文件、本篇
XML 文本 ✅ 好(但冗长) 最大 慢 ✅ XSD 早期 WebService、SOAP
Protobuf 二进制 ❌ 差 最小 最快 ✅ .proto 强 schema gRPC、微服务内部通信
MessagePack 二进制 ❌ 差 小 快 半(兼容 JSON 模型) Redis、游戏、IoT
FlatBuffers 二进制 ❌ 差 小 极快(零拷贝) ✅ .fbs 游戏、高性能移动端
BSON 二进制 中 中 中 ❌ MongoDB

选型建议:

  • 需要给人看 / 调试方便 / 对外接口 → JSON
  • 内部高性能 RPC / 带宽敏感 → Protobuf
  • 极致性能、避免反序列化开销 → FlatBuffers

十五、高频面试题

Q1:什么是粘包?为什么 TCP 有而 UDP 没有?

TCP 面向字节流,只保证字节顺序、不保留消息边界,多个 send 的数据可能在接收缓冲区中粘在一起或被拆开。UDP 面向数据报,一次 sendto 对应一个完整的 recvfrom,内核自带长度,天然有边界。

Q2:解决粘包的方案有哪几种?

① 定长包 :每个包固定 N 字节,不够则补位(简单但浪费);② 分隔符法 :用 \r\n 等特殊字符结尾(正文不能含分隔符,需转义);③ 长度前缀法 (本文方案):包头写明正文长度,最常用、最可靠;④ 更高层的协议自动处理(HTTP 用 Content-Length/chunked)。

Q3:send() 成功返回就一定代表对方收到数据了吗?

不是。 send() 只是把数据从用户空间拷贝 到内核发送缓冲区。真正何时发、发多少、要不要重传,由 TCP 自己决定。要保证对方"处理完",需要应用层自己做应用层 ACK。

Q4:为什么一个 TCP sockfd 既能读又能写?

因为这个 fd 对应着内核中互相独立的两个缓冲区 (接收缓冲区 + 发送缓冲区)。这是 TCP 全双工的物理基础;对比 pipe 只有单向缓冲区,所以必须开两个 fd。

Q5:可以直接把结构体 send 出去吗?

强烈不建议。会遇到内存对齐不一致、大小端不一致、版本扩展不兼容三大问题(详见第十二节)。应通过序列化转换成跨平台一致的格式(如 JSON/Protobuf)。

Q6:为什么 Response 里要有 _code 状态码,直接返回结果不行吗?

不行。请求可能因为业务原因失败(除零、非法运算符、权限不足),只靠返回值无法区分"结果是 0"和"计算失败"。状态码 + 载荷的模式是所有成熟协议的通用做法(如 HTTP 200/404/500)。

Q7:Serialize() 和 Encode() 都在"变字符串",区别是什么?

它们是两个层次:Serialize 解决业务对象 ↔ 消息体字符串 (让数据可表达),Encode 解决消息体字符串 ↔ 完整网络报文 (让数据可切分)。两者都属于广义的序列化。之所以要分开,是因为 Request/Response 结构体只描述"有哪些字段",并不规定这些字段怎么变成字节流上线。

一句话:Serialize 决定消息长什么样,Encode 决定这条消息从哪儿开始、到哪儿结束。

Q8:如果客户端和服务端的 JSON 字段名写不一致,会立刻报错吗?

不会报错,但更危险。 Jsoncpp 的 operator[] 在键不存在时会自动创建一个 null 元素 ,asInt() 拿到的是 0 。所以现象是"数据永远是默认值,程序却一切正常"。排查手段:先用 root.isMember("key") 检查,或改用 at()(键不存在会抛异常)。


附录:Jsoncpp 完整用法手册

A.1 简介

Jsoncpp 是一个用于处理 JSON 数据的 C++ 库,提供将 JSON 序列化为字符串以及从字符串反序列化为 C++ 数据结构的能力。它开源且被广泛使用。

特性:

  1. 简单易用:直观的 API。
  2. 高性能:性能经过优化,能高效处理大量 JSON 数据。
  3. 全面支持:支持 JSON 标准全部数据类型 ------ 对象、数组、字符串、数字、布尔值和 null。
  4. 错误处理:解析 JSON 时提供详细的错误信息和出错位置,方便调试。

A.2 安装

bash 复制代码
# Ubuntu
sudo apt-get install libjsoncpp-dev
# CentOS
sudo yum install jsoncpp-devel

A.3 序列化:三种写法

方式一:Json::Value::toStyledString()(最简)
cpp 复制代码
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    root["name"] = "joe";
    root["sex"] = "男";
    std::string s = root.toStyledString();
    std::cout << s << std::endl;
    return 0;
}

输出(带缩进美化):

json 复制代码
{
   "name" : "joe",
   "sex" : "男"
}
方式二:Json::StreamWriter(可定制缩进/换行)
cpp 复制代码
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;
    root["name"] = "joe";
    root["sex"] = "男";

    Json::StreamWriterBuilder wbuilder;   // StreamWriter 的工厂
    std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
    std::stringstream ss;
    writer->write(root, &ss);
    std::cout << ss.str() << std::endl;
    return 0;
}

想要压缩成一行的风格,可以设置 wbuilder["indentation"] = "";

方式三:Json::FastWriter(无多余空白,最快)
cpp 复制代码
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;

输出:

json 复制代码
{"name":"joe","sex":"男"}

📌 与之对应的是 Json::StyledWriter(同 toStyledString,美化输出)。注意:新版 Jsoncpp 中 FastWriter/StyledWriter 已被标记为 deprecated ,推荐改用 StreamWriterBuilder。

A.4 反序列化

cpp 复制代码
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
    std::string json_string = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";

    Json::Reader reader;
    Json::Value root;

    bool parsingSuccessful = reader.parse(json_string, root);
    if (!parsingSuccessful)
    {
        // 解析失败,输出错误信息
        std::cout << "Failed to parse JSON: "
                  << reader.getFormattedErrorMessages() << std::endl;
        return 1;
    }

    std::string name = root["name"].asString();
    int age = root["age"].asInt();
    std::string city = root["city"].asString();

    std::cout << "Name: " << name << std::endl;
    std::cout << "Age: " << age << std::endl;
    std::cout << "City: " << city << std::endl;
    return 0;
}

输出:

复制代码
Name: 张三
Age: 30
City: 北京

Json::CharReader 的派生类也可用于更精细地控制解析过程,但通常 Json::Reader::parse() 或 Json::parseFromStream 已经足够。

总结:

  • toStyledString、StreamWriter、FastWriter 提供了不同序列化选项,按需选取;
  • Json::Reader 和 parseFromStream 是主要反序列化工具,错误处理机制强大;
  • 序列化和反序列化时务必处理所有可能的错误情况,并校验输入输出的有效性。

A.5 Json::Value 常用 API 清单

① 构造函数

API 说明
Json::Value() 默认构造,创建空的 Value 对象
Json::Value(ValueType type, bool allocated = false) 按给定类型创建(nullValue/intValue/stringValue 等)

② 访问元素

API 说明
Json::Value& operator[](const char* key) 按键访问对象元素;键不存在则新建
Json::Value& operator[](const std::string& key) 同上,参数为 std::string
Json::Value& operator[](ArrayIndex index) 按索引访问数组元素;越界则新建
Json::Value& at(const char* key) 按键访问;键不存在则抛异常
Json::Value& at(const std::string& key) 同上,参数为 std::string

③ 类型检查

API 说明
bool isNull() 是否为 null
bool isBool() 是否为布尔类型
bool isInt() 是否为整数类型
bool isInt64() 是否为 64 位整数
bool isUInt() 是否为无符号整数
bool isUInt64() 是否为 64 位无符号整数
bool isIntegral() 是否为整数或可转为整数的浮点数
bool isDouble() 是否为双精度浮点数
bool isNumeric() 是否为数字(整数或浮点数)
bool isString() 是否为字符串
bool isArray() 是否为数组
bool isObject() 是否为对象(键值对集合)

④ 赋值与类型转换

API 说明
Json::Value& operator=(bool/int/unsigned int/Int64/UInt64/double/const char*/const std::string&) 把对应类型的值赋给 Value
bool asBool() 转为布尔类型(如可行)
int asInt() 转为 int
Int64 asInt64() 转为 64 位整数
unsigned int asUInt() 转为无符号整数
UInt64 asUInt64() 转为 64 位无符号整数
double asDouble() 转为双精度浮点
std::string asString() 转为字符串

⑤ 数组与对象操作

API 说明
size_t size() 返回数组/对象中的元素数量
bool empty() 是否为空
void resize(ArrayIndex newSize) 调整数组大小
void clear() 删除所有元素
void append(const Json::Value& value) 在数组末尾追加元素
Json::Value& operator[](key, const Json::Value& defaultValue = Json::nullValue) 插入或访问对象元素,键不存在时使用默认值

总结

回顾整条知识链路:

  1. 应用层 是我们写业务的地方,TCP 只搬字节 → 所以必须自定义协议;
  2. 协议 = 约定好的结构化数据 ,用 Request/Response 结构体来表达;
  3. 结构体 ↔ 字符串互转 = 序列化 / 反序列化 ,本例用 Jsoncpp (Json::Value 本质是一个 k-v 容器,JSON 就是把它扁平化成字符串);
  4. 序列化分为两层 :Serialize/Deserialize 负责业务对象 ↔ 消息体字符串 ,Encode/Decode 负责消息体字符串 ↔ 完整网络报文;结构体只定字段,报文格式由 Encode 定;
  5. TCP 是面向字节流 的,send/recv 本质是把数据往内核缓冲区里拷 ,"何时发、发多少"由 TCP 控制 → JSON 一进传输层就再也分不出彼此,必须自己加报文边界;
  6. Encode 干的活就是拼接 :最终报文 = 协议头(len\r\n) + 业务数据(JSON) + \r\n;Decode 则是先看够不够一个完整报文,够才切,并把已消费的部分 erase 掉;
  7. 服务端处理循环 = recv 拼接到用户缓冲区 → while(Decode) → Deserialize → 业务 → Serialize → Encode → send;注意客户端与服务端对两个类的用法方向相反(客户端用 Request 发、Response 收;服务端反之);
  8. 同一个 sockfd 既能读又能写,因为它背后是收发两个独立缓冲区 → TCP 全双工。

打油诗收尾:

字节流里无边界,全靠长度来切线;文本协议免字节序,一纸 JSON 走天下。

相关推荐
m0_715674433 小时前
金融数据流转监测排行:从看见到拦截的距离
大数据·网络·安全·网络安全·金融
想做小南娘,发现自己是女生喵4 小时前
第三周学习文档 Linux C 多进程与多线程
linux·c语言·学习
盛世宏博智慧档案4 小时前
TCP以太网温湿度传感器:从RS485痛点突围到工业组网实战
网络·网络协议·tcp/ip
富洋金锐4 小时前
windows-winmtr与pathping命令-网络丢包排查
网络·windows
此生决int4 小时前
Linux系列(06)——进程概念(上)
linux·运维·服务器
m0_547486665 小时前
《网络与信息安全》全套PPT课件2026
网络·网络安全
星云低代码开发平台5 小时前
AI 连接器运行健康度:指标、异常恢复和联调清单
大数据·运维·网络·数据库·人工智能·mcp
花间相见5 小时前
【计算基础|网络12】TCP 抓包与经典边界问题
网络·tcp/ip·策略模式