【Linux】自定义协议与序列化和反序列化

2024-09-06 11:28

本文主要是介绍【Linux】自定义协议与序列化和反序列化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、自定义协议

1.1 自定义报文格式

       在前面的博客中,我们可以知道在TCP协议中,面向的是字节流;而UDP协议中面向的是数据报。因此,在手写简单的TCP和UDP服务器中,所使用的是接收函数和发送函数不同。因此,在TCP协议中,我们需要分清楚一个完整的报文,并将其分离出来,因此,我们应该如何进行分离出一个完整的报文呢??

       如果一个报文中什么标志也没有,那么必然是不可能将一个完整的报文分离出来。因此,我们可以重新定义一下报文格式:为了简单起见,我们采用LV格式,在后面的学习中,当我们学习了TCP报文和UDP报文后,就会知道报文 = 报头 + 有效载荷。报头中存放有效载荷的长度,我们定义的一个简单的报文格式如图所示:

       在上一篇博客中,我们也简单的学习如何使用Json来进行序列化和反序列化的操作。我们可以在协议层中定义出来将请求和响应的报文进行序列化和反序列化操作,然后将数据通过传输层进行传输。在根据具体的报文的格式进行分离出一个完整的报文。

1.2 协议类(表示层)

       在协议类中,我们创建了请求类和响应类,并且创建出添加报头函数和分解完整报文函数,最后在定义出一个工厂用来创建请求和响应。

1.2.1 请求类

    class Request{public:// 我们自定义协议// 报文 = 包头 + 有效载荷// LV格式  固定字段长——后续字符串的长度  正文内容\n\t// 对报文进行分析// "len\r\n"_x_op_y\r\n"Request(){}Request(int x, int y, char oper): _x(x), _y(y), _oper(oper){}bool Serialize(std::string *out) // 序列化{Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;*out = writer.write(root);return true;}bool Deserialize(std::string &in) // 反序列化{Json::Value root;Json::Reader reader;bool ret = reader.parse(in, root);_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return ret;}~Request(){}public:int _x;int _y;char _oper; // "+-*/%"};

1.2.2 响应类

    class Response{public:Response(){}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::Reader reader;Json::Value root;bool ret = reader.parse(in, root);_result = root["result"].asInt();_code = root["code"].asInt();return ret;}~Response(){}public:int _result;int _code;};

1.2.3 添加报文函数

       在将创建出来的请求或者响应进行序列化后,将其进行添加报头方式,将其构造成我们所需要的报文格式,以便在之后进行分离出完整报文的操作。

    const std::string SEP = "\n\t";// 进行拼装报文std::string Encode(const std::string &json_str){int json_str_len = json_str.size();std::string proto_str = std::to_string(json_str_len);proto_str += SEP;proto_str += json_str;proto_str += SEP;return proto_str;}

1.2.4 分解完整报文函数

       解决粘包问题,我们根据报文格式,我们可以得出:在第一次遇见“\n\t”的时候,其前面的字符串会有两种情况:要么是空串,要么是报头。当我们解析到报头后,计算出一个完整的报文的总长度,然后个根据总长度将完整报文解析出来。

    const std::string SEP = "\n\t";// 处理粘包问题std::string Decode(std::string &inbuffer) // const 不能修改{// 先找出SEP,然后截取出来len的长度,最后将完整报文截取出来auto pos = inbuffer.find(SEP);if (pos == std::string::npos)return "";std::string len_str = inbuffer.substr(0, pos);if (len_str.empty())return "";int len = std::stoi(len_str);int total = len + len_str.size() + SEP.size() * 2;if (inbuffer.size() < total)return "";std::string package = inbuffer.substr(pos + SEP.size(), len);inbuffer.erase(0, total); // yichureturn package;}

1.2.5 工厂类

       在这里采用简单工厂模式,我们可以将请求类和响应类设置为私有类,不想外部暴漏;只将工厂类向外暴漏,方便用户进行注册请求和响应。

    // 简单工厂模式class Factory{public:Factory(){srand(time(nullptr) ^ getpid());}// 生产数据   利用随机值将报文填充std::shared_ptr<Request> BuildRequest(){opers = "+-*/%&^";int x = rand() % 10;int y = rand() % 5;char oper = opers[rand() % 8];std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);return req;}std::shared_ptr<Response> BuildResponse(){return std::make_shared<Response>();}~Factory() {}private:std::string opers;};

二、复习一下TCP服务器的写法

2.1 回忆一下Socket类

       在上一节课中,我们将Socket进行封装。我们将TCP套接字和UDP套接字的方法进行封装成一个类中,来回忆一下他们的方法总共有哪些?创建套接字,绑定套接字,监听套接字,TCP接收数据,TCP连接,返回套接字,接收数据,发送数据。

// 在抽象类中,我们可以定义出一些虚函数,供TCPSocket和UDPSocket方便构造各自的函数
virtual void CreateSocketOrDie() = 0;             // 创建套接字
virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字
virtual void ListenSocketOrDie() = 0;             // 监听套接字
virtual socket_sptr Accepter(InetAddr *addr) = 0; // 接受数据
virtual bool Connector(InetAddr &addr) = 0;       // 连接
virtual int Sockfd() = 0;                         // 返回套接字
virtual int Recv(std::string *out) = 0;           // 接收数据
virtual int Send(const std::string &in) = 0;      // 发送数据

2.2 TcpServer类

       在这个类中,我们可以通过2.1中的TcpSocket类进行创建,我们可以创建出智能指针,通过智能指针来进行管理这个变量,调用里面的函数。在执行函数中,我们可以利用Acceptor函数创建出新的套接字,以便于进行与客户端通信。在这个函数中,我们可以使用多线程来进行。

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
#include <memory>using namespace socket_ns;
using io_service_t = std::function<void(socket_sptr, InetAddr)>;
class TcpServer;
const static int backlog = 16; // 连接队列
class ThreadDate
{
public:ThreadDate(socket_sptr sockfd, InetAddr addr, TcpServer *s) : _sockfd(sockfd), _addr(addr), self(s) {}InetAddr _addr;socket_sptr _sockfd;TcpServer *self;
};class TcpServer
{
public:TcpServer(int port, io_service_t service): _local("0", port), _isrunning(false), _listensock(std::make_unique<TcpSocket>()), _service(service){_listensock->BuildListenSocket(_local); // 创建套接字,绑定套接字,监听套接字}~TcpServer(){}static void *HandlerSock(void *args){pthread_detach(pthread_self());ThreadDate *td = static_cast<ThreadDate *>(args);td->self->_service(td->_sockfd, td->_addr); // 这个地址就是客户端的地址::close(td->_sockfd->Sockfd());delete td;return nullptr;}void Loop(){// 4. 不能直接收数据,先获取连接// accept在通信之前先获取客户端的地址, 返回值是文件描述符// 这个文件描述符是什么?? 每建立一个链接就会有一个套接字, 用于IO操作_isrunning = true;while (_isrunning){// 利用accept函数进行创建出新的套接字InetAddr peeraddr;socket_sptr s = _listensock->Accepter(&peeraddr);if (s == nullptr)continue;// version 2 : 采用多线程pthread_t t;ThreadDate *td = new ThreadDate(s, peeraddr, this);pthread_create(&t, nullptr, HandlerSock, td);}_isrunning = false;}
private:InetAddr _local;std::unique_ptr<Socket> _listensock;bool _isrunning;io_service_t _service;
};

三、业务服务类(应用层)

       为了使业务服务与通信服务进一步解耦合,我们可以将业务服务单独封装成一个类,使得整体布局更加具有层次性。

#pragma once
#include <iostream>
#include "Protocol.hpp"
using namespace protocol_ns;// 解决计算层
class Calculate
{
public:Calculate(){}// 处理函数Response Excute(const Request &req){Response rsp(0, 0);switch (req._oper){case '+':rsp._result = req._x + req._y;break;case '-':rsp._result = req._x - req._y;break;case '*':rsp._result = req._x * req._y;break;case '/':{if (req._y == 0){rsp._code = 1;}else{rsp._result = req._x / req._y;}}break;case '%':{if (req._y == 0){rsp._code = 2;}else{rsp._result = req._x % req._y;}}break;default:rsp._code = 3;break;}return rsp;}~Calculate(){}
private:
};

四、创建客户端(会话层)

       由于之前的套接字封装,我们可以简单地通过智能指针创建出TcpSocket,通过智能指针可以直接创建出客户端套接字并连接服务器成功。我们之后就可以进行通信。

客户端与服务器通信的步骤:创建一个请求;将请求进行序列化;将请求添加报头;发送报文;读取应答;判断应答是否是一个完整的报文;将报文反序列化,最后拿到了结构化的应答。

void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(2);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);InetAddr serveraddr(serverip, serverport);std::unique_ptr<Socket> cli = std::make_unique<TcpSocket>();bool res = cli->BuildClientSocket(serveraddr);std::string buffer;while (res){Factory factory;// 1. 创建一个请求auto req = factory.BuildRequest();// 2. 将请求进行序列化std::string request;req->Serialize(&request);// 3. 添加报文长度request = Encode(request);// 4. 发送报文cli->Send(request);// 5. 读取应答int n = cli->Recv(&buffer);if (n < 0){break; // 出错了}buffer = Encode(buffer);// 6. 判断应答是否是一个完整的报文auto resp = factory.BuildResponse();resp->Deserialize(buffer);// 7. 拿到了结构化的应答std::cout << resp->_result << "[" << resp->_code << "]" << std::endl;}return 0;
}

五、总结

       将OSI七层模式与TCP/IP分层模式进行对比,我们会发现TCP/IP分层模型将会话层、表示层与应用层合并为一层。因为应用层必须要在用户层完成,用户决定了与谁构建连接,用户决定了采用什么样的报文格式以及协议,用户决定了采用什么服务。

这篇关于【Linux】自定义协议与序列化和反序列化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1141875

相关文章

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s