【Linux之旅】Linux 应用层自定义协议与序列化:从粘包问题到网络计算器

请君浏览

    • 前言
    • 一、再谈"协议"------结构化数据的约定
      • [1.1 为什么字符串不够用](#1.1 为什么字符串不够用)
      • [1.2 两种方案](#1.2 两种方案)
    • [二、TCP 全双工与缓冲区------理解 write 不等价于"发送"](#二、TCP 全双工与缓冲区——理解 write 不等价于"发送")
    • 三、流式数据与粘包------为什么需要协议帧
      • [3.1 粘包的本质](#3.1 粘包的本质)
      • [3.2 解决方案:长度前缀 Encode/Decode](#3.2 解决方案:长度前缀 Encode/Decode)
    • 四、完整实现------网络版计算器
      • [4.1 代码结构](#4.1 代码结构)
      • [4.2 Socket 封装------模板方法模式](#4.2 Socket 封装——模板方法模式)
      • [4.3 协议定义------Request/Response + 序列化 + Encode/Decode](#4.3 协议定义——Request/Response + 序列化 + Encode/Decode)
    • [五、jsoncpp 快速入门](#五、jsoncpp 快速入门)
    • 六、常见问题与避坑指南
      • [6.1 一次 recv 读到多条消息,没循环消费](#6.1 一次 recv 读到多条消息,没循环消费)
      • [6.2 Encode/Decode 分隔符不一致](#6.2 Encode/Decode 分隔符不一致)
      • [6.3 忘记处理 recv 返回 0](#6.3 忘记处理 recv 返回 0)
      • [6.4 一个进程上同时运行多个协议](#6.4 一个进程上同时运行多个协议)
    • 七、协议设计的进阶话题
      • [7.1 协议版本号------让协议可演进](#7.1 协议版本号——让协议可演进)
      • [7.2 序列化方案对比:JSON vs Protobuf vs XML](#7.2 序列化方案对比:JSON vs Protobuf vs XML)
      • [7.3 多协议并存------在一个端口上服务多种协议](#7.3 多协议并存——在一个端口上服务多种协议)
      • [7.4 jsoncpp 的替代方案:nlohmann/json](#7.4 jsoncpp 的替代方案:nlohmann/json)
    • 总结
    • 尾声

前言

回顾:Linux TCP Socket 编程实战:从单连接到线程池,构建高并发服务端

前三篇我们依次掌握了网络基础概念、UDP Socket 编程和 TCP Socket 编程。但写一个完整的网络应用,还有一道坎要迈------应用层协议设计 。之前的 Echo 和 DictServer 只是收发字符串,没有"协议"的概念。真正的网络程序需要传输结构化的数据:一个计算器请求包含两个操作数和一个运算符;一个 HTTP 请求包含方法、路径、头部和正文。这些数据的格式、编码和解码规则,就是应用层协议。

本文将围绕"网络版计算器"这个具体需求,从协议设计思路出发,引出序列化/反序列化的必要性,用 jsoncpp 实现数据格式转换,再深入 TCP 流式数据的粘包问题并给出 Encode/Decode 方案,最后封装一个完整的通用 Socket 框架。读完本文,你将掌握从零设计一个应用层协议并写成一个完整网络服务的全流程。

一、再谈"协议"------结构化数据的约定

1.1 为什么字符串不够用

之前我们用 socket 收发数据,都是按字符串来处理的。服务端 read(buffer) 拿到客户端发来的 "hello",直接打印。这在回声服务器和查字典的场景中没有问题------因为数据本身就是字符串。

但换一个需求:网络版加法器。客户端把两个加数发给服务器,服务器计算后返回结果。此时数据天然就是结构化的:

复制代码
客户端需要发送:第一个数 100,第二个数 200,运算符 '+'
服务端需要返回:结果 300,状态码 0(表示成功)

1.2 两种方案

方案一:字符串格式约定

复制代码
客户端发送 "100+200"
服务端收到后按 '+' 拆分,拿到 "100" 和 "200",计算,返回 "300"

简单直接,但脆弱------运算符多了怎么办(*/%)?数字是浮点数怎么办?怎么区分 10+2510+250 少传了一个字符?

方案二:结构化数据 + 序列化

步骤 发送方 接收方
1 定义结构体,填入数据 ---
2 序列化:结构体 → 字符串 ---
3 发送 接收
4 --- 反序列化:字符串 → 结构体
5 --- 读取结构体字段,执行业务

序列化 = 将结构化数据(对象/结构体)按某套规则转换为可传输的字符串或字节流。反序列化 = 反过来,从字符串恢复为结构化数据。

无论采用哪种方案,只要保证一端构造的数据,另一端能正确解析,就是一份可用的应用层协议。

二、TCP 全双工与缓冲区------理解 write 不等价于"发送"

在开始编码之前,有一个认知需要校准:

cpp 复制代码
// write() 只是把数据从用户态拷贝到内核的发送缓冲区
ssize_t n = write(sockfd, buffer, size);
// TCP 自己决定什么时候真正发、发多少、出错怎么办
// ------所以叫"传输控制协议"

// read() 只是从内核的接收缓冲区取出已经到达的数据
ssize_t n = read(sockfd, buffer, size);
// 一次 read 读到的内容,不一定是对方一次 write 写入的内容
  • 任何一台主机上的 TCP 连接,既有发送缓冲区 又有接收缓冲区
  • 这就是为什么一个 TCP sockfd 可以同时收发------全双工
  • 这也引出了下面要解决的核心问题------粘包

三、流式数据与粘包------为什么需要协议帧

3.1 粘包的本质

TCP 是面向字节流的协议。这句话的含义是:数据在发送端被拆成字节流,在接收端又被拼回字节流,中间没有消息边界。

考虑这个场景:客户端连续发送了两条 JSON 请求:

复制代码
发送:{"datax":1,"datay":2,"oper":43}
发送:{"datax":10,"datay":20,"oper":43}

服务端一次 read 可能收到:

复制代码
情况A: 只读到第一条的前半截  {"datax":1,"d
情况B: 刚好读到完整的第一条   {"datax":1,"datay":2,"oper":43}
情况C: 读到第一条+第二条前半  {"datax":1,...}{"datax":
情况D: 读到两条完整消息        {"datax":1,...}{"datax":10,...}

你无法控制 read 返回多少字节。 这就是"粘包"------消息边界在字节流中被模糊了。

3.2 解决方案:长度前缀 Encode/Decode

在每条消息前面加上一个长度字段,让接收方知道"这条消息有多长":

复制代码
报文格式: "len\r\nmessage\r\n"

例如: "27\r\n{"datax":1,"datay":2,"oper":43}\r\n"
       └─┬─┘ └──────────┬──────────────┘
      长度=27         消息体(27字节)
cpp 复制代码
const std::string LineBreakSep = "\r\n";

// Encode: 给消息加上长度前缀
std::string Encode(const std::string &message)
{
    std::string len = std::to_string(message.size());
    // "27\r\n{"datax":1,"datay":2,"oper":43}\r\n"
    return len + LineBreakSep + message + LineBreakSep;
}

// Decode: 从字节流中取出一条完整消息
bool Decode(std::string &package, std::string *message)
{
    // ① 找到第一个 \r\n,它前面的就是长度字段
    auto pos = package.find(LineBreakSep);
    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;          // 报文还没收全,继续等

    // ⑤ 完整报文!取出消息体
    *message = package.substr(pos + LineBreakSep.size(), messagelen);
    package.erase(0, total);   // 从缓冲区中移除已取出的部分
    return true;
}

Decode 的核心思想:

步骤 做了什么 失败意味着
① 找分隔符 定位长度字段 连长度都没收全
② 解析长度 知道消息体要多长 长度字段损坏
③ 算总长 长度字段 + 两个分隔符 + 消息体 ---
④ 判完整性 package.size() >= total 消息体还没收全
⑤ 提取+清除 拿走一条,剩下的留着 ---

Decode 的关键在于它是无破坏性 的:如果报文不完整,它返回 false 但不修改 package;如果报文完整,它取出消息后只删除已消费的部分,剩余数据留在缓冲区中等待下一次 Decode。这恰好完美适配了下面要介绍的 Recv 设计。

四、完整实现------网络版计算器

4.1 代码结构

复制代码
Calculate.hpp     // 业务逻辑(计算器)
Protocol.hpp      // 协议定义(Request/Response + 序列化 + Encode/Decode)
Socket.hpp        // Socket 封装(模板方法模式)
TcpServer.hpp     // TCP 服务器框架
Daemon.hpp        // 守护进程化
TcpServerMain.cc  // 服务端入口
TcpClientMain.cc  // 客户端入口

4.2 Socket 封装------模板方法模式

先用面向对象的方式将 Socket API 封装为可扩展的接口:

cpp 复制代码
// Socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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

namespace Net_Work
{
    const static int defaultsockfd = -1;
    const int backlog = 5;
    enum { SocketError = 1, BindError, ListenError };

    // 抽象基类------定义接口,子类实现
    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;

        // 模板方法------组合多个虚函数,定义固定流程
        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);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd) {}

        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);
            if (::bind(_sockfd, Convert(&local), sizeof(local)) < 0)
                exit(BindError);
        }

        void ListenSocketOrDie(int backlog) override
        {
            if (::listen(_sockfd, backlog) < 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);
            return new TcpSocket(newsockfd);
        }

        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);
            return ::connect(_sockfd, Convert(&server),
                             sizeof(server)) == 0;
        }

        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;  // 注意:是拼接!配合 Decode 循环使用
                return true;
            }
            return false;  // n==0 对端关闭, n<0 错误
        }

        void Send(std::string &send_str) override
        {
            send(_sockfd, send_str.c_str(), send_str.size(), 0);
        }

    private:
        int _sockfd;
    };
}

Recv 中 *buffer += inbuffer 是刻意为之。 它把每次接收到的字节追加到 buffer 末尾,而不是覆盖。这样就实现了一个"无限增长的接收缓冲区"------刚收的半截报文和新收到的数据自然拼接在一起,供 Decode 反复消费。

4.3 协议定义------Request/Response + 序列化 + Encode/Decode

cpp 复制代码
// Protocol.hpp
#pragma once
#include <iostream>
#include <memory>
#include <jsoncpp/json/json.h>

namespace Protocol
{
    const std::string LineBreakSep = "\r\n";

    // ==================== 帧层:解决粘包 ====================

    // Encode: 消息 → "len\r\n消息体\r\n"
    std::string Encode(const std::string &message)
    {
        std::string len = std::to_string(message.size());
        return len + LineBreakSep + message + LineBreakSep;
    }

    // Decode: 从字节流中提取一条完整消息(无破坏性,不完整时返回false)
    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);
        int messagelen = std::stoi(lens);
        int total = lens.size() + messagelen + 2 * LineBreakSep.size();
        if (package.size() < total) return false;
        *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) {}

        // 序列化:结构体 → JSON 字符串
        bool Serialize(std::string *out)
        {
            Json::Value root;
            root["datax"] = _data_x;
            root["datay"] = _data_y;
            root["oper"] = _oper;
            Json::FastWriter writer;
            *out = writer.write(root);
            return true;
        }

        // 反序列化:JSON 字符串 → 结构体
        bool Deserialize(std::string &in)
        {
            Json::Value root;
            Json::Reader reader;
            if (!reader.parse(in, root)) return false;
            _data_x = root["datax"].asInt();
            _data_y = root["datay"].asInt();
            _oper = root["oper"].asInt();
            return true;
        }

        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;
            if (!reader.parse(in, root)) return false;
            _result = root["result"].asInt();
            _code = root["code"].asInt();
            return true;
        }

        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;      // 状态码(0=成功, 非0=错误)
    };

    // ==================== 工厂:简化对象创建 ====================

    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);
        }
    };
}

协议的双层设计:

职责 核心函数 解决的问题
帧层 报文边界 Encode / Decode TCP 粘包------识别一条完整报文从哪里开始到哪里结束
数据层 内容编解码 Serialize / Deserialize 结构化数据 ↔ 字符串的转换

这种分层设计让协议具有可扩展性 :将来换 JSON 为 Protobuf,只改数据层,帧层不受影响。也可以在同一套帧层基础上同时支持多种协议(通过协议码区分------Decode 取出完整报文后,根据报文的第一个字段判断交给哪个 Deserialize)。

主循环------Encode/Decode 驱动全流程:

cpp 复制代码
// 服务端处理逻辑
void Service(Socket *sock)
{
    std::string package;  // 接收缓冲区------跨循环保持,累积所有收到的字节
    Factory factory;

    while (true)
    {
        // ① 收数据(追加到 package,不覆盖)
        if (!sock->Recv(&package, 1024))
            break;         // 对端关闭或出错

        // ② 循环消费 package 中所有完整报文
        std::string message;
        while (Decode(package, &message))
        {
            // ③ 反序列化 → 业务处理 → 序列化响应
            auto req = factory.BuildRequest();
            req->Deserialize(message);

            int result = 0;
            if (req->GetOper() == '+')
                result = req->GetX() + req->GetY();
            // ... 其他运算 ...

            auto resp = factory.BuildResponse(result, 0);
            std::string resp_str;
            resp->Serialize(&resp_str);

            // ④ 编码并发送
            std::string send_str = Encode(resp_str);
            sock->Send(send_str);
        }
    }
}

主循环拆解:

步骤 做了什么 关键
sock->Recv(&package, ...) 收数据追加到缓冲区 *buffer += inbuffer 是拼接
while (Decode(package, &message)) 循环取出所有完整报文 一次 Recv 可能收到多条报文
Deserialize → 运算 → Serialize 反序列化→业务→序列化 这是唯一随需求变化的部分
Encode(resp_str) → sock->Send 编码并发送响应 对称结构:收/解/业务/编/发

五、jsoncpp 快速入门

jsoncpp 是一个轻量级 C++ JSON 库,安装简单:

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

序列化(结构体 → 字符串):

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

// 方式一:FastWriter------紧凑格式,适合网络传输
Json::Value root;
root["datax"] = 100;
root["datay"] = 200;
root["oper"] = '+';
Json::FastWriter writer;
std::string s = writer.write(root);
// 输出: {"datax":100,"datay":200,"oper":43}
//                                        ^ ascii码43='+'

// 方式二:StyledWriter------美化格式,适合日志调试
Json::StyledWriter styled;
std::string pretty = styled.write(root);
// 输出:
// {
//    "datax" : 100,
//    "datay" : 200,
//    "oper" : 43
// }

反序列化(字符串 → 结构体):

cpp 复制代码
std::string json_string = R"({"datax":100,"datay":200,"oper":43})";

Json::Value root;
Json::Reader reader;
if (reader.parse(json_string, root))
{
    int x = root["datax"].asInt();   // 100
    int y = root["datay"].asInt();   // 200
    char op = root["oper"].asInt();  // '+' (ASCII 43)
}
else
{
    std::cerr << "Parse error: " << reader.getFormattedErrorMessages() << std::endl;
}
函数 方向 适用场景
FastWriter::write 序列化 网络传输(紧凑无空格)
StyledWriter::write 序列化 日志/调试(方便读)
Reader::parse 反序列化 通用的字符串→Value 解析

六、常见问题与避坑指南

6.1 一次 recv 读到多条消息,没循环消费

现象: 客户端连续发了三个请求,服务端只回复了第一个。

原因: recv 可能一次收到多条完整报文,如果只调用一次 Decode,剩下的报文被丢弃。

解决:while (Decode(...)) 循环消费,直到 Decode 返回 false(缓冲区中的报文头部都不完整了就退出,等下次 recv 追加更多数据再继续)。

6.2 Encode/Decode 分隔符不一致

现象: Decode 始终返回 false

原因: Encode"\n"Decode"\r\n"------两个函数的分隔符不一致。

解决: 统一定义 const std::string LineBreakSep,Encode 和 Decode 都引用同一常量。

6.3 忘记处理 recv 返回 0

现象: 客户端断开后服务端陷入死循环。

原因: TCP 中 recv 返回 0 表示对端关闭连接。如果不检查这个分支,后续调用会一直返回错误但循环不退出。

解决:

cpp 复制代码
if (!sock->Recv(&package, 1024))
    break;  // recv 返回 0 或 <0 都退出

6.4 一个进程上同时运行多个协议

问: 一个服务器能同时支持计算器协议和字典查询协议吗?

答: 可以。在 Decode 取出完整报文后,报文头部增加一个协议码字段来标识:

复制代码
报文格式: "len\r\nprotocol_code message\r\n"
例如:     "32\r\nCALC {"datax":1,"datay":2,"oper":43}\r\n"
          "23\r\nDICT {"word":"apple"}\r\n"

然后根据 protocol_code 选择对应的 Deserialize 函数即可。


七、协议设计的进阶话题

7.1 协议版本号------让协议可演进

协议不是一成不变的。你今天定义的计算器协议只支持整数加法,三个月后产品经理要求支持浮点四则运算。怎么办?在报文中加入协议版本号

复制代码
报文格式: "version len\r\nmessage\r\n"

例如: "1 27\r\n{"datax":1,"datay":2,"oper":43}\r\n"
      版本号=1
cpp 复制代码
std::string EncodeV2(const std::string &message, int version)
{
    std::string header = std::to_string(version) + " " + std::to_string(message.size());
    return header + LineBreakSep + message + LineBreakSep;
}

bool DecodeV2(std::string &package, std::string *message, int *version)
{
    // 先解析版本号和长度
    auto pos = package.find(' ');
    if (pos == std::string::npos) return false;
    *version = std::stoi(package.substr(0, pos));
    int messagelen = std::stoi(package.substr(pos + 1, package.find(LineBreakSep) - pos - 1));
    // ... 后续与 Decode 类似
}

版本号给了协议向后兼容 的能力------服务端检测到旧版本(v1),走旧的解析逻辑;检测到新版本(v2),走新的逻辑。没有版本号的协议,升级时要么强制所有客户端升级(不现实),要么搞出各种 if-else 判断字段是否存在(脆弱)。

7.2 序列化方案对比:JSON vs Protobuf vs XML

我们用的是 jsoncpp,但 JSON 不是唯一的序列化方案。三种主流方案的对比:

对比维度 JSON Protobuf(Google) XML
可读性 ✅ 可直接阅读 ❌ 二进制不可读 ✅ 可读(冗长)
数据体积 中等(有字段名冗余) 极小(字段名被编号替代) 大(成对的标签)
解析速度 中等 极快(二进制直接映射) 慢(需解析标签嵌套)
Schema 要求 无(自由格式) 必须有(.proto 文件定义结构) 可选(DTD/XSD)
跨语言支持 ✅ 所有语言 ✅ 主流语言都有 ✅ 所有语言
适合场景 Web API、日志、配置文件 微服务间通信、移动端与服务端通信、对带宽和速度敏感的 RPC 企业系统对接、SOAP Web Service

我们的网络计算器用 JSON 足够了------简单、可视、调试方便。但如果这一天到来:你的网络服务 QPS 从 1000 涨到 100000,带宽从 1Mbps 涨到 1Gbps,切换 Protobuf 可以把数据传输量减半、解析速度翻倍。协议选型应该从实际需求出发,而非一味追求"先进"。

7.3 多协议并存------在一个端口上服务多种协议

如果同一台服务器想要同时支持计算器和字典查询,有两种做法:

做法一:多端口------计算器 8888、字典 8889。简单粗暴,但浪费端口。

做法二:协议码------在帧层 Encode 时加入一个协议标识字段:

复制代码
报文格式: "protocol_code len\r\nmessage\r\n"

例如: "CALC 27\r\n{"datax":1,...}\r\n"   ← 计算器请求
      "DICT 20\r\n{"word":"apple"}\r\n"  ← 字典查询请求
cpp 复制代码
// 在 Decode 取出完整 message 后,根据 protocol_code 分发
if (protocol_code == "CALC")
{
    auto req = factory.BuildRequest();
    req->Deserialize(message);
    // 执行计算...
}
else if (protocol_code == "DICT")
{
    auto query = factory.BuildDictQuery();
    query->Deserialize(message);
    // 查字典...
}

这就是 HTTP、SMTP、FTP 等众多协议能共用 80/25/21 等端口的原因------每个协议有自己固定的端口号,/etc/services 文件列出了所有知名端口分配。

7.4 jsoncpp 的替代方案:nlohmann/json

如果你觉得 jsoncpp 的 API 不够直观(Json::ValueJson::ReaderJson::FastWriter 三个类来回调),可以考虑 nlohmann/json------单头文件、现代化 C++11 API:

cpp 复制代码
#include <nlohmann/json.hpp>
using json = nlohmann::json;

// 序列化------像写 map 一样写 JSON
json j;
j["datax"] = 100;
j["datay"] = 200;
j["oper"] = '+';
std::string s = j.dump();  // {"datax":100,"datay":200,"oper":43}

// 反序列化------像读 map 一样读 JSON
json parsed = json::parse(s);
int x = parsed["datax"];    // 100
int y = parsed["datay"];    // 200
char op = parsed["oper"];   // 43 ('+')

nlohmann/json 的 API 风格更接近现代 C++------operator[] 直接读写、dump() 序列化、parse() 反序列化,不需要记住三个不同的类名。但 jsoncpp 的优势在于大部分 Linux 发行版直接 apt/yum 安装,而 nlohmann/json 需要手动引入头文件。


总结

应用层协议设计 = 帧层(Encode/Decode)+ 数据层(Serialize/Deserialize):

层级 解决的问题 核心方案
帧层 TCP 粘包,消息边界不清 长度前缀:"len\r\n消息\r\n"
数据层 结构化数据 ↔ 字符串转换 JSON 序列化/反序列化(jsoncpp)
业务层 具体的数据处理逻辑 请求→计算→响应

完整数据链路:

复制代码
客户端:
  填入Request → Serialize → Encode → send
                                        ↓
服务端:                                 ↓
  recv → 追加到package → while(Decode) → Deserialize → 业务处理
       → Serialize响应 → Encode → send →

客户端:
  recv → 追加到package → while![[]](Decode) → Deserialize → 拿到结果

动手试试

  1. 给计算器服务器增加 '*''/''%' 运算支持(提示:只需修改 Service 中的 if-else 分支,注意除零检查设置 _code 为非 0)。
  2. 在 Encode 之前加一个协议码 字段(如 "CALC"),让同一套服务器能同时支持计算器和英译汉两个协议(提示:Decode 后根据协议码选择用 Request::Deserialize 还是 Dict::Translate)。

尾声

本章讲解就到此结束了,若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!

更多内容可见主页

推荐阅读:

相关推荐
2401_892423361 小时前
OSPF笔记
网络·智能路由器
草莓熊Lotso1 小时前
【Linux网络】深入理解 HTTP 协议(二):从协议格式到手写工业级 HTTP 服务器
linux·运维·服务器·网络·c++·http
The Straggling Crow7 小时前
Network
网络
剑神一笑7 小时前
Linux pgrep 命令详解:按名称查找进程 PID 的高效方法
linux·运维·chrome
yyuuuzz8 小时前
独立站的技术基础与常见运维问题
大数据·运维·服务器·网络·数据库·aws
剑神一笑8 小时前
Linux killall 命令详解:按进程名批量终止进程的原理与实践
linux·运维·chrome
MC皮蛋侠客10 小时前
C++17 多线程系列(五):C++17 并行算法——从串行到并行的零成本迁移
c++·多线程
Oll Correct10 小时前
实验二十九:TCP的运输连接管理
网络·笔记
日取其半万世不竭12 小时前
iftop、nethogs 和 nload:Linux 服务器网络流量实时监控工具介绍
linux·运维·服务器