当前位置: 首页 > news >正文

【Linux网络】再谈 “协议“

目录

再谈 "协议"

结构化数据的传输

序列化和反序列化

网络版计算器 

封装套接字操作 

服务端代码

服务进程执行例程

启动网络版服务端

协议定制

客户端代码

代码测试

使用JSON进行序列化与反序列化


我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。

再谈 "协议"

我们之前讲过:协议是一种 "约定"。网络协议是通信计算机双方必须共同遵从的一组约定,只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。

结构化数据的传输

socket api的接口,在读写数据时,都是按 "字符串" 的方式来发送接收的。

  • 如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。
  • 但如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。 

那么如果我们要传输一些"结构化的数据" 怎么办呢?

例如,我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。

当客户端选择将结构化的数据逐一发送到网络中,服务端接收数据的过程也会相应地碎片化。每次从网络中接收到一部分数据,服务端都需要对这些离散的信息进行整理,尝试将它们重新组合成原始的结构化数据。这个过程既复杂又容易出错,因为数据可能在传输过程中出现丢失或顺序混乱的情况。

因此,为了简化数据传输和处理的流程,客户端通常会采取“打包”策略。打包意味着将多个相关的数据元素组合成一个整体,然后再进行传输。这样,服务端每次从网络中接收到的都是一个完整的数据包,其中包含了所有必要的信息。

客户端常见的“打包”方式主要有两种:

约定方案一:

  • 客户端发送一个形如"1+1"的字符串;
  • 这个字符串中有两个操作数, 都是整形;
  • 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
  • 数字和运算符之间没有空格;

客户端能够将结构化的数据编排成一个字符串格式,并通过网络将其发送出去。当服务端从网络接收到这个字符串时,它会采用与客户端相同的解析方法,从而从这个字符串中提取出原始的结构化数据。这样的通信方式确保了数据的完整性和准确性在客户端和服务端之间的传输。

约定方案二:

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

客户端可以设计一个特定的结构体,将需要交互的信息定义到这个结构体当中。在发送数据前,客户端会利用序列化技术将这个数据结构转换成一种统一的、可传输的字符串或字节流格式。当服务端接收到这些数据后,它会利用反序列化技术将这个字符串或字节流还原成原始的数据结构。通过这种方式,服务端可以轻松地提取出客户端发送的信息,并进行相应的处理。这种序列化和反序列化的过程确保了数据在不同系统间的兼容性和可交换性。 

序列化和反序列化

  • 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。
  • 反序列化是把字节序列恢复为对象的过程。

OSI七层模型中的表示层的主要任务是将设备内部特有的数据格式,即应用层上的数据格式,转换为符合网络传输标准的格式。这种网络标准数据格式通常是通过序列化过程得到的,使得数据能够以一致和可理解的方式在网络中进行传输。

网络版计算器 

封装套接字操作 

由于服务端和客户端都需要创建套接字,以及使用套接字完成一些固定的操作,因此我们实现一个简单的TCP套接字(socket)类的实现,它封装了套接字的基本操作:包括创建、绑定、监听、接受连接和连接。这样服务端和客户端都可以直接调用这些函数。封装套接字操作可以使服务端和客户端代码更整洁、可重用,并减少重复代码。

Socket.hpp

#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"enum{SocketErr = 2,BindErr,ListenErr,AcceptErr
};#define backlog 10class Sock
{
public:Sock(){}~Sock(){}
public:void Socket(){sockfd_ = socket(AF_INET,SOCK_STREAM,0);if(sockfd_ < 0){lg(Fatal,"socker error, %s:%d",strerror(errno),errno);exit(SocketErr);}   }void Bind(uint16_t& port){struct sockaddr_in local;memset(&local,0,sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if(bind(sockfd_,(struct sockaddr *)&local,sizeof(local)) < 0){lg(Fatal,"bind error, %s: %d", strerror(errno), errno);exit(BindErr);}}void Listen(){if(listen(sockfd_ , backlog) < 0){lg(Fatal,"listen error, %s: %d", strerror(errno), errno);exit(ListenErr);}}int Accept(std::string* clientip, std::uint16_t* clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(sockfd_,(struct sockaddr*)&peer,&len);if(newfd < 0){lg(Warning,"accept error, %s: %d", strerror(errno), errno);exit(AcceptErr);}char ipstr[64];inet_ntop(AF_INET,&(peer.sin_addr.s_addr),ipstr,sizeof(ipstr));*clientip = ipstr;*clientport = ntohs(peer.sin_port);return newfd;}bool Connect(const std::string &ip, const uint16_t &port){struct sockaddr_in peer;memset(&peer,0,sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(port);inet_pton(AF_INET,ip.c_str(),&(peer.sin_addr.s_addr));int n = connect(sockfd_,(struct sockaddr*)&peer,sizeof(peer));if(n == -1){std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;return false;}return true;}void Close(){close(sockfd_);}int FD(){return sockfd_;}private:int sockfd_;
};

服务端代码

首先,我们需要初始化服务器,这包括三个关键步骤:

  1. 使用socket函数来创建一个新的套接字。
  2. 接着,通过bind函数,我们将这个套接字绑定到一个特定的端口号上,这样客户端就可以通过这个端口与服务器建立连接。
  3. 然后,通过调用listen函数,我们将套接字设置为监听状态,等待客户端的连接请求。

服务器初始化完成后,就可以启动它了。启动后,服务器的主要任务是不断地调用accept函数,从监听套接字中接收新的连接请求。每当成功接受到一个新连接时,服务器会创建一个新的进程。这个新进程将负责为该客户端提供计算服务,确保每个客户端都能得到及时且独立的响应。 

TcpServer.hpp 

#pragma once#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <functional>
#include "Socket.hpp"//这允许我们为 TCP 服务器提供一个自定义的回调函数,该函数处理从客户端接收到的数据。
using func_t = std::function<std::string(std::string &package)>; //std::function对象,该对象接受一个 std::string 引用作为参数并返回一个 std::stringclass TcpServer
{
public:TcpServer(uint16_t port,func_t callback):port_(port),callback_(callback){}//初始化tcp服务器bool InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();lg(Info,"init server .... done");return true;}//启动服务器void Start(){   signal(SIGCHLD, SIG_IGN);//忽略了 SIGCHLD 和 SIGPIPE 信号,当子进程终止或管道写入失败时,服务器不会接收到这些信号。signal(SIGPIPE, SIG_IGN);//无限循环,在该循环中,它尝试接受新的客户端连接。对于每个新的连接,它创建一个子进程来处理该连接。//并使用之前提供的回调函数来处理这些数据。如果回调函数返回一个非空字符串,那么该字符串将被发送回客户端。while (true){std::string clientip;uint16_t clientport;int sockfd = listensock_.Accept(&clientip,&clientport);if(sockfd < 0)continue;// 返回继续监听       lg(Info,"accept a new link, sockfd: %d, clientip: %s, clientport: %d",sockfd, clientip, clientport);// 提供服务if(fork() == 0)//{listensock_.Close();//在子进程中,服务器不再需要监听套接字,调用 listensock_.Close(); 关闭监听套接字,释放相关的系统资源。  std::string inbuffer_stream;//数据计算while (true){char buffer[1280];ssize_t n = read(sockfd,buffer,sizeof(buffer));if(n > 0)   {buffer[n] = 0;inbuffer_stream += buffer;while(true){std::string info = callback_(inbuffer_stream);if(info.empty())break;write(sockfd, info.c_str(), info.size());}}else if(n == 0)break;elsebreak;}   exit(0);//}close(sockfd);}   }~TcpServer(){}
private:uint16_t port_;Sock listensock_;func_t callback_;
};

说明一下: 

  • 当前服务器采用的是多进程的方案,对于每个新的连接,创建一个子进程来处理该连接。
  • 提供的回调函数来处理客户端发送过来的数据。如果回调函数返回一个非空字符串,那么该字符串将被发送回客户端。

服务进程执行例程

当服务端调用accept函数获取到新连接并创建新进程后,该线程就需要为该客户端提供计算服务,此时该进程需要先读取客户端发来的计算请求,然后进行对应的计算操作,如果客户端发来的计算请求存在除0、模0、非法运算等问题,就将响应结构体当中的状态字段对应设置为1、2、3即可。

ServerCal.hpp 

#pragma once
#include <iostream>
#include "Protocol.hpp"enum
{DivZero = 1,ModZero,Other_Oper
};class ServerCal
{
public:ServerCal(){}Response CalculatorHelper(const Request &req){Response resp(0, 0);switch (req.op){case '+':resp.result = req.x + req.y;break;case '-':resp.result = req.x - req.y;break;case '*':resp.result = req.x * req.y;break;case '/':{if (req.y != 0)resp.result = req.x / req.y;elseresp.code = DivZero;}break;case '%':{if (req.y != 0)resp.result = req.x % req.y;elseresp.code = ModZero;}break;default:resp.code = Other_Oper;break;}return resp;}// "len"\n"10 + 20"\nstd::string Calculator(std::string &package){std::string content;bool r = Decode(package, &content); // "len"\n"10 + 20"\nif (!r)return "";// "10 + 20"Request req;r = req.Deserialize(content); // 反序列化完req中的变量就拿到值了if (!r)return "";content = "";                          //清空Response resp = CalculatorHelper(req); // result=30 code=0;// 计算完进行序列化resp.Serialize(&content);content = Encode(content);return content;}~ServerCal(){}
};

启动网络版服务端

ServerCal.cpp

前面我们在TcpServer.hpp封装了服务器初始化和启动服务器函数的类,以及ServerCal类实现网络版计算器的类执行例程。下面我们实现一个ServerCal.cpp来启动网络版服务器,只有要调用前面两个类实现的接口即可。

  • 从命令行参数获取端口号
  • 创建ServerCal实例
  • 绑定ServerCal的Calculator方法
  • 创建TcpServer实例,并将绑定的Calculator方法和端口号作为参数传递给它。
  • 调用InitServer方法初始化服务器
  • 最后调用Start方法启动服务器。
#include "TcpServer.hpp"
#include "ServerCal.hpp"void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " port\n" << std::endl; 
}int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);ServerCal cal;//std::bind是C++标准库中的一个函数模板,它可以将一个可调用对象(如函数、lambda函数或成员函数指针)与其参数绑定,生成一个新的可调用对象。//&ServerCal::Calculator是ServerCal类中的Calculator成员函数的指针。//&cal是ServerCal类的一个实例的地址,该实例用于调用Calculator成员函数。//std::placeholders::_1是一个占位符,它表示bind生成的新可调用对象接受的第一个参数将传递给Calculator成员函数作为它的第一个参数。TcpServer *tsvp = new TcpServer(port,std::bind(&ServerCal::Calculator,&cal,std::placeholders::_1));tsvp->InitServer();tsvp->Start();return 0;
}

协议定制

为了实现一个网络版的计算器,确保通信双方遵循共同的规则和约定是至关重要的。这就需要我们制定一套简明的协议。数据的交互通常涉及请求数据和响应数据,因此需要分别定义两者的结构。在实现层面,C++允许通过类来组织代码和数据,但同样也可以使用更简单的结构体来定义数据结构。考虑到简洁性和直接性,这里我们选择使用结构体来定义请求和响应的数据格式。

因此我们需要设计一个请求结构体,用于封装从客户端发送到服务器的计算请求信息,以及一个响应结构体,用于封装服务器处理完请求后返回给客户端的结果。通过这种方式,我们可以确保通信双方按照预定的格式发送和接收数据,从而实现网络计算器的功能。

  • 请求结构体中需要包括两个操作数,以及对应需要进行的操作。
  • 响应结构体中需要包括一个计算结果,除此之外,响应结构体中还需要包括一个状态字段,表示本次计算的状态,因为客户端发来的计算请求可能是无意义的。
  • 请求结构体和响应结构体当中都封装了序列化函数和反序列化函数。
  • 我们在类外设置了编码函数和解码函数
#pragma once#include <iostream>
#include <string>const std::string blank_space_sep = " ";
const std::string protocol_sep  = "\n";std::string Encode(std::string &content)
{std::string package = std::to_string(content.size());package += protocol_sep;package += content;package += protocol_sep;return package;
}// "len"\n"x op y"\nXXXXXX
// "protocolnumber"\n"len"\n"x op y"\nXXXXXX
bool Decode(std::string &package, std::string *content)
{size_t pos = package.find(protocol_sep);if(pos == std::string::npos) return false;std::string len_str = package.substr(0,pos);std::size_t len = std::stoi(len_str);std::size_t total_len = len_str.size() + len + 2;if(package.size() < total_len)  return false;//传入的序列化字符串没有达到报头提供的字符串长度*content = package.substr(pos+1,len);package.erase(0,total_len);//return true;
}class Request
{
public:Request(int data1,int data2,char oper):x(data1),y(data2),op(oper){}Request()//{}
public:bool Serialize(std::string *out){// 构建报文的有效载荷// struct => string, "x op y"std::string s = std::to_string(x);s += blank_space_sep;s += op;s += blank_space_sep;s += std::to_string(y);*out = s;return true;}bool Deserialize(const std::string &in)// "x op y"{std::size_t left = in.find(blank_space_sep);if(left == std::string::npos)return false;std::string part_x = in.substr(0,left);std::size_t right = in.rfind(blank_space_sep);if(right == std::string::npos)return false;std::string part_y = in.substr(right);if(left+2 != right)return false;op = in[left + 1];x = std::stoi(part_x);x = std::stoi(part_y);return true;}void DebugPrint(){std::cout << "新请求构建完成:  " << x << op << y << "=?" << std::endl;}public://x op yint x;int y;char op;//+ - * / %
};class Response
{
public:Response(int res,int c):result(res),code(c){}Response(){}
public:bool Serialize(std::string *out){// "result code"// 构建报文的有效载荷std::string s = std::to_string(result);s += blank_space_sep;s += std::to_string(code);*out = s;return true;}bool Deserialize(const std::string &in){std::size_t pos = in.find(blank_space_sep);if(pos == std::string::npos) return false;std::string part_left = in.substr(0,pos);std::string part_right = in.substr(pos+1);result = std::stoi(part_left);code = std::stoi(part_right);return true;}void DebugPrint(){std::cout << "结果响应完成, result: " << result << ", code: "<< code << std::endl;}public:int result;int code;//0表示结果是可信的;否则!0具体是几,表明对应的错误原因
};

请求结构体

  • 序列化函数用于构建报文的有效载荷,将 `Request` 对象转换为一个字符串。它首先将 `x` 和 `y` 转换为字符串,并使用空格和操作符 `op` 将它们连接在一起。例如,如果 `x` 是 5,`y` 是 3,并且 `op` 是 `+`,则生成的字符串将是 `"5 + 3"`
  • 反序列化函数尝试从一个字符串中恢复一个 `Request` 对象。它首先查找空格和操作符来分隔 `x`、`op` 和 `y`。然后,它将这些部分转换回它们的原始类型,并检查字符串的格式是否正确。如果一切正常,它将更新 `Request` 对象的 `x`、`y` 和 `op`。

响应结构体

  • Serialize 函数接受一个指向 std::string 的指针 out,并将 result 和 code 成员变量的值转换为字符串,然后用空格(blank_space_sep)分隔它们,并将结果字符串存储在 out 所指向的位置。函数总是返回 true,表示序列化操作总是成功的。
  • Deserialize 函数接受一个常量字符串引用 in,并尝试从中解析出 result 和 code 的值。它首先查找空格的位置,然后提取空格前后的两个子字符串,并将它们分别转换为整数来更新 result 和 code 的值。如果字符串中没有找到空格,函数返回 false,否则返回 true。

编码函数 Encode:

  • 函数接受一个字符串 content,并返回一个编码后的字符串 package。
  • 首先,将 `content` 的大小(长度)转换为字符串并添加到 `package`。  
  • 然后,添加一个换行符。  
  • 接着,添加原始的 `content`。  
  • 最后,再添加一个换行符。这样,编码后的字符串格式是:`"length\ncontent\n"`。

解码函数 Decode:

  • 这个函数尝试从给定的 package 字符串中解码出 content。它首先查找换行符来确定 content 的长度,并检查 package 是否包含足够的数据。如果成功,它会提取 content 并从 package 中删除已解码的部分。

注意:

  • 编码函数和解码函数是多个结构体或类都可能需要的共同操作,因此将它们放在类外作为独立的函数。这种做法不仅增强了代码的可重用性,还方便了协议的编码和解码逻辑的更换。通过将编码和解码逻辑与具体的数据结构分离,我们可以在不修改数据结构定义的情况下更换编码和解码的实现,从而实现了更好的模块化和可扩展性。

规定状态字段对应的含义:

  • 状态字段为0,表示计算成功。
  • 状态字段为1,表示出现除0错误。
  • 状态字段为2,表示出现模0错误。
  • 状态字段为3,表示非法计算。

此时我们就完成了协议的设计,但需要注意,只有当响应结构体当中的状态字段为0时,计算结果才是有意义的,否则计算结果无意义。 

客户端代码

客户端首先也需要进行初始化:

  • 调用socket函数,创建套接字。

客户端初始化完毕后需要调用connect函数连接服务端,当连接服务端成功后,客户端就可以向服务端发起计算请求了。这里可以让用户输入两个操作数和一个操作符构建一个计算请求,然后将该请求发送给服务端。而当服务端处理完该计算请求后,会对客户端进行响应,因此客户端发送完请求后还需要读取服务端发来的响应数据。

客户端在向服务端发送或接收数据时,可以使用write或read函数进行发送或接收(也可以使用send或recv函数对应进行发送或接收。)

  • 连接服务器

由于我们前面封装了TCP套接字(socket)类的实现,这里我们之间调用我们封装的接口即可,下面是客户端代码:

客户端代码:

#include <iostream>
#include <string>
#include <time.h>
#include <assert.h>
#include "Protocol.hpp"
#include"Socket.hpp"void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " serverip serverport\n" << std::endl;
}// ./clientcal ip port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);//创建套接字Sock sockfd;sockfd.Socket();//链接服务器bool r = sockfd.Connect(serverip,serverport);if(!r) return 1;srand(time(nullptr)^getpid());int cnt = 1;const std::string opers = "+-*/%=-=&^";std::string inbuffer_stream;while(cnt <= 10){//准备数据std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;int x = rand() % 100 + 1;usleep(1000);int y = rand() % 100 + 1;usleep(1000);char op = opers[rand() % opers.size()];Request req(x,y,op);req.DebugPrint();//客户端发送请求std::string packge;req.Serialize(&packge);packge = Encode(packge);write(sockfd.FD(),packge.c_str(),packge.size());//接受请求响应char buffer[128];ssize_t n = read(sockfd.FD(),buffer,sizeof(buffer));// 我们也无法保证我们能读到一个完整的报文if(n > 0){buffer[n] = 0;inbuffer_stream += buffer;// "len"\n"result code"\nstd::cout << inbuffer_stream << std::endl;std::string content;bool r = Decode(inbuffer_stream,&content);// "result code"assert(r);Response resp;r = resp.Deserialize(content);assert(r);resp.DebugPrint();}std::cout << "=================================================" << std::endl;sleep(1);cnt++;}sockfd.Close();return 0;
}

代码测试

运行服务端后再让客户端连接服务端,此时服务端就会对客户端发来的计算请求进行处理,并会将计算后的结果响应给客户端。

我们看到如果客户端要进行除0、模0、非法运算,在服务端识别后就会按照约定对应将响应数据的状态码设置为1、2、3,此时响应状态码为非零,因此在客户端打印出来的计算结果就是没有意义的。

此时我们就以这样一种方式约定出了一套应用层的简单的网络计算器,这就叫做协议。 

使用JSON进行序列化与反序列化

上面我们进行序列化和反序列化是自己进行协议定制,其实我们也可以用JSON或者Protobuf进行数据的序列化和反序列化操作。

JSON (JavaScript Object Notation) 和 Protobuf (Protocol Buffers) 都是数据序列化格式,但它们在设计目标、性能、使用场景等方面有所不同。

下面我们主要来介绍一下使用JSON进行序列化和反序列化操作:

JSON

  • 设计目标:JSON 主要用于人类可读性和易于编写。它是基于 JavaScript 的子集,但不仅限于 JavaScript 使用。
  • 性能:JSON 的解析和序列化速度相对较慢,尤其是对于大型数据结构。
  • 使用场景:JSON 广泛用于 API 通信、配置文件、Web 存储等场景,因为它易于阅读和编写,并且跨语言、跨平台。
  • 在使用 JsonCpp 之前,你需要确保已经安装了这个库。
sudo yum install -y jsoncpp-devel
  • 安装完成后,项目中加入头文件#include <jsoncpp/json/json.h>
  • 编译命令后面加上-ljsoncpp

下面是一个简单的示例,展示了如何使用 JsonCpp 来解析和生成 JSON 数据:

#include <iostream>  
#include <jsoncpp/json/json.h>  
#include <unistd.h>int main() {  // 创建一个 JSON 对象  Json::Value root; // 将用于存储 JSON 数据的根对象  root["x"] = 40;  root["y"] = 30;  root["op"] = '+';  root["desc"] = "this is a + oper";  // 序列化:将 JSON 对象转换为字符串  Json::FastWriter writer;  //Json::StyledWriter writer;  //StyledWriter比Fastwriter多加了\n,可读性比较好std::string jsonString = writer.write(root);  // 输出 JSON 字符串  std::cout << "JSON string: " << jsonString << std::endl;  sleep(3);// 反序列化:从字符串解析 JSON  Json::Value v;  Json::Reader Reader;  Reader.parse(jsonString,v);// 访问 JSON 对象中的值  int x = v["x"].asInt();  int y = v["y"].asInt();  char op = v["op"].asInt();  std::string desc = v["desc"].asString();  // 输出解析后的值  std::cout << x <<std::endl;  std::cout << y <<std::endl;      std::cout << op <<std::endl;      std::cout << desc <<std::endl;     return 0;  
}

运行结果:

有了上面对JSON基本使用的理解后,下面我们在网络版计算器的协议定制的代码中增加JSON方式的序列化与反序列化:

我们根据是否定义了MySelf宏,来选择使用两种序列化方式:

#pragma once#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>// #define MySelf 1const std::string blank_space_sep = " ";
const std::string protocol_sep  = "\n";std::string Encode(std::string &content)
{std::string package = std::to_string(content.size());package += protocol_sep;package += content;package += protocol_sep;return package;
}// "len"\n"x op y"\nXXXXXX
// "protocolnumber"\n"len"\n"x op y"\nXXXXXX
bool Decode(std::string &package, std::string *content)
{size_t pos = package.find(protocol_sep);if(pos == std::string::npos) return false;std::string len_str = package.substr(0,pos);std::size_t len = std::stoi(len_str);std::size_t total_len = len_str.size() + len + 2;if(package.size() < total_len)  return false;//传入的序列化字符串没有达到报头提供的字符串长度*content = package.substr(pos+1,len);package.erase(0,total_len);//return true;
}// json, protobuf
class Request
{
public:Request(int data1,int data2,char oper):x(data1),y(data2),op(oper){}Request()//{}
public:bool Serialize(std::string *out){
#ifdef MySelf// 构建报文的有效载荷// struct => string, "x op y"std::string s = std::to_string(x);s += blank_space_sep;s += op;s += blank_space_sep;s += std::to_string(y);*out = s;return true;
#elseJson::Value root;root["x"] = x;root["y"] = y;root["op"] = op;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool Deserialize(const std::string &in)// "x op y"{
#ifdef MySelfstd::size_t left = in.find(blank_space_sep);if(left == std::string::npos)return false;std::string part_x = in.substr(0,left);std::size_t right = in.rfind(blank_space_sep);if(right == std::string::npos)return false;std::string part_y = in.substr(right);if(left+2 != right)return false;op = in[left + 1];x = std::stoi(part_x);y = std::stoi(part_y);return true;
#elseJson::Value root;Json::Reader r;r.parse(in,root);x = root["x"].asInt();y = root["y"].asInt();char op = root["op"].asInt();return true;
#endif}void DebugPrint(){std::cout << "新请求构建完成:  " << x << op << y << "=?" << std::endl;}public://x op yint x;int y;char op;//+ - * / %
};class Response
{
public:Response(int res,int c):result(res),code(c){}Response(){}
public:bool Serialize(std::string *out){
#ifdef MySelf// "result code"// 构建报文的有效载荷std::string s = std::to_string(result);s += blank_space_sep;s += std::to_string(code);*out = s;return true;
#elseJson::Value root;root["result"] = result;root["code"] = code;// Json::FastWriter w;Json::StyledWriter w;*out = w.write(root);return true;
#endif}bool Deserialize(const std::string &in){
#ifdef MySelfstd::size_t pos = in.find(blank_space_sep);if(pos == std::string::npos) return false;std::string part_left = in.substr(0,pos);std::string part_right = in.substr(pos+1);result = std::stoi(part_left);code = std::stoi(part_right);return true;
#elseJson::Value root;Json::Reader r;r.parse(in,root);int result = root["result"].asInt();int code = root["code"].asInt();
#endif}void DebugPrint(){std::cout << "结果响应完成, result: " << result << ", code: "<< code << std::endl;}public:int result;int code;//0表示结果是可信的;否则!0具体是几,表明对应的错误原因
};

makefile文件:

编译时要加上-ljsoncpp选项,我们也可以在makefile文件中进行宏定义Myself

.PHONY:all
all:servercal clientcal# Flag= -DMySelf=1
Flag= #-DMySelf=1
Lib=-ljsoncppservercal:ServerCal.cppg++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
clientcal:ClientCal.cppg++ -o $@ $^ -std=c++11 $(Lib) $(Flag).PHONY:clean
clean:rm -f servercal clientcal

代码测试:

以上我们就成功用Json实现了数据序列化和反序列化。

相关文章:

【Linux网络】再谈 “协议“

目录 再谈 "协议" 结构化数据的传输 序列化和反序列化 网络版计算器 封装套接字操作 服务端代码 服务进程执行例程 启动网络版服务端 协议定制 客户端代码 代码测试 使用JSON进行序列化与反序列化 我们程序员写的一个个解决我们实际问题&#xff0c;满…...

猫头虎分享已解决Bug || 系统监控故障:MonitoringServiceDown, MetricsCollectionError

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …...

Java中的基本数据类型有哪些

在Java编程语言中&#xff0c;基本数据类型&#xff08;Primitive Types&#xff09;是预定义的数据类型&#xff0c;它们不是由用户定义的类创建的&#xff0c;而是由语言本身提供的。这些基本数据类型是构成Java程序的基础&#xff0c;用于存储不同类型的值&#xff0c;如整数…...

二叉树遍历(前中后序的递归/非递归遍历、层序遍历)

二叉树的遍历 1. 二叉树的前序、中序、后序遍历 前、中、后序遍历又叫深度优先遍历 注&#xff1a;严格来说&#xff0c;深度优先遍历是先访问当前节点再继续递归访问&#xff0c;因此&#xff0c;只有前序遍历是严格意义上的深度优先遍历 首先需要知道下面几点&#xff1a; …...

UE4升级UE5 蓝图节点变更汇总(4.26/27-5.2/5.3)

一、删除部分 Ploygon Editing删除 Polygon Editing这个在4.26、4.27中的插件&#xff0c;在5.1后彻底失效。 相关的蓝图&#xff0c;如编辑器蓝图 Generate mapping UVs等&#xff0c;均失效。 如需相关功能&#xff0c;请改成Dynamic Mesh下的方法。 GetSupportedClass删…...

【python】异常处理

前言 省略各种废话&#xff0c;直接快速整理知识点 try-except 基础 作用 程序不可能永远都是对的&#xff0c;当7除a&#xff0c;a由用户输入时&#xff0c;用户输入0就会报错。try-except就是解决这些问题。 结构 多分支自定义错误类型 上方的exception是一个错误类型…...

【xv6操作系统】Lab systems calls

一、实验前须知 阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件&#xff1a; 系统调用的用户空间代码在 user/user.h 和 user/usys.pl 中。 内核空间代码在 kernel/syscall.h 和 kernel/syscall.c 中。 与进程相关的代码在 kernel/proc.h 和 kernel/proc.c…...

python的scripts文件夹作用

Windows系统&#xff1a; Scripts文件夹通常位于Python的安装目录下&#xff0c;如C:\Python\Scripts。该文件夹内包含了各种有用的工具&#xff0c;例如pip、virtualenv等&#xff0c;这些工具有助于管理和配置Python环境和依赖包。 Linux系统&#xff1a; 在Linux系统中&…...

Discuz论坛网站报错Discuz!Database Error(0)notconnect的解决办法

运营服务器大本营有段时间了&#xff0c;在运营期间遇到两次Discuz&#xff01;Database Error&#xff08;0&#xff09;notconnect报错&#xff0c;和你们分享遇到Discuz报错的解决办法&#xff0c;希望可以帮助到你。 首先网站报错&#xff08;0&#xff09;notconnect&…...

掌握mysql,看完这篇文章就够了

​数据库 对大量数据进行存储和管理&#xff08;增删改查&#xff09; 客户端&#xff1a; 黑窗口终端navicat 熊掌软件数据库分类&#xff1a; 关系型数据库 通过表与表产生关联关系&#xff0c;每个表中都存储结构化数据&#xff0c;支持sql结构化查询语言MysqlOracleSQLS…...

守护Web安全:了解Web攻击与防护策略

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…...

变换,动画

面试题——需求&#xff1a;在不知道父元素与子元素的宽高时 如何让子元素在父元素内居中&#xff1f; 1.定位 父相子绝 2.子元素 top&#xff1a;50% left:50% 3.子元素 transform: translate(-50%,-50%) .parent{height: 500px;background-color: red;position: relative;}.c…...

深度解析速卖通商品详情API:Python实战与高级技术探讨

速卖通商品详情API接口实战&#xff1a;Python代码示例 一、准备工作 在开始之前&#xff0c;请确保你已经完成了以下步骤&#xff1a; 在速卖通开放平台注册账号并创建应用&#xff0c;获取API密钥。阅读速卖通商品详情API接口的文档&#xff0c;了解接口的使用方法和参数要…...

背包问题算法

背包问题算法 0-1背包问题二维数组一维数组 完全背包问题二维数组一维数组 多重背包问题一维数组 0-1背包问题 问题&#xff1a;背包的容量为9&#xff0c;有重量分别为[2, 4, 6, 9]的四个物品&#xff0c;价值分别为[3, 4, 5, 6]&#xff0c;求背包能装的物品的最大价值是多少…...

echarts柱状图可鼠标左击出现自定义弹框,右击隐藏弹框并阻止默认右击事件

每项x轴数据对应有两条柱图和一条阴影效果是学习其它博客得到的效果&#xff0c;这个是学习的原文链接&#xff1a;echarts两个合并柱体&#xff08;普通柱状图象形柱图&#xff09;共享一个柱体阴影 因为这次情况比较特殊&#xff0c;不仅需要自定义弹框内容&#xff0c;而且…...

存算一体成为突破算力瓶颈的关键技术?

大模型的训练和推理需要高性能的算力支持。以ChatGPT为例&#xff0c;据估算&#xff0c;在训练方面&#xff0c;1746亿参数的GPT-3模型大约需要375-625台8卡DGX A100服务器训练10天左右&#xff0c;对应A100 GPU数量约3000-5000张。 在推理方面&#xff0c;如果以A100 GPU单卡…...

Pytorch_1_基本语法

一、Pytorch的基本元素操作 1.引入torch from __future__ import print_function import torch 2.创建矩阵 x torch.empty(5,3) print(x) 3.输出结果&#xff1a; tensor([[7.9191e34, 1.1259e24, 1.2359e-42], [4.0824e-40, 1.1379e-35, 2.5353e30], [8.…...

2024上海国际玻璃纤维及新材料展览会

2024上海国际玻璃纤维及新材料展览会 时间&#xff1a;2024年12月18&#xff5e;20日 地点&#xff1a;上海新国际博览中心 ◆ 》》》展会概况&#xff1a; 玻璃纤维是一种性能优异的无机非金属材料&#xff0c;比有机纤维耐温高&#xff0c;不燃&#xff0c;抗腐&#xff…...

云计算项目九:K8S安装

K8S安装 Kube-master安装 按照如下配置准备云主机 防火墙相关配置&#xff1a;禁用selinux&#xff0c;禁用swap&#xff0c;且在firewalld-*。上传kubernetes.zip 到跳板机 配置yum仓库&#xff08;跳板机&#xff09; 跳板机主机配置k8s软件源服务端 [rootjs ~]# yum -y…...

sign加密方法生成

1. 引入包的问题 2. 原因 .pycrypto、pycrytodome和crypto是一个东西&#xff0c;crypto在python上面的名字是pycrypto&#xff0c;它是一个第三方库&#xff0c;但是已经停止更新 3. 解决方法 --直接安装&#xff1a;pip install pycryptodome 3.但是&#xff0c;在使用的时…...

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

如何理解 IP 数据报中的 TTL?

目录 前言理解 前言 面试灵魂一问&#xff1a;说说对 IP 数据报中 TTL 的理解&#xff1f;我们都知道&#xff0c;IP 数据报由首部和数据两部分组成&#xff0c;首部又分为两部分&#xff1a;固定部分和可变部分&#xff0c;共占 20 字节&#xff0c;而即将讨论的 TTL 就位于首…...

return this;返回的是谁

一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请&#xff0c;不同级别的经理有不同的审批权限&#xff1a; // 抽象处理者&#xff1a;审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...

基于IDIG-GAN的小样本电机轴承故障诊断

目录 🔍 核心问题 一、IDIG-GAN模型原理 1. 整体架构 2. 核心创新点 (1) ​梯度归一化(Gradient Normalization)​​ (2) ​判别器梯度间隙正则化(Discriminator Gradient Gap Regularization)​​ (3) ​自注意力机制(Self-Attention)​​ 3. 完整损失函数 二…...

Linux部署私有文件管理系统MinIO

最近需要用到一个文件管理服务&#xff0c;但是又不想花钱&#xff0c;所以就想着自己搭建一个&#xff0c;刚好我们用的一个开源框架已经集成了MinIO&#xff0c;所以就选了这个 我这边对文件服务性能要求不是太高&#xff0c;单机版就可以 安装非常简单&#xff0c;几个命令就…...

从物理机到云原生:全面解析计算虚拟化技术的演进与应用

前言&#xff1a;我的虚拟化技术探索之旅 我最早接触"虚拟机"的概念是从Java开始的——JVM&#xff08;Java Virtual Machine&#xff09;让"一次编写&#xff0c;到处运行"成为可能。这个软件层面的虚拟化让我着迷&#xff0c;但直到后来接触VMware和Doc…...

Easy Excel

Easy Excel 一、依赖引入二、基本使用1. 定义实体类&#xff08;导入/导出共用&#xff09;2. 写 Excel3. 读 Excel 三、常用注解说明&#xff08;完整列表&#xff09;四、进阶&#xff1a;自定义转换器&#xff08;Converter&#xff09; 其它自定义转换器没生效 Easy Excel在…...

ffmpeg(三):处理原始数据命令

FFmpeg 可以直接处理原始音频和视频数据&#xff08;Raw PCM、YUV 等&#xff09;&#xff0c;常见场景包括&#xff1a; 将原始 YUV 图像编码为 H.264 视频将 PCM 音频编码为 AAC 或 MP3对原始音视频数据进行封装&#xff08;如封装为 MP4、TS&#xff09; 处理原始 YUV 视频…...

自定义线程池1.2

自定义线程池 1.2 1. 简介 上次我们实现了 1.1 版本&#xff0c;将线程池中的线程数量交给使用者决定&#xff0c;并且将线程的创建延迟到任务提交的时候&#xff0c;在本文中我们将对这个版本进行如下的优化&#xff1a; 在新建线程时交给线程一个任务。让线程在某种情况下…...

iOS 项目怎么构建稳定性保障机制?一次系统性防错经验分享(含 KeyMob 工具应用)

崩溃、内存飙升、后台任务未释放、页面卡顿、日志丢失——稳定性问题&#xff0c;不一定会立刻崩&#xff0c;但一旦积累&#xff0c;就是“上线后救不回来的代价”。 稳定性保障不是某个工具的功能&#xff0c;而是一套贯穿开发、测试、上线全流程的“观测分析防范”机制。 …...