前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Linux】应用层自定义协议与序列化

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

作者头像
大耳朵土土垚
发布2025-02-16 20:01:01
发布2025-02-16 20:01:01
6900
代码可运行
举报
文章被收录于专栏:c/c++c/c++
运行总次数:0
代码可运行

1. 应用层协议

  • 应用层: 我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。
  • 协议: 协议是一种 “约定”。例如:socket api 的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的。其实,协议就是双方约定好的结构化的数据

2. 序列化与反序列化

  定义结构体来表示我们需要交互的信息; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体; 这个过程叫做 “序列化”“反序列化”

  只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解析, 就是 ok 的.。这种约定, 就是 应用层协议

3. 重新理解 read、write、recv、send 和 tcp 为什么支持全双工

  • 在任何一台主机上,TCP 连接既有发送缓冲区,又有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工;
  • 这就是为什么一个 tcp sockfd 读写都是它的原因;
  • 实际数据什么时候发,发多少,出错了怎么办,由 TCP 控制,所以 TCP 叫做传输控制协议。

4. 网络版本计算器实现

  我们可以自定义一个协议方便客户端与服务器之间进行IO交互,例如使用json库来进行序列化与反序列化,所以客户端向服务器发送的信息可能是这样子的:len\r\n{json}\r\n,json序列长度len——方便我们读取完整的内容,以及分隔符\r\n和json序列。服务器向客户端发送的信息也该和上述一致,只不过json序列中包含的应该是result和错误码code,而客户端向服务器发送的json序列中包含的应该是操作数xy以及操作方法operate 可以是加、减、乘、除、取模等。

定制协议

代码语言:javascript
代码运行次数:0
复制
#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
const std::string Sep = "\r\n";
// {json} -> len\r\n{json}\r\n
bool Encode(std::string &message)
{
    if (message.size() == 0)
        return false;
    std::string package = std::to_string(message.size()) + Sep + message + Sep;
    message = package;
    return true;
}
// len\r\n{json}\r\n
// 123\r\n{json}\r\n -> {json}
// 123\r\n
// 123\r\n{json
// 123\r\n{json}\r
// 123\r\n{json}\r\n123\r\n{json}\r\n123\r\n{js
// 协议,请求和应答序列化和反序列化还要加报头
// 使用JSON序列化与反序列化

bool Decode(std::string &package, std::string *content)
{
    if (package.empty())
        return false;
    auto pos = package.find(Sep);
    if (pos == std::string::npos)
        return false;

    // 判断包是否完整
    int len = std::stoi(package.substr(0, pos));
    package = package.substr(pos + Sep.size());
    if (package.size() < len + Sep.size())
        return false;
    *content = package.substr(0, len);
    package = package.substr(len + Sep.size()); // 注意最后package要将已经解包的部分删除
    return true;
}

class Request
{
public:
    Request() : _x(0), _y(0), _operate(0)
    {
    }
    Request(int x, int y,char operate) : _x(x), _operate(operate), _y(y)
    {
    }
    bool Serialize(std::string &out_string)
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["operate"] = _operate;
        out_string = root.toStyledString();
        return true;
    }
    bool Deserialize(std::string &in_string)
    {
        Json::Value root;
        Json::Reader reader;
        bool parsingSuccessful = reader.parse(in_string, root);

        if (!parsingSuccessful)
        {
            std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
            return false;
        }
        _x = root["x"].asInt(); // 注意要转化为整数
        _y = root["y"].asInt();
        _operate = root["operate"].asInt();
        Print();
        return true;
    }
    int X() const
    {
        return _x;
    }
    int Y() const
    {
        return _y;
    }
    char Operate() const
    {
        return _operate;
    }
    void Print()
    {
        std::cout<<"x: "<<_x<<std::endl;
        std::cout<<"y: "<<_y<<std::endl;
        std::cout<<"operate: "<<_operate<<std::endl;

    }
private:
    int _x;
    int _y;
    char _operate;
};

class Response
{
public:
    Response() : _result(0), _code(0)
    {
    }
    Response(int result, int code) : _result(result), _code(code)
    {
    }
    bool Serialize(std::string &out_string)
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;
        out_string = root.toStyledString();
        return true;
    }
    bool Deserialize(std::string &in_string)
    {
        Json::Value root;
        Json::Reader reader;
        bool parsingSuccessful = reader.parse(in_string, root);

        if (!parsingSuccessful)
        {
            std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
            return false;
        }
        _result = root["result"].asInt(); // 注意要转化为整数
        _code = root["code"].asInt();
        return true;
    }
    void SetResult(int result)
    {
        _result = result;
    }
    void SetCode(int code)
    {
        _code = code;
    }
    int Result()
    {
        return _result;
    }

    int Code()
    {
        return _code;
    }
    void Print()
    {
        std::cout<<"result: "<<_result<<std::endl;
        std::cout<<"code: "<<_code<<std::endl;
    }
    ~Response()
    {
    }

private:
    int _result;
    int _code;
};

应答序列和请求序列尽管使用的方法类似,但是因为序列中包含的内容不一样,所以还是需要使用两个类RequestResponse;Decode和Encode方法在不同请求和应答中都是一样的,所以直接定义在类外即可。

计算器功能实现

  这里仅仅实现+、-、*、/、%这五种方法,通过Request对象来执行并返回Response对象:

代码语言:javascript
代码运行次数:0
复制
#pragma once
#include <iostream>
#include "Protocol.hpp"

class Calculator
{
public:
    Calculator()
    {
    }
    Response Execute(const Request &req)
    {
        // 我们拿到的都是结构化的数据,拿到的不就是类对象吗!!!
        Response resp;
        switch (req.Operate())
        {
        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); // 1 就是除0
            }
            else
            {
                resp.SetResult(req.X() / req.Y());
            }
        }
        break;
        case '%':
        {
            if (req.Y() == 0)
            {
                resp.SetCode(2); // 2 就是mod 0
            }
            else
            {
                resp.SetResult(req.X() % req.Y());
            }
        }
        break;
        default:
            resp.SetCode(3); // 3 用户发来的计算类型,无法识别
            break;
        }
        resp.Print();
        return resp;
    }
    ~Calculator()
    {
    }
};

服务器代码

  与之前实现的TCP通信代码类似,只是增加一个回调方法用来处理序列化与反序列化和计算:

代码语言:javascript
代码运行次数:0
复制
#pragma once

#include <iostream>
#include <string.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <functional>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"
#include "ThreadPool.hpp"

#define BACKLOG 8
using namespace InetAddrModule;
using namespace LogModule;
using namespace ThreadPoolModule;

static const uint16_t defaultport = 8888;
using calculator_t = std::function<std::string(std::string&)>;
class TcpServer
{
    using task_t = std::function<void()>;
    struct ThreadData
    {
        int sockfd;
        TcpServer *self;
    };

public:
    TcpServer(calculator_t calculator,uint16_t port = defaultport) :_calculator(calculator), _port(port), _listensockfd(-1), _isruning(false)
    {
    }
    void InitServer()
    {
        // 1.创建Tcp套接字
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::ERROR) << "InitServer socket fail ...";
            Die(SOCKET_ERR);
        }

        // 填充信息
        struct sockaddr_in serveraddr;
        memset(&serveraddr, 0, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = ::htons(_port);    // aaa注意要转网络!!!!!!!!!!
        serveraddr.sin_addr.s_addr = INADDR_ANY; // 表示可以接收任意地址的信息

        // 2. bind;
        int n = ::bind(_listensockfd, CONV(&serveraddr), sizeof(serveraddr));
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "InitServer bind fail ...";
            Die(BIND_ERR);
        }

        // 3.监听
        int m = ::listen(_listensockfd, BACKLOG);
        if (m < 0)
        {
            LOG(LogLevel::ERROR) << "InitServer listen fail ...";
            Die(LISTEN_ERR);
        }

        LOG(LogLevel::INFO) << "ServerInit success...";
    }
    void handler(int sockfd)
    {
        char buffer[4096];
        std::string package;
        while (true)
        {
            ssize_t n = ::read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                LOG(LogLevel::INFO) << buffer;
                package += buffer;
                std::string result = _calculator(package);
                ::write(sockfd, result.c_str(), result.size());
            }
            else if (n == 0) // client 退出
            {
                LOG(LogLevel::INFO) << "client quit: " << sockfd;
                break;
            }
            else
            {
                // 读取失败
                break;
            }
        }
        ::close(sockfd); // fd泄漏问题!
    }


    void Start()
    {
        _isruning = true;
        while (_isruning)
        {
            struct sockaddr_in peer;
            socklen_t peerlen = sizeof(peer); // 这个地方一定要注意,要不然,会有问题!
            LOG(LogLevel::DEBUG) << "accepting ...";
            // 我们要获取client的信息:数据(sockfd)+client socket信息(accept || recvfrom)
            int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);

            if (sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "StartServer accept fail ...";
                continue; // 继续接收
            }
            LOG(LogLevel::INFO) << "ServerStart success...";

            // version-3:线程池版本 比较适合处理短任务,或者是用户量少的情况
            ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd]()
                                                      { this->handler(sockfd); });
            // 连接成功后就可以通信
        }
        _isruning = false;
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool _isruning;
    calculator_t _calculator;
};

  服务器执行代码:

代码语言:javascript
代码运行次数:0
复制
#include "TcpServer.hpp"
#include "Log.hpp"
#include "Calculator.hpp"

using namespace LogModule;
using cal_fun = std::function<Response(const Request &req)>;
class Parse
{
public:
    Parse(cal_fun c) : _func(c)
    {
    }
    std::string Entry(std::string &package) // 解码——反序列化——计算——序列化——编码
    {
        std::string reqstr;
        std::string retstr;
        // 1.解码
        while (Decode(package, &reqstr))
        {
            LOG(LogLevel::DEBUG) << "Reqstr: \n"
                                 << reqstr;
            if (reqstr.empty())
                break;
            // 2.反序列化
            Request req;
            req.Deserialize(reqstr);

            // 3. 计算
            Response res = _func(req);

            // 4.序列化
            std::string resstr;
            res.Serialize(resstr);
            LOG(LogLevel::DEBUG) << "resstr: \n"
                                 << resstr;
            // 5.编码
            Encode(resstr);

            // 6.返回注意不是只返回一个,而是有多少返回多少
            retstr += resstr;
        }
        return retstr;
    }

private:
    cal_fun _func;
};

int main()
{
    ENABLE_FILE_LOG_STRATEGY();
    Calculator cal;
    Parse parse([&cal](const Request &req)
                { return cal.Execute(req); });
    std::unique_ptr<TcpServer> tcpserver = std::make_unique<TcpServer>([&parse](std::string& package){return parse.Entry(package);});
    tcpserver->InitServer();
    tcpserver->Start();
    return 0;
}

客户端代码

  与服务器运行代码类似需要序列化与反序列化方法:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <arpa/inet.h>

#include "Protocol.hpp" // 形成约定

// ./client_tcp server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Usage:./client_tcp server_ip server_port" << std::endl;
        return 1;
    }
    std::string server_ip = argv[1]; // "192.168.1.1"
    int server_port = std::stoi(argv[2]);
    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cout << "create socket failed" << std::endl;
        return 2;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // client 不需要显示的进行bind, tcp是面向连接的, connect 底层会自动进行bind
    int n = ::connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (n < 0)
    {
        std::cout << "connect failed" << std::endl;
        return 3;
    }
    // echo client
    std::string message;
    while (true)
    {
        int x, y;
        char oper;
        std::cout << "input x: ";
        std::cin >> x;
        std::cout << "input y: ";
        std::cin >> y;
        std::cout << "input oper: ";
        std::cin >> oper;

        Request req(x, y, oper);

        // 1. 序列化
        req.Serialize(message);

        // 2. Encode
        Encode(message);

        // 3. 发送
        n = ::send(sockfd, message.c_str(), message.size(), 0);
        if (n > 0)
        {
            char inbuffer[1024];
            // 4. 获得应答
            int m = ::recv(sockfd, inbuffer, sizeof(inbuffer), 0);
            if (m > 0)
            {
                inbuffer[m] = 0;
                std::string package = inbuffer;//TODO
                std::string content;
                // 4. 读到应答完整--暂定, decode
                Decode(package, &content);

                // 5. 反序列化
                Response resp;
                resp.Deserialize(content);

                // 6. 得到结构化数据
                std::cout << resp.Result() << "[" << resp.Code() << "]" << std::endl;
            }
            else
                break;
        }
        else
            break;
    }

    ::close(sockfd);
    return 0;
}

运行结果

Jsoncpp

  Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。 特性:

  1. 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
  2. 高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
  3. 全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
  4. 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。

  当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时,确实存在不同的做法和工具类可供选择。以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍: 安装

代码语言:javascript
代码运行次数:0
复制
C++
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-deve

序列化

  序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。Jsoncpp 提供了多种方式进行序列化:

  1. 使用 Json::Value 的 toStyledString 方法:
  • 优点:将 Json::Value 对象直接转换为格式化的 JSON 字符串。
  • 示例:
代码语言:javascript
代码运行次数:0
复制
#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;
}
$ ./test.exe
{
"name" : "joe",
"sex" : "男"
}
  1. 使用 Json::StreamWriter:
  • 优点:提供了更多的定制选项,如缩进、换行符等。
  • 示例:
代码语言:javascript
代码运行次数:0
复制
#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;
}
$ ./test.exe
{
"name" : "joe",
"sex" : "男"
}
  1. 使用 Json::FastWriter:
  • 优点:比 StyledWriter 更快,因为它不添加额外的空格和换行符。
  • 示例:
代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
$ ./test.exe
{"name":"joe","sex":"男"}
代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
// Json::FastWriter writer;
Json::StyledWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
$ ./test.exe
{
"name" : "joe",
"sex" : "男"
}

反序列化

  反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp 提供了以下方法进行反序列化:

  1. 使用 Json::Reader:
  • 优点:提供详细的错误信息和位置,方便调试。
  • 示例:
代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main() {
// JSON 字符串
std::string json_string = "{\"name\":\"张三\",
\"age\":30, \"city\":\"北京\"}";
// 解析 JSON 字符串
Json::Reader reader;
Json::Value root;
// 从字符串中读取 JSON 数据
bool parsingSuccessful = reader.parse(json_string,
root);
if (!parsingSuccessful) {
// 解析失败,输出错误信息
std::cout << "Failed to parse JSON: " <<
reader.getFormattedErrorMessages() << std::endl;
return 1;
}
// 访问 JSON 数据
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;
}
$ ./test.exe
Name: 张三
Age: 30
City: 北京
  1. 使用 Json::CharReader 的派生类(不推荐了,上面的足够了):
  • 在某些情况下,你可能需要更精细地控制解析过程,可以直接使用Json::CharReader 的派生类。
  • 但通常情况下,使用 Json::parseFromStream 或 Json::Reader 的 parse方法就足够了。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 应用层协议
  • 2. 序列化与反序列化
  • 3. 重新理解 read、write、recv、send 和 tcp 为什么支持全双工
  • 4. 网络版本计算器实现
    • 定制协议
    • 计算器功能实现
    • 服务器代码
    • 客户端代码
    • 运行结果
    • Jsoncpp
    • 序列化
    • 反序列化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档