Linux 应用层自定义协议与序列化

应⽤层

我们程序员写的⼀个个解决我们实际问题,满⾜我们⽇常需求的⽹络程序,都是在应⽤层

再谈"协议"

在 Linux 网络(乃至所有计算机网络)里,协议说白了就是通信双方提前商量好的「规则手册」------ 比如数据怎么发、发什么格式、出了问题怎么处理,都得按约定来,不然就像两个人说话鸡同鸭讲,根本没法沟通。

4层TCP/IP模型:

层级 核心作用 常见协议 生活类比
应用层 给应用程序提供特定服务,定义数据的格式和含义 HTTP、SSH、DNS、自定义协议(比如你的计算器) 信的内容(用中文写的 "你好")
传输层 负责端到端的连接和数据可靠性 TCP(可靠、有连接)、UDP(快速、无连接) 怎么把信安全送到(挂号信 vs 平信)
网络层 寻址和路由,找到目标设备 IP(IPv4/IPv6)、ICMP(ping) 信封上的地址(国家 - 城市 - 街道)
网络接口层 处理物理硬件(网卡、网线) ARP、以太网协议 送信的交通工具(货车、飞机)

我们之前写的 TCP/UDP 程序,只解决了「数据怎么传」的问题,但「传什么格式的数据、数据代表什么意思」,需要我们自己定规则 ------ 这就是我们的自定义应用层协议

网络版计算器

例如,我们需要实现⼀个服务器版的加法器.我们需要客⼾端把要计算的两个加数发过去,然后由服务器 进⾏计算,最后再把结果返回给客⼾端.

  1. 客户端发送计算式,如1+1,发送给服务器
  2. 服务器接受到计算式,在后台进行计算,然后发送给客户端

问题来了:客户端能直接把 "1+1" 这个字符串发给服务器吗?

答案是:不可以直接裸发

举个例子:假设我们要发送 1+1+1 这个式子给服务器。由于互联网传输的不确定性,数据可能会被拆分传输:第一次只传了 1+1,第二次才传 +1。服务器收到 1+1 后,不知道后面还有数据,就会直接当成完整式子计算,得到错误的结果。

解决方案:序列化与反序列化

因此,我们需要对数据进行一层封装,就像给快递套上 "包装盒":

这样,服务器就永远只会处理完整的计算式,不会再被 "半包 / 粘包" 问题干扰了。

序列化和反序列化

在网络传输中,序列化与反序列化就像是给数据做「标准化打包和拆包」:

  • 序列化:客户端发送数据前,把程序里的结构化数据(比如结构体、自定义对象),按约定格式转换成一串带边界标记的字节流,方便网络传输。
  • 反序列化:服务端收到数据后,再按照约定的规则,把这串字节流还原成程序能直接用的结构化数据。

用结构体理解序列化 / 反序列化

1. 先约定双方通用的通信协议

想要顺畅通信,两端必须提前约定好消息的结构(对应上图的struct/ 类class),这条聊天消息固定包含 3 个字段:

  • message:聊天正文内容,示例值:你好啊
  • time:消息发送时间,约定固定格式为20xx-yy-zz aabbcc
  • nickname:发送者昵称,示例值:新时代好青年两端都清楚每个字段的含义、排版规则,这就是通信的协议根基。
2. 发送端:序列化(多变一)

程序里messagetimenickname是相互独立的三段数据,没法直接高效走网络传输。序列化会把这三段内容按约定顺序拼接,整合为一整条连续长字符串:你好啊 20xx-yy-zz aabbcc 新时代好青年,实现信息由多变一。处理完成后发送方只需要把这条连续字节流交给网络,不用关心底层传输的分包细节。

3. 网络传输环节

整合好的长字节流在网络中传输,传输层只保障字节有序送达,不会区分原本的字段边界,所以必须依赖两端提前定好的协议,后续才能拆分还原内容。

4. 接收端:反序列化(一变多)

接收端拿到整条传输过来的长字符串后,按照提前约定的字段规则,把连续数据拆分还原成三段独立信息:

  • 提取message你好啊
  • 提取time20xx-yy-zz aabbcc
  • 提取nickname新时代好青年实现信息由一变多,上层聊天软件就可以正常展示、处理这条消息了,这个拆分还原的过程就是反序列化。

网络计算器实现

Socket封装

为了方便,我们专门把socket相关函数用C++封装成类Socket虚基类 :保留socket基本接口,如bind,listen,recv等,设计InitTcpServer和InitTcpClient接口,后续派生类可调用这两个接口自由创建服务器端和客户端

TcpSocket派生类 :根据虚基类实现具体的TCP的接口

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>
#include "com.hpp"
#include <memory>
 
class Socket
{
public:
    Socket()
    {}
    ~Socket()
    {}
    virtual void create_socket() = 0;
    virtual void Bind(uint16_t) = 0;
    virtual void Listen(int) = 0;
    virtual std::shared_ptr<Socket> Accept(InetAddr&) = 0;
    virtual bool Connect(const std::string&,const uint16_t&) = 0;
    virtual int Recv(std::string&) = 0;
    virtual void Send(const std::string&) = 0;
    virtual int get_sockfd() = 0;
    void InitTcpServer(uint16_t port = DEFAULT_PORT,int backlog = DEFAULT_BACKLOG)
    {
        create_socket();
        Bind(port);
        Listen(backlog);
    }
    void InitTcpClient(std::string ip = DEFAULT_IP,uint16_t port = DEFAULT_PORT)
    {
        create_socket();
        Connect(ip,port);
    }
};
 
class TcpSocket : public Socket
{
private:
    using func_t = std::function<void()>;
public:
    TcpSocket(int sockfd = DEFAULT_SOCKFD)
    :_sockfd(sockfd)
    {}
    void create_socket() override
    {
        _sockfd = socket(AF_INET,SOCK_STREAM,0);
        
        if(_sockfd < 0)
            exit(ExitCode::SOCKET);
    }
    void Bind(uint16_t port)override
    {
        InetAddr addr(port);
        int n = bind(_sockfd,CONV(addr.get_addr()),sizeof(addr.get_addr()));
        if(n < 0)
            exit(ExitCode::BIND);
    }
    void Listen(int backlog)override
    {
        int n = listen(_sockfd,backlog);
        if(n < 0)
            exit(ExitCode::LISTEN);
    }
    std::shared_ptr<Socket> Accept(InetAddr& addr)override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = accept(_sockfd,CONV(peer),&len);
        if(sockfd < 0)
            return nullptr;
        InetAddr tmp(peer);
        addr = tmp;
        std::shared_ptr<Socket> p = std::make_shared<TcpSocket>(sockfd);
        return p;
    }
    bool Connect(const std::string& ip,const uint16_t& port)override
    {
        InetAddr addr(ip,port);
        int n = connect(_sockfd,CONV(addr.get_addr()),sizeof(addr.get_addr()));
        if(n < 0)
            return false;
        return true;
    }
    int Recv(std::string& out)override
    {
        char buffer[MAXNUM];
        ssize_t n = recv(_sockfd,buffer,sizeof(buffer) - 1,0);
        if(n > 0)
        {
            buffer[n] = '\0';
            out += buffer;
            return n;
        }
        else if(n == 0)
            return 0;
        else
            return -1;
    }
    void Send(const std::string& out)override
    {
        send(_sockfd,out.c_str(),sizeof(out),0);
    }
    int get_sockfd()override
    {
        return _sockfd;
    }
private:
    int _sockfd;
};

定制协议

我们定制的协议:

1.结构化字段,提供好的序列和反序列化方案;

2.解决因为字节流问题,导致读取报文不完整的问题。

因此,一个完整的协议至少需要这两个类:

  • Request:发送端发送的请求数据
  • Response:接收端根据请求,进行处理后返回给发送端的数据

具体流程:

发送端->request序列化,发送给接收端->接收端接受request,反序列化->对request处理,制作response->response序列化,返回给发送端->发送端接受response,反序列化得到结果

Request:

cpp 复制代码
class Request
{
public:
    Request(int x = 0,char oper = ' ',int y = 0)
    :_x(x),_oper(oper),_y(y)
    {}
 
    void Serialize(std::string& out)//序列化:成员变量序列化成字符串
    {
    }
 
    bool Dserialize(std::string& out)//反序列化:字符串反序列化,提取成类中的成员变量
    }
    int get_x(){return _x;}
    char get_oper(){return _oper;}
    int get_y(){return _y;}
    std::string get_request_string()
    {
        return std::to_string(_x) + " " + _oper + " " + std::to_string(_y);
    }
private:
    int _x;
    char _oper;
    int _y;
};

Response:

cpp 复制代码
class Rseponse
{
public:
    Response(double ret = 0,int code = 0)
    :_result(ret),_code(code)
    {}
    Response(Response& rep)
    {}
    Response(Response&& rep)
    {}
    Response& operator=(Response&& rep)
    {}
    Response& operator=(Response& rep)
    {}
    void Serialize(std::string& out)//序列化
    {}
 
    bool Dserialize(std::string& out)//反序列化
    {}
    double get_ret()
    {
        return _result;
    }
    int get_code()
    {
        return _code;
    }
    std::string get_result_string()
    {
        return std::to_string(_result) + "[" +std::to_string(_code) + "]";
    }
private:
    double _result;//计算结果
    int _code;//计算状态码
};

TCP 是一个面向字节流 的协议,它本身不维护消息边界。当发送方连续发送多个小数据包,或接收方一次读取到多个包的数据时,就可能出现"粘包 "------即多个应用层消息粘在一起,无法区分哪里是结束、哪里是开始。解决粘包问题的核心是在应用层定义消息边界

一、发送端:打包流程(两步走)

  1. 业务序列化 :先把业务请求(如计算式 1+1)转成结构化的正文数据,比如 {1 + 1}
  2. 加协议边界(定界) :给正文加上「长度头 + 分隔符」,封装成可识别的完整报文:
    • 固定格式:正文长度\r\n正文内容\r\n
    • 例子:正文 {1 + 1} 共 7 个字节,最终报文为 7\r\n{1 + 1}\r\n
    • 作用:用长度头 + 首尾分隔符,给 TCP 字节流打上 "消息边界"。

二、接收端:解包流程(三步判断)

  1. 找分隔符 :先找报文开头的 \r\n,如果没找到,说明数据不完整,直接返回继续接收。
  2. 算总长度 :从分隔符前拿到正文长度,计算完整报文的总长度: 总长度 = 长度头长度 + 2个分隔符长度 + 正文长度 (例子中为 1 + 2*2 + 7 = 12 字节)
  3. 校验并处理
    • 收到的数据长度 < 总长度:包不完整,等待后续数据。
    • 收到的数据长度 ≥ 总长度:切出完整报文处理,剩余字节留到下一次解析。

这就是打包和解包的过程

我们再构建Protocol类,这个类用来:

  1. 对数据的打包和解包
  2. 接发送数据
  3. 处理数据(具体业务,需要上层指定)
cpp 复制代码
class Protocol
{
private:
    const std::string proto_sep = " ";
    const std::string line_sep = "\r\n";//分隔符
    using func_t = std::function<Reponse(Request&)>;//业务处理函数,上层指定
public:
    Protocol()
    {}
    Protocol(func_t func)
    :_func(func)
    {}
    void Encode(std::string& message)//打包
    {
        message = std::to_string(message.size()) + line_sep + message + line_sep; 
    }
    
    bool Decode(std::string& package,std::string& message)//解包
    {
        int pos = package.find(line_sep);
        if(pos == std::string::npos)
            return false;
        std::string num = package.substr(0,pos);
        int total = std::stoi(num) + num.size() + 2 * line_sep.size();
        if(package.size() < total)
            return false;
        message = package.substr(pos + line_sep.size(),std::stoi(num));
        package.erase(0,total);
        return true;
    }
 
    bool get_request(std::shared_ptr<Socket>& sock,Request& req)//接受request,进行1.解包2.反序列化
    {
        std::string package;
        while(1)
        {
            int n = sock->Recv(package);
            if(n > 0)
            {
                std::string message;
                if(!Decode(package,message))
                    continue;
                req.Dserialize(message);
                return true;
            }
            else if(n == 0)
                exit(ExitCode::NORMAL);
            else
                return false;
        }
    }
    bool get_reponse(std::shared_ptr<Socket>& sock,Reponse& rep)//接受response,进行1.解包2.反序列化
    {
        std::string package;
        while(true)
        {
            int n = sock->Recv(package);
            if(n > 0)
            {
                std::string message;
                if(!Decode(package,message))
                    continue;
                rep.Dserialize(message);
                return true;
            }
            else if(n == 0)
                return false;
            else
                return false;
        }
    }
    Reponse handle(Request& req)//数据处理(接受端的任务)
    {
        return _func(req);
    }
private:
    func_t _func;
};

Jsoncpp

Jsoncpp是⼀个⽤于处理JSON数据的C++库。它提供了将JSON数据序列化 为字符串以及从字符串 反序列化为C++数据结构的功能。

Jsoncpp是开源的,⼴泛⽤于各种需要处理JSON数据的C++项⽬ 中。

特性

  1. 简单易⽤:Jsoncpp提供了直观的API,使得处理JSON数据变得简单。

  2. ⾼性能:Jsoncpp的性能经过优化,能够⾼效地处理⼤量JSON数据。

  3. 全⾯⽀持:⽀持JSON标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和null。

  4. 错误处理:在解析JSON数据时,Jsoncpp提供了详细的错误信息和位置,⽅便开发者调试。

安装

bash 复制代码
ubuntu:sudo apt-get install libjsoncpp-dev
Centos:sudo yum installjsoncpp-devel

检查头文件是否安装

bash 复制代码
ls /usr/include/jsoncpp/json/

能看到 json.hvalue.h 等文件,说明头文件已经装好了。

序列化

使用Json::ValueJson::FastWriter快速上手

cpp 复制代码
#include <jsoncpp/json/json.h>
#include <iostream>
#include <jsoncpp/json/value.h>
 
int main() 
{
    Json::Value json;
    json["x"] = 1;
    json["oper"] = '+';
    json["y"] = 2;
    Json::FastWriter w;
    std::string out = w.write(json);
    std::cout<<out;
    return 0;
}

反序列化

使用Json::ValueJson::Reader快速上手

cpp 复制代码
#include <jsoncpp/json/json.h>
#include <iostream>
#include <jsoncpp/json/reader.h>
#include <jsoncpp/json/value.h>
 
int main() 
{
    Json::Value json;
    json["x"] = 1;
    json["oper"] = '+';
    json["y"] = 2;
    Json::FastWriter w;
    std::string out = w.write(json);
    std::cout<<out;
    Json::Reader r;
    bool res = r.parse(out,json);
    if(res)
    {
        int x = json["x"].asInt();
        char oper = json["oper"].asInt();
        int y = json["y"].asInt();
        std::cout<<x<<" "<<oper<<" "<<y<<std::endl;
    }
    return 0;
}

Request和Response实现

cpp 复制代码
class Request
{
public:
    Request(int x = 0,char oper = ' ',int y = 0)
    :_x(x),_oper(oper),_y(y)
    {}
 
    void Serialize(std::string& out)
    {
        Json::Value json;
        json["x"] = _x;
        json["oper"] = _oper;
        json["y"] = _y;
        Json::FastWriter w;
        out = w.write(json);
    }
 
    bool Dserialize(std::string& out)
    {
        Json::Value json;
        Json::Reader r;
        bool res = r.parse(out,json);
        if(res)
        {
            _x = json["x"].asInt();
            _oper = json["oper"].asInt();
            _y = json["y"].asInt();
        }
        return res;
    }
    int get_x(){return _x;}
    char get_oper(){return _oper;}
    int get_y(){return _y;}
    std::string get_request_string()
    {
        return std::to_string(_x) + " " + _oper + " " + std::to_string(_y);
    }
private:
    int _x;
    char _oper;
    int _y;
};
 
class Reponse
{
public:
    Reponse(double ret = 0,int code = 0)
    :_result(ret),_code(code)
    {}
    Reponse(Reponse& rep)
    {
        _result = rep.get_ret();
        _code = rep.get_code();
    }
    Reponse(Reponse&& rep)
    {
        _result = rep.get_ret();
        _code = rep.get_code();
    }
    Reponse& operator=(Reponse&& rep)
    {
        _result = rep.get_ret();
        _code = rep.get_code();
        return *this;
    }
    Reponse& operator=(Reponse& rep)
    {
        _result = rep.get_ret();
        _code = rep.get_code();
        return *this;
 
    }
    void Serialize(std::string& out)
    {
        Json::Value json;
        json["result"] = _result;
        json["code"] = _code;
        Json::FastWriter w;
        out = w.write(json);
    }
 
    bool Dserialize(std::string& out)
    {
        Json::Value json;
        Json::Reader r;
        bool res = r.parse(out,json);
        if(res)
        {
            _result = json["result"].asDouble();
            _code = json["code"].asInt();
        }
        return res;
    }
    double get_ret()
    {
        return _result;
    }
    int get_code()
    {
        return _code;
    }
    std::string get_result_string()
    {
        return std::to_string(_result) + "[" +std::to_string(_code) + "]";
    }
private:
    double _result;
    int _code;
};

jsoncpp总结

• toStyledString 、StreamWriter 和 FastWriter 提供了不同的序列化选项,你可以根 据具体需求选择使⽤。

• Json::Reader 和 parseFromStream 函数是Jsoncpp中主要的反序列化⼯具,它们提供了强 ⼤的错误处理机制。

• 在进⾏序列化和反序列化时,请确保处理所有可能的错误情况,并验证输⼊和输出的有效性。

计算器设计

自定义协议能够让我们成功接收到"{1 + 1}"这个字符串,并对这个字符串反序列化成了Request,但这个Request怎么处理?

我们需要上层自定义。而在这里,我们要对这个Request中的数据进行普通的加减乘除的运算,因此我们需要设计计算器

这个计算器需要:

1.接受Request参数

2.对Request中的数据进行处理

3.对处理后数据做成Response

cpp 复制代码
class cal
{
public:
    Reponse func(Request &req)
    {
        auto x = req.get_x();
        auto y = req.get_y();
        char oper = req.get_oper();
        switch (oper)
        {
        case '+':
            return Reponse(x + y, 0);
        case '-':
            return Reponse(x - y, 0);
        case '*':
            return Reponse(x * y, 0);
        case '/':
            if (y == 0)
                return Reponse(0, 1);
            else
                return Reponse(x / y, 0);
        case '%':
            return Reponse(x % y, 0);
        default:
            return Reponse(0, 2);
        }
    }
};

服务器设计

我们作为开发者,期望设计一个TcpServer,这个TcpServer内部包含TcpSocket和业务处理函数;TcpSocket由我们自己提供,而业务处理函数需要上层进行提供

cpp 复制代码
#pragma once
 
#include "socket.hpp"
 
class TcpServer : public nocopy
{
private:
    using func_t = std::function<void(std::shared_ptr<Socket>&,InetAddr&)>;//自定义处理函数
public:
    TcpServer(uint16_t port,func_t func)
    :_port(port),_ioserver(func),_listensock(std::make_shared<TcpSocket>())
    {}
    void init(int backlog = DEFAULT_BACKLOG)
    {
        _listensock->InitTcpServer(_port,backlog);
        _listensockfd = _listensock->get_sockfd();
    }
    void run()
    {
        _isrunning = true;
        while(_isrunning)
        {
            InetAddr addr;
            std::shared_ptr<Socket> sock = _listensock->Accept(addr);
            if(sock == nullptr)
                continue;
            pid_t pid = fork();
            if(pid > 0)
            {
                close(sock->get_sockfd());
                waitpid(pid,nullptr,0);
            }
            else if(pid == 0)
            {
                if(fork() > 0)
                    exit(ExitCode::NORMAL);
                close(_listensock->get_sockfd());
                _ioserver(sock,addr);//把数据全盘交给用户的业务处理函数
            }
            else
                exit(ExitCode::FORK);
        }
    }
 
private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning = false;
    std::shared_ptr<Socket> _listensock;
    func_t _ioserver;
};

上层使用

  • 底层:提供TcpServer,用来接受数据。但这个数据接受后,我该如何处理?看上层的选择
  • 上层:提供自定义协议,在TcpServer接受到数据后进行个性化处理
cpp 复制代码
#include "NetCal.hpp"
#include "protocol.hpp"
#include "TcpServer.hpp"
#include "com.hpp"
 
int main(int argv, char *argc[])
{
    if (argv != 2)
        exit(ExitCode::FORMAT);
    std::shared_ptr<cal> netcal = std::make_shared<cal>();
    std::shared_ptr<Protocol> prot = std::make_shared<Protocol>([&netcal](Request &req) -> Reponse
                                                                { return netcal->func(req); });
    TcpServer server(atoi(argc[1]), [&prot](std::shared_ptr<Socket> &server, InetAddr &addr)
    {//上层自定义处理函数
        while(1)
        {
            Request req;
            prot->get_request(server,req);
            std::cout<<"request : "<<req.get_request_string()<<std::endl;
            Reponse rep = prot->handle(req);
            std::cout<<"reponse : "<<rep.get_result_string()<<std::endl;
            std::string message;
            rep.Serialize(message);
            prot->Encode(message);
            server->Send(message);
        } 
    }
    );
    server.init();
    server.run();
}