目录
再谈 "协议"
结构化数据的传输
序列化和反序列化
网络版计算器
封装套接字操作
服务端代码
服务进程执行例程
启动网络版服务端
协议定制
客户端代码
代码测试
使用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 10
class 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_;
};
服务端代码
首先,我们需要初始化服务器,这包括三个关键步骤:
- 使用socket函数来创建一个新的套接字。
- 接着,通过bind函数,我们将这个套接字绑定到一个特定的端口号上,这样客户端就可以通过这个端口与服务器建立连接。
- 然后,通过调用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::string
class 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;
else
break;
}
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;
else
resp.code = DivZero;
}
break;
case '%':
{
if (req.y != 0)
resp.result = req.x % req.y;
else
resp.code = ModZero;
}
break;
default:
resp.code = Other_Oper;
break;
}
return resp;
}
// "len"\n"10 + 20"\n
std::string Calculator(std::string &package)
{
std::string content;
bool r = Decode(package, &content); // "len"\n"10 + 20"\n
if (!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 y
int 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"\n
std::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 1
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;
}
// 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;
#else
Json::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 MySelf
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);
y = std::stoi(part_y);
return true;
#else
Json::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 y
int 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;
#else
Json::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 MySelf
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;
#else
Json::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=-ljsoncpp
servercal:ServerCal.cpp
g++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
clientcal:ClientCal.cpp
g++ -o $@ $^ -std=c++11 $(Lib) $(Flag)
.PHONY:clean
clean:
rm -f servercal clientcal
代码测试:
以上我们就成功用Json实现了数据序列化和反序列化。