目录
一、协议定制
1.再谈协议
2.认识序列化与反序列化
二、网络计算器
1.序列化与反序列化头文件
2.服务端
3.客户端
4.makefile
5.运行结果
三、json序列化与反序列化
1.常用序列化与反序列化库
2.安装
3.使用
(1)Request的序列化
(2)Request的反序列化
协议本质是一种“约定”,在前面的TCP和UDP网络通信中,读写数据的时候都是按照"字符串"的形式发送和接收的,那我们如果不发送字符串,而是要传送一些结构化的数据怎么办呢?
比如说,我们观察QQ或微信的聊天窗口,屏幕上的信息包括头像(url)、时间、昵称、消息等。
用户发送的消息虽然大部分是字符串,但是经过用户层(QQ或微信软件)处理后,还需要增加头像,时间,昵称等信息。如果我们将这几个信息看做多个字符串,那么就可以将这多个字符串形成一个结构化的数据,比如下面示例的struct message。
此时网络中发送的就不再只是一个字符串,而是像类变量这样的复杂结构。
struct message
{
string url;
string time;
string nickname;
string msg;
};
序列化:将任意类型的数据或者数据结构转换成一个字符串。
由于系统调用接口的限制,在发送这样的结构体之前,必须将结构化的数据序列化,然后才能通过socket发送到网络中。
比如用户用微信A向用户B发送信息,微信就创建了一个上面的message结构体。序列化可将所有成员合并成一个字符串报文,网络再将序列化后的数据发送给用户B。
反序列化:将一个字符串中不同信息类型的字串提取出来,并且还原回结构化类型的数据。
用户B接收到一个字符串报文,然后用户B的应用层(微信软件)将接收到的报文反序列化,还原回原来结构化的message结构体,此时微信就能将对应数据显示在不同位置,用户B就可以看到这些信息。
对于面向字节流的TCP协议,业务结构数据在发送到网络中的时候,需要先序列化。接收方收到的是序列化后的字节流,要先进行反序列化才能使用。
由于UDP协议面向数据报,所以无需进行序列化以及反序列化。
在微信聊天的过程中,用户A发送的与用户B接受的都是同一种数据结构,所以用户A构建的message是按照特定的成员顺序组成的,用户B就必须按照这样的成员顺序去使用它接收到的message。
所以我们可以说,这样的通信“约定”就是一个用户层的协议,者个协议的载体就是message结构体。
我们使用之前写过的tcp通信客户端和服务端的多进程版本实现这个网络计算器。
数据的处理与流转过程大致如下:
用户输入计算式(比如:1+1)->构造一个Request变量->序列化为字符串"a op b"
->添加报头转化为报文"content_len"\r\n"a op b"\r\n->将报文发给服务端
服务端接收报文->去掉报头得到正文"a op b"->反序列化为Request变量->服务端使用计算函数处理Request变量得到Response变量->Response变量序列化为字符串"exitcode result"->添加报头转化为报文"content_len"\r\n"exitcode result"\r\n->将报文发给客户端
客户端接收报文->去掉报头得到正文"exitcode result"->反序列化为Response变量->打印Response变量的内容。
下面的代码中有详细的注释介绍
protocol.hpp
#pragma once
#define MYSELF
#include
#include
#include
#include
#include
#include
#define SEP " "
#define SEP_LEN strlen(SEP) //使用sizeof()多了一个\0
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) //使用sizeof()多了一个\0
enum
{
OK = 0,
DIV_ZERO,
MOD_ZERO,
OP_ERROR
};
//"content" 转化为 "content_len"\r\n"content"\r\n
//加上报头
std::string enlength(const std::string &text)
{
//按"content_len"\r\n"content"\r\n拼接字符串
std::string send_string = std::to_string(text.size());
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
return send_string;
}
//"content_len"\r\n"content"\r\n 转化为 "content"
//去掉报头
bool delength(const std::string &package, std::string *text)
{
//查找左侧的\r\n
auto pos = package.find(LINE_SEP);
if (pos == std::string::npos)
return false;//没找到表明数据有问题
//截取正文长度的字符串并转为整形
std::string text_len_string = package.substr(0, pos);
int text_len = std::stoi(text_len_string);
//截取正文放入储存正文的string里
*text = package.substr(pos + LINE_SEP_LEN, text_len);
return true;
}
class Request
{
public:
Request()
:_a(0)
,_b(0)
,_op(0)
{}
Request(int a, int b, char op)
:_a(a)
,_b(b)
,_op(op)
{}
//Request序列化
//Request结构体转化为字符串"a op b"
bool serialize(std::string *out)
{
out->clear();//清空
//将变量转为字符串
std::string a_string = std::to_string(_a);
std::string b_string = std::to_string(_b);
*out = a_string;
*out += SEP;
*out += _op;
*out += SEP;
*out += b_string;
return true;
}
//Request反序列化
//字符串"a op b"转化为Request结构体
bool deserialize(const std::string &in)
{
auto left = in.find(SEP);//查找左侧的SEP
auto right = in.rfind(SEP);//查找右侧的SEP
if (left == std::string::npos || right == std::string::npos)
return false;//找不到,数据有问题
if (left == right)
return false;//只有一个SEP,数据有问题
if ((right - 1) != (left + SEP_LEN))//在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
return false;//指向的不是一个位置,数据有问题
//按左闭右开的方式构造两个数字
std::string a_string = in.substr(0, left);
std::string b_string = in.substr(right + SEP_LEN);
//读取到的数字不能为空
if (a_string.empty() || b_string.empty())
return false;
//填入数据
_a = std::stoi(a_string);
_b = std::stoi(b_string);
_op = in[left + SEP_LEN];
return true;
}
public:
int _a;
int _b;
char _op;
};
class Response
{
public:
Response()
:_exitcode(0)
,_result(0)
{}
Response(int exitcode, int result)
:_exitcode(exitcode)
,_result(result)
{}
//Response序列化
//Response结构体转化为字符串"exitcode result"
bool serialize(std::string *out)
{
out->clear();//清空
//将变量转为字符串
std::string ec_string = std::to_string(_exitcode);
std::string res_string = std::to_string(_result);
//拼接字符串
*out = ec_string;
*out += SEP;
*out += res_string;
return true;
}
//Response反序列化
//字符串"exitcode result"转化为Response结构体
bool deserialize(const std::string &in)
{
auto mid = in.find(SEP);//查找中间的SEP
if (mid == std::string::npos)
return false;//找不到,出错
//按左闭右开的方式构造两个数字
std::string ec_string = in.substr(0, mid);
std::string res_string = in.substr(mid + SEP_LEN);
//读取到的退出码和计算结果不能为空
if (ec_string.empty() || res_string.empty())
return false;
//填入数据
_exitcode = std::stoi(ec_string);
_result = std::stoi(res_string);
return true;
}
public:
int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
int _result; // 计算结果
};
//接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvpackage(int sock, std::string &inbuffer, std::string *text)
{
char buffer[1024];//接收网络数据的字符串
while (true)
{
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);//接收数据
if (n > 0)//读到了数据
{
buffer[n] = 0;
inbuffer += buffer;//以追加的方式放入缓冲区ibuffer
auto pos = inbuffer.find(LINE_SEP);//查找LINE_SEP,就是\r\n
if (pos == std::string::npos)
//如果没有找到LINE_SEP,就说明报文"content_len"的部分可能没有读全
continue;//接着到最开始读数据直到读全
//执行至此时,报文"content_len"\r\n这部分一定读全了
//可以将"content_len"的部分取出来并转化为整形
std::string text_len_string = inbuffer.substr(0, pos);
int text_len = std::stoi(text_len_string);
//我们现在知道了报文中正文部分的长度,所以就能确定我们读到的是否是一个完整的报文
//一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
//所以,只要缓冲区ibuffer的内容多于一个报文的字符个数,我们就必定保证ibuffer中已经存在了一个完整的报头;
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;
if (inbuffer.size() < total_len)
{
std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
continue;//报文没读全继续到上面读取
}
//此时缓冲区中就至少有一个完整的报文
*text = inbuffer.substr(0, total_len);//将一个完整报文构建子串放入输出参数中
inbuffer.erase(0, total_len);//删除缓冲区中的完整报文
std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
break;//准备处理报文,跳出循环
}
else//没读到数据
return false;
}
return true;
}
server.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"log.hpp"
#include"protocol.hpp"
using namespace std;
enum errorcode
{
USAGE_ERROR = 1,
SOCKET_ERROR,
BIND_ERROR,
LISTEN_ERROR
};
static const uint16_t given_port = 8080;
static const int given_backlog = 5;
typedef function func_t;
void handler_enter(func_t func, int sock)
{
std::string inbuffer;//缓冲区
while (true)
{
//第一步,读取一个完整的"content_len"\r\n"a op b"\r\n字符串请求
std::string req_text;//原数据
std::string req_str;//正文
//使用recvPackage接收数据并将一个完整的请求放入req_text
if (!recvpackage(sock, inbuffer, &req_text))
return;//函数执行错误,退出
//打印该请求,让我们看到执行流程
std::cout << "未去掉报头的请求:\n" << req_text << std::endl;
//将字符串转化为格式化数据保存在req_text内
if (!delength(req_text, &req_str))
return;//函数执行错误,退出
//打印,让我们看到执行流程
std::cout << "已去掉报头的正文:\n" << req_str << std::endl;
//第二步,对Request进行反序列化
Request req;
if (!req.deserialize(req_str))//将正文信息填入req内
return;//函数执行错误,退出
//第三步,利用func函数处理请求结构体
Response resp;
func(req, resp);//将req的处理结果放入resp
//第四步,对Response进行序列化
std::string resp_str;
resp.serialize(&resp_str);//将序列化后的结构题放入resp_str
std::cout << "计算完成, 序列化后的信息为:\n" << resp_str << std::endl;
//将序列化的Response加上报头并发回客户端
std::string send_string = enlength(resp_str);//加报头
std::cout << "构建完成完整的响应\n" << send_string << std::endl;//打印加上报头后的信息
send(sock, send_string.c_str(), send_string.size(), 0);//发回
}
}
class tcpserver
{
public:
//构造函数
tcpserver(const uint16_t& port = given_port)
:_port(port)
,_listensock(-1)
{}
//初始化服务端进程
void initserver()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if(_listensock < 0)//创建套接字失败打印错误原因
{
logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
exit(SOCKET_ERROR);//退出
}
logmessage(NORMAL, "create socket success:%d", _listensock);//创建套接字成功,打印让用户观察到
struct sockaddr_in local;//储存本地网络信息
local.sin_family = AF_INET;//通信方式为网络通信
local.sin_port = htons(_port);//将网络字节序的端口号填入
local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏
if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息
{
logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误
exit(BIND_ERROR);//退出
}
logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到
//_listensock用于监听,不是用于通信的端口号
//listen函数可设置socket为监听模式
if(listen(_listensock, given_backlog) < 0) // 第二个参数backlog后面在填这个坑
{
logmessage(FATAL, "listen socket error");
exit(LISTEN_ERROR);
}
logmessage(NORMAL, "listen socket success");
}
//启动服务端进程,多进程版本
void start(func_t func)
{
while(1)
{
struct sockaddr_in peer;//储存本地网络信息
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
//如果没有客户端连接服务端,则accept会阻塞等待新连接
//如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sock
if(sock < 0)
{
logmessage(ERROR, "accept fail");//接收新文件描述符失败
continue;//重新回到头接收
}
logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
//创建子进程
pid_t id = fork();
if(id == 0)
{
//由于子进程也继承了父进程的监听套接字,而监听套接字只需要一个,所以需要关闭
close(_listensock);
if(fork() > 0)
{
//子进程再次创建子进程,fork对父进程(当前是服务器的子进程)返回子进程pid,对子进程(当前是服务器的孙子进程)返回0
exit(0);//服务器子进程退出
}
//服务器的孙子进程执行IO处理
//不断处理IO任务
handler_enter(func, sock);
//退出该函数时,客户端已经退出,需要将进行网络传输的文件描述符释放,否则会引起资源泄露
close(sock);
//退出孙子进程,由于它是孤儿进程,所以会被操作系统自行回收
exit(0);
}
}
}
~tcpserver()
{}
private:
uint16_t _port;//服务端进程的端口号
int _listensock;//监听文件描述符
};
server.cc
#include"server.hpp"
#include
#include
#include
//计算器函数
const string ops = "+-*/%";
bool calculate(const Request &req, Response &resp)
{
// req已经有结构化完成的数据啦,你可以直接使用
resp._exitcode = OK;
resp._result = 0;
switch (req._op)
{
case '+':
resp._result = req._a + req._b;
break;
case '-':
resp._result = req._a - req._b;
break;
case '*':
resp._result = req._a * req._b;
break;
case '/':
{
if (req._b == 0)
resp._exitcode = DIV_ZERO;
else
resp._result = req._a / req._b;
}
break;
case '%':
{
if (req._b == 0)
resp._exitcode = MOD_ZERO;
else
resp._result = req._a % req._b;
}
break;
default:
resp._exitcode = OP_ERROR;
break;
}
return true;
}
static void Usage(string proc)
{
printf("\nUsage:\n\t%s local_port\n\n",proc.c_str());
}
int main(int argc, char* argv[])
{
if(argc != 2)//如果没输入端口号,argc保存的命令参数只有一个,进程出错
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
uint16_t port = atoi(argv[1]);
unique_ptr p(new tcpserver(port));
p->initserver();
p->start(calculate);
return 0;
}
client.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"log.hpp"
#include"protocol.hpp"
#define NUM 1024
enum errorcode
{
USAGE_ERROR = 1,
SOCKET_ERROR,
BIND_ERROR,
CONNECT_ERROR
};
Request ParseLine(const std::string &line);
class tcpclient
{
public:
//构造函数
tcpclient(const std::string& ip, const uint16_t& port)
:_ip(ip)
,_port(port)
,_sock(-1)
{}
void initclient()
{
//创建套接字,创建失败打印错误原因
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock == -1)
{
logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
exit(SOCKET_ERROR);//退出
}
logmessage(NORMAL, "create socket success:%d", _sock);//创建套接字成功,打印让用户观察到
//客户端不需要显式绑定,该工作交给操作系统完成
}
//启动客户端进程
void run()
{
struct sockaddr_in local;
local.sin_family = AF_INET;//通信方式为网络通信
local.sin_port = htons(_port);//将网络字节序的端口号填入
local.sin_addr.s_addr = inet_addr(_ip.c_str());//填充结构体
//客户端连接服务器
if(connect(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)//客户端与服务器进行连接
{
logmessage(FATAL, "connect error");//connect失败属于最严重的错误
}
else
{
std::string line;
std::string inbuffer;
while (true)
{
std::cout << "mycal>>> ";
std::getline(std::cin, line); // 1+1
Request req = ParseLine(line); // "1+1"
std::string content;
req.serialize(&content);
std::string send_string = enlength(content);
std::cout << "sendstring:\n" << send_string << std::endl;
send(_sock, send_string.c_str(), send_string.size(), 0); // bug?? 不管
std::string package, text;
// "content_len"\r\n"exitcode result"\r\n
if (!recvpackage(_sock, inbuffer, &package))
continue;
if (!delength(package, &text))
continue;
// "exitcode result"
Response resp;
resp.deserialize(text);
std::cout << "exitCode: " << resp._exitcode << std::endl;
std::cout << "result: " << resp._result << std::endl;
}
}
}
Request ParseLine(const std::string &line)
{
// 建议版本的状态机!
//"1+1" "123*456" "12/0"
int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
int i = 0;
int cnt = line.size();
std::string left, right;
char op;
while (i < cnt)
{
switch (status)
{
case 0:
{
if(!isdigit(line[i]))
{
op = line[i];
status = 1;
}
else left.push_back(line[i++]);
}
break;
case 1:
i++;
status = 2;
break;
case 2:
right.push_back(line[i++]);
break;
}
}
std::cout << std::stoi(left)<<" " << std::stoi(right) << " " << op << std::endl;
return Request(std::stoi(left), std::stoi(right), op);
}
//析构函数要释放不使用的文件描述符
~tcpclient()
{
if( _sock >= 0)
close(_sock);
}
private:
int _sock;//套接字文件描述符
std::string _ip;//服务器IP地址
uint16_t _port;//服务器的端口号
};
client.cc
#include"client.hpp"
using namespace std;
static void Usage(string proc)
{
printf("\nUsage:\n\t%s server_ip server_port\n\n", proc.c_str());
}
int main(int argc, char* argv[])
{
if(argc != 3)//如果没输入端口号和目的IP,argc保存的命令参数就不是三个,进程出错
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
uint16_t port = atoi(argv[2]);
string ip = argv[1];
unique_ptr p(new tcpclient(ip, port));
p->initclient();
p->run();
return 0;
}
.PHONY:
all:server client
server:server.cc
g++ server.cc -o server -std=c++11 -ljsoncpp
client:client.cc
g++ client.cc -o client -std=c++11 -ljsoncpp
.PHONY:
clean:
rm -f server client
对于正确的计算式,也能返回0作为退出码,指示结果有效。
对于不正确的计算式,也能返回非0退出码,指示结果无效。
上面的序列化和反序列化都是我们自己通过处理字符串实现的。其实这部分的工作早就有人写好了现成的库,我们直接使用它们对数据进行序列化和反序列化即可。我们以后代码中的序列化和反序列化,绝对不要自己写。
常见的有以下三个:
json——使用简单。
protobuf——比较复杂,局域网或者本地网络通信使用较多。
xml——适合在其他编程语言中使用(如Java等)。
这里只介绍使用最简单也最广泛的json的使用,学有余力可以去了解下protobuf。
所以,都说到这里了,序列化反序列化与协议又有和关系呢?
序列化与反序列化可以理解为,为了传输结构体这样的结构将原数据转化为字符串方便传输。
而协议是网络通信双方约定以某种数据结构或者机制进行数据的解读和包装。
二者并不相同,协议可以自己定制,序列化一般使用别人写好的库。二者也都是组成程序的重要部分。
在使用json之前,需要先在Linux机器上使用yum安装json工具
语句:sudo yum install -y jsoncpp-devel
如上图所示就是安装成功了。(我直接以root身份安装的,不用sudo)
json安装后,它的头文件json.h所在路径为/usr/include/jsoncpp/json/。而由于编译器查找头文件的目录仅限usr/include,所以在包含头文件需要加路径jsoncpp/json/json.h。
json是一个动态库,它所在路径为/lib64/,完整的名字为libjsoncpp.sp。在使用的时候,编译器会自动到/lib64路径下查找所用的库,但编译需要指定库名,也需要加上-ljonscpp。
还可以在makefile中增加一个宏-DMYSELF,实现条件编译,做到对序列化与反序列化的自我实现函数和json库实现的切换。
Json数据的格式是采用键值对的形式,key为字符串,value为需要存储的变量如:
"first" : a、"second" : b、"oper" : op、"exitcode" : exitcode、"result" : result
序列化就是将这些键值对拼接在一起形成一个字符串。
反序列化就是将多个字符串拆开,用户根据键值对的对应关系可以找到绑定的变量。
当然这仅仅是一个很粗糙的理解,它内部的实现很复杂,我们只需要知道如何使用Json这个工具即可。
我们使用Json对Request和Response进行序列化和反序列化写出一个新的版本,并利用条件编译实现其切换。
我以Request的序列化和反序列化为例:
Json::Value root创建的Value类型对象root可以看作一个万能对象。
使用root["字符串"] = 变量;的形式将不同类型的变量和字符串绑定形成键值对。
然后创建一个Json::FastWriter类型的对象writer,使用write函数对root进行序列化,返回的结果为string类型,赋值给*out。
首先,依旧要创建一个万能对象root,然后再创建一个Json::Reader类型的对象reader。
调用对象reader的成员函数parse()将字符串in反序列化并将得到的键值对再放入到万能对象root中。
根据键值对中的key值("first"等字符串)使用[]就能找到value。由于反序列化后的value依旧为字符串,所以需要使用asInt()转化成int类型,此时才算彻底完成了反序列化。
再使用Json对Response进行序列化和反序列化的过程除了成员变量的数目和属性不同之外,其他并没有什么不同,代码就都放在下面了。
#pragma once
#define MYSELF
#include
#include
#include
#include
#include
#include
#define SEP " "
#define SEP_LEN strlen(SEP) //使用sizeof()多了一个\0
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) //使用sizeof()多了一个\0
enum
{
OK = 0,
DIV_ZERO,
MOD_ZERO,
OP_ERROR
};
//"content" 转化为 "content_len"\r\n"content"\r\n
//加上报头
std::string enlength(const std::string &text)
{
//按"content_len"\r\n"content"\r\n拼接字符串
std::string send_string = std::to_string(text.size());
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
return send_string;
}
//"content_len"\r\n"content"\r\n 转化为 "content"
//去掉报头
bool delength(const std::string &package, std::string *text)
{
//查找左侧的\r\n
auto pos = package.find(LINE_SEP);
if (pos == std::string::npos)
return false;//没找到表明数据有问题
//截取正文长度的字符串并转为整形
std::string text_len_string = package.substr(0, pos);
int text_len = std::stoi(text_len_string);
//截取正文放入储存正文的string里
*text = package.substr(pos + LINE_SEP_LEN, text_len);
return true;
}
class Request
{
public:
Request()
:_a(0)
,_b(0)
,_op(0)
{}
Request(int a, int b, char op)
:_a(a)
,_b(b)
,_op(op)
{}
//Request序列化
//Request结构体转化为字符串"a op b"
bool serialize(std::string *out)
{
#ifdef MYSELF
out->clear();//清空
//将变量转为字符串
std::string a_string = std::to_string(_a);
std::string b_string = std::to_string(_b);
*out = a_string;
*out += SEP;
*out += _op;
*out += SEP;
*out += b_string;
#else
Json::Value root;
root["first"] = _a;
root["second"] = _b;
root["oper"] = _op;
Json::FastWriter writer;
// Json::StyledWriter writer;
*out = writer.write(root);
#endif
return true;
}
//Request反序列化
//字符串"a op b"转化为Request结构体
bool deserialize(const std::string &in)
{
#ifdef MYSELF
auto left = in.find(SEP);//查找左侧的SEP
auto right = in.rfind(SEP);//查找右侧的SEP
if (left == std::string::npos || right == std::string::npos)
return false;//找不到,数据有问题
if (left == right)
return false;//只有一个SEP,数据有问题
if ((right - 1) != (left + SEP_LEN))//在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
return false;//指向的不是一个位置,数据有问题
//按左闭右开的方式构造两个数字
std::string a_string = in.substr(0, left);
std::string b_string = in.substr(right + SEP_LEN);
//读取到的数字不能为空
if (a_string.empty() || b_string.empty())
return false;
//填入数据
_a = std::stoi(a_string);
_b = std::stoi(b_string);
_op = in[left + SEP_LEN];
#else
Json::Value root;
Json::Reader reader;
reader.parse(in, root);
_a = root["first"].asInt();
_b = root["second"].asInt();
_op = root["oper"].asInt();
#endif
return true;
}
public:
int _a;
int _b;
char _op;
};
class Response
{
public:
Response()
:_exitcode(0)
,_result(0)
{}
Response(int exitcode, int result)
:_exitcode(exitcode)
,_result(result)
{}
//Response序列化
//Response结构体转化为字符串"exitcode result"
bool serialize(std::string *out)
{
#ifdef MYSELF
out->clear();//清空
//将变量转为字符串
std::string ec_string = std::to_string(_exitcode);
std::string res_string = std::to_string(_result);
//拼接字符串
*out = ec_string;
*out += SEP;
*out += res_string;
#else
Json::Value root;
root["exitcode"] = _exitcode;
root["result"] = _result;
Json::FastWriter writer;
*out = writer.write(root);
#endif
return true;
}
//Response反序列化
//字符串"exitcode result"转化为Response结构体
bool deserialize(const std::string &in)
{
#ifdef MYSELF
auto mid = in.find(SEP);//查找中间的SEP
if (mid == std::string::npos)
return false;//找不到,出错
//按左闭右开的方式构造两个数字
std::string ec_string = in.substr(0, mid);
std::string res_string = in.substr(mid + SEP_LEN);
//读取到的退出码和计算结果不能为空
if (ec_string.empty() || res_string.empty())
return false;
//填入数据
_exitcode = std::stoi(ec_string);
_result = std::stoi(res_string);
#else
Json::Value root;
Json::Reader reader;
reader.parse(in, root);
_exitcode = root["exitcode"].asInt();
_result = root["result"].asInt();
#endif
return true;
}
public:
int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
int _result; // 计算结果
};
//接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvpackage(int sock, std::string &inbuffer, std::string *text)
{
char buffer[1024];//接收网络数据的字符串
while (true)
{
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);//接收数据
if (n > 0)//读到了数据
{
buffer[n] = 0;
inbuffer += buffer;//以追加的方式放入缓冲区ibuffer
auto pos = inbuffer.find(LINE_SEP);//查找LINE_SEP,就是\r\n
if (pos == std::string::npos)
//如果没有找到LINE_SEP,就说明报文"content_len"的部分可能没有读全
continue;//接着到最开始读数据直到读全
//执行至此时,报文"content_len"\r\n这部分一定读全了
//可以将"content_len"的部分取出来并转化为整形
std::string text_len_string = inbuffer.substr(0, pos);
int text_len = std::stoi(text_len_string);
//我们现在知道了报文中正文部分的长度,所以就能确定我们读到的是否是一个完整的报文
//一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
//所以,只要缓冲区ibuffer的内容多于一个报文的字符个数,我们就必定保证ibuffer中已经存在了一个完整的报头;
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;
if (inbuffer.size() < total_len)
{
std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
continue;//报文没读全继续到上面读取
}
//此时缓冲区中就至少有一个完整的报文
*text = inbuffer.substr(0, total_len);//将一个完整报文构建子串放入输出参数中
inbuffer.erase(0, total_len);//删除缓冲区中的完整报文
std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
break;//准备处理报文,跳出循环
}
else//没读到数据
return false;
}
return true;
}
上面我们使用了条件编译,如果MYSELF被定义,就是用我们自己写的序列化和反序列化。
如果MYSELF没被定义,就使用json库的定义。