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

文章目录


前言

本章要实现一个自定义协议


一、序列化和反序列化

什么是序列化和反序列化

  • 序列化:把内存的结构体,对象转换成字节流或通用文本(比如JSON)
  • 反序列化:把收到的字节或者文本还原回程序直接用的结构体或对象

为什么要有序列化和反序列化

  • 内存中的结构体、对象是带类型、带内存布局的,不能直接在网络传输或存储,因为 TCP 只认字节流,不认结构体,所以必须用序列化把结构转成字节流,再用反序列化还原;所以必须转换成统一的字节或字符串格式,才能被网络和文件识别。

序列化和反序列化的作用

  • 让数据可以跨机器、跨语言、跨架构传输
  • 解决大小端、内存对齐、结构差异问题
  • 保证数据能稳定存储和兼容升级

序列化和反序列化解决了哪些问题

  • 解决网络传输问题TCP 只认字节流,不认结构体 / 对象,序列化把结构化数据转成字节流才能传输。
  • 解决跨平台、跨架构问题屏蔽大小端、内存对齐、32/64 位差异,不同机器、系统解析结果一致。
  • 解决跨语言互通问题把各语言的对象 / 结构体转成通用格式(JSON、Protobuf 等),C++、Java、Go 可互相通信。
  • 解决数据持久化问题让内存数据能存入文件、数据库,程序重启后仍可恢复。
  • 解决版本兼容问题结构增减字段时,不会直接解析失败,保证新旧程序兼容。

二、自定义协议(实现一个网络版本的计算器)

应用层和协议

再次之前我们先来了解一下什么是应用层和协议。

  • 应用层 :我们程序员写的解决我们实际问题,满足我们日常需求的网络程序,都在应用层。简单来说就是我们写的代码就是在这一层。
  • 协议 :简单来说协议就是一种"约定",它的本质就是双方约定好的结构化数据
  • 应用层协议: 应用之间约定好的数据格式与通信规则,用来看懂传输的字节流。。底层只管发字节,协议负责让双方能看懂这些字节。

理解read、write、recv、send和tcp为什么⽀持全双

通过上图可以看出,在每一台主机上都会有一个发送缓冲区接收缓冲区 ,发送缓冲区在发数据的同时,接收缓冲区也可以接收来自其他主机的数据。这就是全双工

  • 数据是怎样发送的
    首先数据会在应用层序列化成字符串,然后在通过系统调用write,send把字符串拷贝到发送缓冲区,所以write和send并不是把数据直接发送给目标主机,而是拷贝到当前主机的发送缓冲区,将来实际数据什么时候发,发多少,出错了怎么办,上层根本不用关心,因为这是TCP控制决定的,所以TCP也叫传输控制协议。当数据被目标主机的接受缓冲区拿到时,上层通过系统调用read,recv把数据拷贝到上层,然后在通过反序列化就能拿到数据了,所以read和recv的本质也是拷贝。
  • read和recv存在的问题
    我在之前TCP网络通信编程哪里标注了read和recv是存在bug的。今天就来解释一下为什么会存在bug。原因就是read和recv只负责有数据就拷贝数据到上层,但是不一定能保证拷贝一个完整的报文到上层,保证不了报文的完整性。它可能只读到半个报文,也可能读到多个报文。就像音译汉的那个功能,当客户端给服务器发送一个单词,服务器在read的时候可能只读到了半个单词,也可能读到完整单词。像这样在TCP中读取到的报文不完整,或者多读了导致下一个报文不完整了,我们管这个问题叫作 "粘包" 问题。那谁来保证读取报文的完整性呢?由应用层程序员自己保证,所以就有了自定义应用层协议

三、实现一个网络版本的计算器

程序设计思路

由于实现这个功能所需的代码量比较大,所以我想设计出:

  • 职责清晰:谁干什么非常明确;
  • 可替换:可以换计算逻辑、换协议、换网络模型
  • 可扩展:加功能不用改原来代码
  • 可维护:代码不乱
  • 可测试:业务逻辑可以单独测试

设计的整体结构:五层架构+回调解耦

  • 第一层:守护进程层(Daemon)

实现目的:让程序后台运行,脱离终端,切换根目录,重定向输入输出,忽略异常信号,保证服务长期稳定运行

  • 第二层:入口层(main),作用是组装所有模块

具体实现:创建计算器,创建协议(绑定计算器),创建服务器(绑定协议),启动服务器

  • 第三层:网络层(TcpServer +Socket),作用是处理连接,收发数据

具体实现:Socket:封装底层接口,TcpServer:多进程接受连接,不关心计算,不关心协议格式,收到连接 直接丢给协议层处理

  • 第四层:协议层(Protocol),作用是打包,解包和序列化

具体实现:从网络读取数据,按照自定义协议解包,调用 业务层(Cal) 计算,再把结果打包发回去

  • 第五层:业务层(Cal),作用是只做计算

具体实现:只做计算,不知道网络和协议,只输入 Request和输出 Response

代码部分

要实现这一功能所需要9个文件,大概700多行代码,分别是:

  • 守护进程层:Daemon。hpp(进程守护化)

  • 网络层TcpServer.cc(服务端),TcpServer.hpp(客服端开源文件 ),TcpClient.cc(客户端),Socket.hpp(封装tcp底层接口)。InetAddr.hpp(对端口号和ip的封装),Common.hpp(错误吗设置和禁止拷贝功能),Makefile(自动编译文件)

  • 协议层:Protocol.hpp(自定义协议)

  • 业务层:NetCal.hpp(实现计算文件)

守护进程层

Daemon。hpp(进程守护化)
cpp 复制代码
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Common.hpp"

// 将服务进程守护

const static std::string dev = "/dev/null";
void Daemon(int nochdir, int noclose)
{
    // 忽略IO/子进程退出等相关的信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);

    // 父进程退出,让子进程来创建新的会话
    if (fork() > 0)
        exit(0);

    setsid(); // 让子进程成为一个独立的会话

    if (nochdir == 0)
        chdir("/"); // 更改目录

    // 守护进程,不需要键盘输入,显示器输出
    // 1.关闭文件描述0,1,2,但是不推荐。
    // 2.打开/dev/null文件,重定向标准输入,标准输出,标准错误到/dev/null文件中,
    // 向文件/dev/null输入,他会默认全部都是舍弃的,如果要读它,那读到的都是空
    if (noclose == 0)
    {
        int fd = ::open(dev.c_str(), O_RDWR);
        if (fd < 0)
        {
            // 守护进程化失败
            exit(OPEN_ERR);
        }
        // 守护进程化成功,把fd拷贝给0,1,2
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

网络层

TcpServer.cc(服务端)
cpp 复制代码
#include <iostream>
#include <memory>
#include "NetCal.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"
using namespace SocketModule;
using namespace std;

void Usage(std::string line)
{
    std::cout << line << " port " << std::endl;
}
int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port = std::stoi(argv[1]);

    //进程守护化
    Daemon(0,0);//这里也可以用系统提供的接口,这里为了方便理解我就把代码自己实现一遍
    
    // 业务层
    std::unique_ptr<Cal> cal = std::make_unique<Cal>();

    // 协议层
    std::unique_ptr<Protocol> ptr = std::make_unique<Protocol>([&cal](Request &req) -> Response
                                                               { return cal->Execute(req); });

    // 网络层
    std::unique_ptr<TcpServer> stvr = std::make_unique<TcpServer>(port, [&ptr](std::shared_ptr<Socket> &sock, InetAddr &client)
                                                                  { ptr->GetRequest(sock, client); });

    // std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    stvr->Run();

    return 0;
}
TcpServer.hpp(客服端开源文件 )
cpp 复制代码
#include <iostream>
#include "Socket.hpp"
#include <memory>
#include <sys/wait.h>
#include <functional>

using namespace SocketModule;

using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
class TcpServer
{
public:
    TcpServer(uint16_t port, ioservice_t service)
        : _port(port), _ListenSockptr(std::make_unique<TcpSocket>()), _isrunning(false), _service(service)
    {
        _ListenSockptr->BuildListenSocket(_port); // 创建listen套接字
    };

    void Run()
    {
        while (true)
        {
            _isrunning = true;

            InetAddr client;
            auto sock = _ListenSockptr->Accept(&client);
            if (sock == nullptr)
            {
                std::cout << "accept error" << std::endl;
                continue;
            }
            std::cout << "accept seccuss......." << client.StringAddr() << std::endl;

            // 多进程版本实现
            pid_t id = fork();
            if (id < 0)
            {
                std::cout << "fork error" << std::endl;
                exit(FORK_ERR);
            }
            else if (id == 0) // 子进程
            {
                _ListenSockptr->Close(); // 关闭不需要的文件描述符
                if (fork() > 0)
                    exit(OK);

                // 提供服务
                _service(sock, client);
                exit(OK);
            }
            else // 父进程
            {
                sock->Close(); // 关闭_sockfd
                pid_t n = waitpid(id, nullptr, 0);
            }
        }
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _ListenSockptr;
    bool _isrunning;
    ioservice_t _service;
};
TcpClient.cc(客户端)
cpp 复制代码
#include <iostream>
#include <memory>
#include "Socket.hpp"
#include "Common.hpp"
#include "Protocol.hpp"

using namespace SocketModule;
void usage(std::string client)
{
    std::cout << client << " ip port" << std::endl;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();

    client->BuildTcpClientSocketMethod(); // 创建套接字

    if (client->Connect(server_ip, server_port) != 0)
    {
        // 连接失败
        std::cout << "connect error" << std::endl;
        exit(CONNECT_ERR);
    }

    // 构建协议对象
    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
    std::string resp_buffer;

    // 连接服务器成功
    while (true)
    {
        int x, y;
        char oper;
        std::cout << "Please Enter x # ";
        std::cin >> x;
        std::cout << "Please Enter y # ";
        std::cin >> y;
        std::cout << "Please Enter oper # ";
        std::cin >> oper;

        // 构建一个请求->可以直接发送的字符串
        std::string req_str = protocol->BuildRequestString(x, y, oper);
        std::cout << "-----------encode req string-------------" << std::endl;
        std::cout << req_str << std ::endl;
        std::cout << "-----------------------------------------" << std::endl;

        // 发送请求
        client->Send(req_str);

        // 获取应答
        Response resp;
        bool res = protocol->GetResponse(client, resp_buffer, &resp);
        if (res == false)
        {
            break;
        }
        // 显示结果
        resp.ShowResult();
    }
    client->Close();
    return 0;
}
Socket.hpp(封装tcp底层接口)
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <unistd.h>
#include "Common.hpp"
#include "InetAddr.hpp"

// 以模版的方式封装socket套接字
namespace SocketModule
{
    // const static int backlog = 253;
    //  基类Socket,包含大部分方法,但都是纯虚方法
    class Socket : public NoCopy
    {
    public:
        virtual ~Socket() {}
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t port) = 0;
        virtual void ListenOrDie() = 0;
        virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual void Close() = 0;
        virtual int Recv(std::string *out) = 0;
        virtual int Send(std::string &out) = 0;
        virtual int Connect(const std::string &server_ip, uint16_t port) = 0;

        void
        BuildListenSocket(uint16_t port) // 创建一个tcp套接字
        {
            SocketOrDie();
            BindOrDie(port);
            ListenOrDie();
        }
        void BuildTcpClientSocketMethod()
        {
            SocketOrDie();
        }

    private:
    };
    const static int defaultfd = -1;
    class TcpSocket : public Socket
    {
    public:
        TcpSocket() : _sockfd(defaultfd) {}

        TcpSocket(int fd) : _sockfd(fd)
        {
        }

        void SocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                std::cout << "socket error" << std::endl;
                exit(SOCK_ERR);
            }
            std::cout << "socket success" << std::endl;
        }

        void BindOrDie(uint16_t port) override
        {
            InetAddr peer(port);
            int n = ::bind(_sockfd, peer.NetAddrPtr(), peer.InetAddrLen());
            if (n < 0)
            {
                std::cout << "bind error" << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind success" << std::endl;
        }

        void ListenOrDie() override
        {
            int n = ::listen(_sockfd, backlog);
            {
                if (n < 0)
                {
                    std::cout << "listen error" << std::endl;
                    exit(LISTEN_ERR);
                }
                std::cout << "listen success" << std::endl;
            }
        }

        std::shared_ptr<Socket> Accept(InetAddr *client) override // 这样返回的目的是为了让普通的文件描述符也能调用其他方法
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = ::accept(_sockfd, CONV(peer), &len);
            if (fd < 0)
            {
                std::cout << "listen error" << std::endl;
                return nullptr;
            }

            client->SetAddr(peer);

            return std::make_shared<TcpSocket>(fd);
        }

        int Connect(const std::string &server_ip, uint16_t port)
        {
            InetAddr server(server_ip, port);
            return ::connect(_sockfd, server.NetAddrPtr(), server.InetAddrLen());
        }

        int Recv(std::string *out) override
        {
            char buffer[1024];
            ssize_t n = recv(_sockfd, buffer, sizeof(buffer) - 0, 0);
            if (n > 0)
            {
                buffer[n] = 0;
                *out += buffer;
            }
            return n;
        }

        int Send(std::string &out) override
        {
            return send(_sockfd, out.c_str(), out.size(), 0);
        }
        void Close() override
        {
            if (_sockfd >= 0)
            {
                ::close(_sockfd);
            }
        }
        ~TcpSocket() {}

    private:
        int _sockfd; // 这里可以是listencosket 也可能是普通套接字
        int backlog = 253;
    };
}
InetAddr.hpp(对端口号和ip的封装)**
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
class InetAddr
{
public:
    InetAddr() {}
    InetAddr(const struct sockaddr_in &addr) // 网络格式转换成本地格式
        : _addr(addr)
    {
        SetAddr(_addr);
    }

    InetAddr(uint16_t port)
        : _port(port)
    {

        bzero(&_addr, sizeof(_addr)); // 清空sockaddr_in
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port); // 本地格式转化成网络格式
        _addr.sin_addr.s_addr = INADDR_ANY;
    }
    InetAddr(std::string ip, uint16_t port)
        : _ip(ip), _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }
    std::string StringAddr() const
    {
        return _ip + ":" + std::to_string(_port);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }

    // 返回port和ip
    uint16_t port() { return _port; }
    std::string ip() { return _ip; }

    const struct sockaddr_in &NetAddr() const { return _addr; }

    const struct sockaddr *NetAddrPtr() { return CONV(_addr); }

    socklen_t InetAddrLen() { return sizeof(_addr); }

    void SetAddr(struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        //_ip = inet_ntoa(_addr.sin_addr); // 四字节网络ip风格转换成点分十进制
        char ipbuffer[64];
        inet_ntop(AF_INET, &addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }

    ~InetAddr() {}

private:
    struct sockaddr_in _addr;
    uint16_t _port;
    std::string _ip;
};
Common.hpp(错误吗设置和禁止拷贝功能)
cpp 复制代码
#pragma once
#include <iostream>
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

enum ExitCode
{
    OK = 0,
    USAGE_ERR,
    SOCK_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    WRITE_ERR,
    FORK_ERR,
    ACCEPT_ERR,
    OPEN_ERR
};

class NoCopy
{
public:
    NoCopy() {}
    NoCopy(const NoCopy &) = delete;
    const NoCopy &operator=(const NoCopy &) = delete;
    ~NoCopy() {}

private:
};

#define CONV(addr) ((struct sockaddr *)&addr)
Makefile(自动编译文件)
bash 复制代码
.PHONY:all
all:tcpclient tcpserver

tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++17 -ljsoncpp
tcpserver:TcpServer.cc
	g++ -o $@ $^ -std=c++17  -ljsoncpp

.PHONY:clean
clean:
	rm -f tcpclient tcpserver 

协议层

Protocol.hpp(自定义协议)
cpp 复制代码
#pragma once
#include <string>
#include <iostream>
#include <memory>
#include "Socket.hpp"
#include <jsoncpp/json/json.h>
#include <functional>
using namespace SocketModule;
// 实现一个网络版本的计算器

// client -->server
// 50\r\n{"x": 10 "y": 20 "oper": '+'}\r\n
class Request
{
public:
    Request() {}
    Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
    {
    }
    // 序列化
    std::string Serialize()
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;//里存的其实是ASCCL码值
        Json::FastWriter writer;
        std::string s = writer.write(root);
        return s;
    }
    // 反序列化
    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in, root);
        if (ok)
        {
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();
        }
        return ok;
    }

    ~Request() {}

    int X() { return _x; }
    int Y() { return _y; }
    char oper() { return _oper; }

private:
    int _x;
    int _y;
    char _oper; //+,-,*,/
};

// server -->client
class Response
{
public:
    Response() {}

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

    std::string Serialize() // 序列化
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter writer;
        return writer.write(root);
    }
    bool Deserialize(std::string &in) // 反序列化
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in, root);
        if (ok)
        {
            _result = root["result"].asInt();
            _code = root["code"].asInt();
        }
        return ok;
    }

    void SetResult(int res)
    {
        _result = res;
    }
    void SetCode(int res)
    {
        _code = res;
    }
    void ShowResult()
    {
        std::cout << "计算的结果是:" << _result << "[" << _code << "]" << std::endl;
    }
    ~Response() {}

private:
    int _result; // 运算结果
    int _code;   // 0:success,1,2,3...不同的运算异常情况
};

using func_t = std::function<Response(Request &req)>;
const std::string sep = "\r\n";

class Protocol
{
public:
    Protocol() {}
    Protocol(func_t func) : _func(func) {}

    std::string Encode(const std::string jsonstr) // 添加报文长度
    {
        // 50\r\n{"x" : 10 "y" : 20 "oper" : '+'}\r\n
        std::string len = std::to_string(jsonstr.size());
        return len + sep + jsonstr + sep;
    }

    // 判断报文的完整性,如果包含一个完整的报文,那就提取它,并从移除它,方便处理下一个报文
    bool Decode(std::string &buffer, std::string *package)
    {
        ssize_t pos = buffer.find(sep);
        if (pos == std::string::npos)
        {
            return false;
        }
        std::string package_len_str = buffer.substr(0, pos);
        int package_len_int = std::stoi(package_len_str); // 拿到报文的长度

        // 到这里说buffer中一定有数据,但不一定是完整的报文
        int target_len = package_len_str.size() + package_len_int + 2 * sep.size(); // 这个表示一个完整的报文的长度
        if (buffer.size() < target_len)                                             // 判断是否是一个完整报文
        {
            return false;
        }
        // 到这里已经确定至少有一个完整的报文
        *package = buffer.substr(pos + sep.size(), package_len_int); // 提取完整的报文
        buffer.erase(0, target_len);                                 // 移除完整报文,方便处理下一个报文
        return true;
    }

    void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {
        // 读取
        std::string buffer_unique;
        while (true)
        {
            int n = sock->Recv(&buffer_unique); // 读取数据,只负责读,不处理数据
            if (n > 0)
            {
                // 处理数据
                std::cout << "-----------------request_buffer---------------" << std::endl;
                std::cout << buffer_unique << std::endl;
                std::cout << "-------------------------------------------" << std::endl;
                std::string json_package;
                // 1. 解析报文,提取完整的json请求,如果不完整整,那就继续读取
                while (Decode(buffer_unique, &json_package)) // 解决报文多的情况
                {
                    // 到这里就能保证一定拿到了一个完整的报文
                    std::cout << "-----------------request_json---------------" << std::endl;
                    std::cout << json_package << std::endl;
                    std::cout << "-------------------------------------------" << std::endl;
                    std::cout << "-----------------request_buffer---------------" << std::endl;
                    std::cout << buffer_unique << std::endl;
                    std::cout << "-------------------------------------------" << std::endl;

                    // 2.请求json串,反序列化
                    Request req;
                    bool ok = req.Deserialize(json_package);
                    if (!ok)
                        continue;
                    // 3.这里一定得到一个内部属性已经被设置的req

                    // 完成计算功能
                    Response resp = _func(req);

                    // 4.序列化
                    std::string json_str = resp.Serialize();

                    // 5.添加自定长度
                    std::string sen_str = Encode(json_str);

                    // 6.直接发送
                    sock->Send(sen_str);
                }
            }
            else if (n == 0)
            {
                std::cout << client.StringAddr() << "QUIT" << std::endl;
                break;
            }
            else
            {
                std::cout << client.StringAddr() << "读取异常" << std::endl;
                break;
            }
        }
    }

    bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buffer, Response *resp)
    {

        while (true)
        {
            int n = client->Recv(&resp_buffer);
            if (n > 0)
            {
                std::cout << "-----------------resp_buffer---------------" << std::endl;
                std::cout << resp_buffer << std::endl;
                std::cout << "-------------------------------------------" << std::endl;
                std::string json_package;
                // 1. 解析报文,提取完整的json请求,如果不完整,那就继续读取
                bool ret = Decode(resp_buffer, &json_package);
                if (!ret)
                    continue;
                std::cout << "-----------------response json---------------" << std::endl;
                std::cout << json_package << std::endl;
                std::cout << "-----------------------------------------------" << std::endl;

                std::cout << "-----------------resp_buffer---------------" << std::endl;
                std::cout << resp_buffer << std::endl;
                std::cout << "-------------------------------------------" << std::endl;
                // 这里一定能拿到一个完整的报文,然后反序列化

                return resp->Deserialize(json_package);
            }
            else if (n == 0)
            {
                std::cout << " SERVER QUIT" << std::endl;
                break;
            }
            else
            {
                std::cout << "读取异常" << std::endl;
                break;
            }
        }
        return false;
    }
    std::string BuildRequestString(int x, int y, char oper)
    {
        // 构建一个完整的请求
        Request req(x, y, oper);

        // 序列化
        std::string json_req = req.Serialize();
        std::cout << "-----------------json_req string---------------" << std::endl;
        std::cout << json_req << std::endl;
        std::cout << "-----------------------------------------------" << std::endl;

        // 添加长度报头
        return Encode(json_req);
    }

    ~Protocol() {}

private:
    Request req;
    Response resp;
    func_t _func;
};

业务层

NetCal.hpp(实现计算文件)
cpp 复制代码
#pragma once
#include "Protocol.hpp"
class Cal
{
public:
    Response Execute(Request &req)
    {
        Response resp(0, 0);
        switch (req.oper())
        {
        case '+':
            resp.SetResult(req.X() + req.Y());
            break;
        case '-':
            resp.SetResult(req.X() - req.Y());
            break;
        case '*':
            resp.SetResult(req.X() * req.Y());
            break;
        case '/':
        {
            if (req.Y() == 0)
            {
                resp.SetCode(1); // 除0操作不被允许
            }
            else
            {
                resp.SetResult(req.X() / req.Y());
            }
        }
        break;
        case '%':
        {
            if (req.Y() == 0)
            {
                resp.SetCode(2); // %0操作不被允许
            }
            else
            {
                resp.SetResult(req.X() % req.Y());
            }
        }
        break;
        default:
            resp.SetCode(3); // 其他非法运算
            break;
        }
        return resp;
    }
};

四、 守护进程的补充

  • 什么是守护进程

守护进程就是运行在后台、不依赖任何终端、开机能自启、长期默默运行的程序。

  • 为什么要有守护进程

当关闭终端、SSH 断开连接、按 Ctrl+C终止程序时、程序就直接死掉。但服务器程序(如 Nginx、MySQL、你的后端服务)不能死,要一直跑。所以需要一种特殊的进程,不受终端影响、后台运行、系统级服务,这就是守护进程。

  • 守护进程的特点

运行在后台不占用终端,不输出乱七八糟的日志。脱离终端(会话)终端关了,它还在跑。父进程是 init 或 systemd不会变成僵尸进程(本质它就是一个孤儿进程)。长期运行从开机跑到关机。

  • 守护进程是怎么做到 "关终端不死" 的

核心就三步:
fork 子进程 ,父进程退出让子进程变成孤儿,被 init 收养。
setsid 创建新会话彻底脱离原来的终端 ,成为会话组长。→ 从此和终端没关系了。
忽略信号重定向输入输出到 /dev/null不接收键盘输入,不往屏幕打印东西。

  • 守护进程的代码展示

一般系统会给我们提供一个接口:int daemon(int nochdir, int noclose);这个就是直接把进程守护化,把的底层跟下面差不多。它的两个参数分别代表:nochdir(是否切换目录)nochdir = 0→ 把当前目录切换为 /(根目录)nochdir = 1→ 不切换目录,保持原来的目录;noclose(是否关闭文件描述符)noclose = 0→ 关闭 0、1、2(标准输入、输出、错误)→ 重定向到 /dev/null(空设备,不输出内容)。noclose = 1→ 不关闭,保持原来的输入输出

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Common.hpp"

// 将服务进程守护

const static std::string dev = "/dev/null";
void Daemon(int nochdir, int noclose)
{
    // 忽略IO/子进程退出等相关的信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);

    // 父进程退出,让子进程来创建新的会话
    if (fork() > 0)
        exit(0);

    setsid(); // 让子进程成为一个独立的会话

    if (nochdir == 0)
        chdir("/"); // 更改目录

    // 守护进程,不需要键盘输入,显示器输出
    // 1.关闭文件描述0,1,2,但是不推荐。
    // 2.打开/dev/null文件,重定向标准输入,标准输出,标准错误到/dev/null文件中,
    // 向文件/dev/null输入,他会默认全部都是舍弃的,如果要读它,那读到的都是空
    if (noclose == 0)
    {
        int fd = ::open(dev.c_str(), O_RDWR);
        if (fd < 0)
        {
            // 守护进程化失败
            exit(OPEN_ERR);
        }
        // 守护进程化成功,把fd拷贝给0,1,2
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

五、Jsoncpp的介绍的使用

我们在写序列化和反序列化的时候有两种选择,一是直接完成序列化和反序列化,但是这样就大大提高的书写成本,降低效率。而是我们直接用别人写好的,直接使用,那就是jsoncpp

Jsoncpp 简介

Jsoncpp 是一款轻量、高效、跨平台的 C++ JSON 处理库,广泛用于网络通信、配置文件解析、数据交互等场景。它支持 JSON 标准的全部数据类型:对象、数组、字符串、数字、布尔、null,并提供完善的序列化、反序列化与错误处理能力。

特点

  • 接口简洁易用,学习成本低
  • 性能优秀,适合大量 JSON 数据处理
  • 支持格式化输出与紧凑输出
  • 解析失败时提供详细错误信息
  • 开源稳定,被大量 C++ 项目采用

安装

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

//头文件
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

示例

序列化(C++ 对象 → JSON 字符串)

序列化指的是将数据结构或对象转换为⼀种格式,以便在⽹络上传输或存储到⽂件中。Jsoncpp提供

了多种⽅式进⾏序列化:

  1. 使用Json::Value 的 toStyledString ⽅法:
    优点:将 Json::Value 对象直接转换为格式化的JSON字符串。

示例

cpp 复制代码
int main() {
    Json::Value root;

    root["name"] = "joe";
    root["sex"]  = "男";
    root["age"]  = 24;

    // 格式化输出(带缩进、换行)
    std::string json_str = root.toStyledString();
    std::cout << json_str << std::endl;

    return 0;
}	

输出结果

cpp 复制代码
{
   "age" : 24,
   "name" : "joe",
   "sex" : "男"
}
  1. 使用FastWriter(紧凑输出,网络传输推荐)
    优点:无空格、无换行,体积更小,效率更高。

示例

cpp 复制代码
int main() {
    Json::Value root;
    root["name"] = "joe";
    root["sex"]  = "男";

    Json::FastWriter writer;
    std::string json_str = writer.write(root);

    std::cout << json_str << std::endl;
    return 0;
}

输出结果

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

3.使用StreamWriterBuilder(现代推荐写法)
优点 :更灵活,支持配置缩进、换行等格式。
示例

cpp 复制代码
int main() {
    Json::Value root;
    root["name"] = "joe";
    root["sex"]  = "男";

    Json::StreamWriterBuilder builder;
    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());

    std::stringstream ss;
    writer->write(root, &ss);
    std::cout << ss.str() << std::endl;

    return 0;
}

输出结果

cpp 复制代码
{
	"name" : "joe",
	"sex" : "\u7537"
}

反序列化(JSON 字符串 → C++ 数据)

反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Json

  1. 使⽤ Json::Reader :
    优点:提供详细的错误信息和位置,⽅便调试

⽰例

cpp 复制代码
int main() {
    std::string json_str = R"(
    {
        "name": "张三",
        "age": 30,
        "city": "北京"
    }
    )";
//R 是 C++11 新标准 里的 Raw string literal(原始字符串字面量)。
//作用:让字符串里的引号 ""、反斜杠 \ 不用转义!
//不写R就得写成:std::string json_str = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";
    Json::Reader reader;
    Json::Value root;

    // 解析
    bool ok = reader.parse(json_str, root);//从字符串中读取json数据
    
    if (!ok) {
    //// 解析失败,输出错误信息
        std::cerr << "解析失败:"<< 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;
}

编译与链接(关键)

使用 Jsoncpp 必须链接动态库,否则会报未定义引用错误:

bash 复制代码
g++ main.cpp -o main -ljsoncpp

常见API速查

  • 创建对象:Json::Value root;
  • 赋值:root["key"] = value;
  • 数组添加:root.append(val);
  • 格式化字符串:root.toStyledString()
  • 紧凑字符串:Json::FastWriter().write(root)
  • 解析 JSON:reader.parse(json_str, root)
  • 取值:asString()、asInt()、asBool()、asDouble()
  • 判断键是否存在:root.isMember("key")

总结

相关推荐
CoderCodingNo2 小时前
【GESP】C++二级真题 luogu-B4497, [GESP202603 二级] 数数
开发语言·c++·算法
white-persist2 小时前
【vulhub spring CVE-2018-1270】CVE-2018-1270 Spring Messaging 远程命令执行漏洞 完整复现详细分析解释
java·服务器·网络·数据库·后端·python·spring
Amnesia0_02 小时前
理解Linux中的OS管理和进程属性
linux·运维·服务器
十五年专注C++开发2 小时前
cpolar(极点云): 一款主流的内网穿透工具
linux·windows·cpolar·穿透
徒 花2 小时前
HCIP学习05 链路聚合(Eth-Trunk)+ VRRP
服务器·网络·学习·hcip
liliangcsdn2 小时前
LLM如何与mcp server交互示例
linux·开发语言·python
小夏子_riotous2 小时前
openstack的使用——7. 共享文件系统manila服务
linux·运维·服务器·系统架构·centos·openstack·运维开发
南境十里·墨染春水2 小时前
linux学习进展 进程的内存管理
linux·服务器·学习
Bert.Cai2 小时前
Linux cp命令详解
linux·运维