UDP是面向数据报的,所以不用担心数据。TCP是面向字节流的,如何确定读的数据就是对的?
实际生活中,很多时候都是结构化数据的网络发送,聊天时,头像,昵称,发送时间,内容等等都会发送,之前的代码,发送时都是发送一个字符串,而像这种结构化的数据如何传输?网络会把这些数据变成一条长字符串,发送给对方,对方再转回原来的结构,结构解释成字符串就是序列化,字符串解释成结构就是反序列化。序列化和反序列化是为了方便网络通信。
但对方怎么知道如何转化字符串为结构体?怎么知道这是自己要反序列化的字符串,怎么知道都有哪些成员,以什么顺序来反序列化?协议就是解决这个的,根据协议来进行通信,协议本质是约定好的某种格式的数据,常见的就是用结构体或者类来表达,意思就是客户端定义的数据的结构体,服务端也用一样的结构体对象,这样两方就都知道所有的成员了,也知道如何解释了。之前写的代码是用来的处理新套接字,没有定制协议,序列化和反序列化。
结构体数据其实也可以直接传,但对于两个平台适配性要求太高,没法保证通信稳定。
通过网络计算器代码的实现,来理解协议的自定义和序列反序列化。
本篇gitee
创建文件,TcpClient.hpp,TcpServer.hpp,CalculatorClient.cc,CalculatorServer.cc。两个cc文件对应两个hpp文件,以及Sock.hpp套接字头文件。makefile:
.PHONY:all
all:calserver calclient
calclient:CalculatorClient.cc
g++ -o $@ $^ -std=c++11
calserver:CalculatorServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f calclient calserver
sock.hpp直接写
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include "err.hpp"
#include "log.hpp"
static const int gbacklog = 32;
static const int defaultfd = -1;
class Sock
{
public:
Sock(): _sock(defaultfd)
{}
void Socket()
{
_sock= socket(AF_INET, SOCK_STREAM, 0);
if(_sock < 0)
{
logMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));
exit(SOCKET_ERR);
}
}
void Bind(const 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(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));
exit(BIND_ERR);
}
}
void Listen()
{
if(listen(_sock, gbacklog) < 0)//第二个参数维护了一个队列,发送了连接请求但是服务端没有处理的客户端,服务端开始accept后,就会出现另一个队列,就是服务端接受了请求但还没被accept的客户端
{
logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));
exit(LISTEN_ERR);
}
}
int Accept(std::string* clientip, uint16_t* clientport)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int sock = accept(_sock, (struct sockaddr*)&temp, &len);
if(sock < 0)
{
logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));
}
else
{
*clientip = inet_ntoa(temp.sin_addr);//这个函数就可以从结构体中拿出ip地址,转换好后返回
*clientport = ntohs(temp.sin_port);
}
return sock;
}
int Connect(const std::string& serverip, const uint16_t& serverport)//让别的客户端来连接服务端
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
return connect(_sock, (struct sockaddr*)&server, sizeof(server));//先不打印消息
}
int Fd()
{
return _sock;
}
~Sock()
{
if(_sock != defaultfd) close(_sock);
}
private:
int _sock;
};
CalculatorServer.cc
#include "TcpServer.hpp"
#include
int main()
{
uint16_t port = 8888;
std::unique_ptr tsvr(new TcpServer(port));
tsvr->InitServer();
tsvr->Start();
return 0;
}
TcpServer.hpp
#pragma once
#include
#include
#include "Sock.hpp"
class ThreadData
{
public:
ThreadData(int s, TcpServer* tsvrp): sock(s), tsvrp(p)
{}
~ThreadData(){}
private:
int sock;
TcpServer* tsvrp;
};
class TcpServer
{
public:
TcpServer(uint16_t port): _port(port)
{}
void InitServer()
{
//1. 初始化服务器
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
logMessage(Info, "init server done, listensock: %d", _listensock.Fd());
}
static void* ThreadRoutine(void* args)
{
pthread_detach(pthread_self());
ThreadData* td = static_cast(args);
//强转成ThreadData类型就可以使用类里的函数来完成任务了
delete td;
return nullptr;
}
void Start()
{
for( ; ; )
{
std::string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if(sock < 0) continue;
logMessage(Debug, "get a new client, client info : [%s:%d]", clientip.c_str(), clientport);
pthread_t tid;//创建出来的线程应当让它有任务可做,所以创建一个ThreadData类
ThreadData* td = new ThreadData(sock, this);
pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
}
}
~TcpServer()
{}
public:
uint16_t _port;
Sock _listensock;
};
现在可以make一下看看能否正常运行,不过没写客户端,就用telnet这个接口,写法和之前的客户端一样,会向当前服务端发送连接请求,退出时Ctrl + ],然后按quit就可以了。
这部分写完后,就开始写service函数,服务端提供服务。读取数据,处理数据,需要协议,所以再创建一个Protocol.hpp头文件。先建立一个大概框架。
#pragma once
#include
#include
//给网络版本计算机定制协议
namespace protocol_ns
{
class Request
{
public:
Request() {}
~Request() {}
public:
int _x;
int _y;
char op;
};
class Response
{
public:
Response() {}
~Response() {}
public:
int _result;
int _code;//0表示计算成功,剩余的数字就是各种非法操作的错误码
};
}
服务端和客户端都需要序列化和反序列化,服务端发送服务,客户端得到后处理,然后再给出响应,服务端收到,所以两方都会发结构体和得到字符串。
#pragma once
#include
#include
//给网络版本计算机定制协议
namespace protocol_ns
{
class Request
{
public:
Request() {}//为无参构造而准备的,这样就是一个无参一个有参
Request(int x, int y, char op)
: _x(x), _y(y), _op(op)
{}
bool Serialize(std::string* outstr)//序列化:结构体转字符串
{
}
bool Deserialize(const std::string* instr)//反序列化:字符串转结构体
{
}
~Request() {}
public:
int _x;
int _y;
char _op;
};
class Response
{
public:
Response() {}
Response(int result, int code)
: _result(result), _code(code)
{}
~Response() {}
public:
int _result;
int _code;//0表示计算成功,剩余的数字就是各种非法操作的错误码
};
}
成员变量对应这个计算器要用的变量。写一下服务端使用这个头文件的逻辑。
void ServiceIO(int sock, const std::string& ip, const uint16_t& port)
{
//1. 读取数据
std::string str;
//2. 假设已经拿到一个完整的报文
Request req;
req.Deserialize(str);//反序列化
//3. 直接提取用户的请求数据
x = req._x;
}
逻辑就是这样,再看序列化和反序列化。我们定义字符串应当是这样的:_x op _y,两个操作数,一个计算符,中间有两个空格。不过分隔符可以不是空格。
bool Serialize(std::string* outstr)//序列化:结构体转字符串
{
*outstr = "";
std::string x_string = std::to_string(_x);
std::string y_string = std::to_string(_y);
//手动序列化
*outstr = x_string + SEP + _op + SEP + y_string;
return true;
}
bool Deserialize(const std::string* instr)//反序列化:字符串转结构体
{
std::vector result;
StringSplit(instr, SEP, &result);
_x = toInt(str[0]);
op = str[1][0];//因为是字符,所以只要一个符号即可
_y = toInt(str[2]);
return true;
}
创建一个Util.hpp写自定义的函数
Util.hpp
#pragma once
#include
#include
#include
using namespace std;
class Util
{
public:
static bool StringSplit(const string& str, const string& sep, vector* result)
{
size_t start = 0;
while(start < str.size())
{
auto pos = str.find(sep, start);
if(pos == string::npos) break;
result->push_back(str.substr(start, pos - start));
start = pos + sep.size();
}
if(start < str.size()) result->push_back(str.substr(start));
return true;
}
static int toInt(const string& s)
{
return atoi(s.c_str());
}
};
在Protocol.hpp中
bool Deserialize(const std::string& instr)//反序列化:字符串转结构体
{
std::vector result;
Util::StringSplit(instr, SEP, &result);
if(result.size() != 3) return false;
_x = Util::toInt(result[0]);
_y = Util::toInt(result[2]);
if(result[1].size() == 1) return false;//协议规定
_op = result[1][0];//因为是字符,所以只要一个符号即可
return true;
}
这样的Request类里序列化和反序列都做完了,再写Response的。
bool Serialize(std::string* outstr)
{
*outstr = "";
std::string res_string = std::to_string(_result);
std::string code_string = std::to_string(_code);
//手动序列化
*outstr = res_string + SEP + code_string;
return true;
}
bool Deserialize(const std::string& instr)
{
std::vector result;
Util::StringSplit(instr, SEP, &result);
if(result.size() != 2) return false;
_result = Util::toInt(result[0]);
_code = Util::toInt(result[1]);
return true;
}
回到TcpServer.hpp部分,我们更处理反序列化后的数据,用一个回调函数。
#pragma once
#include
#include
#include //头文件
#include "Sock.hpp"
#include "Protocol.hpp"
namespace tcpserver_ns
{
using namespace protocol_ns;
class TcpServer;
using func_t = std::function(const Request&)>;//所有类的前面加上一个函数的初始化,返回值是Response这个函数的结果,参数类型是const Request&
//......
TcpServer(func_t func, uint16_t port) : _port(port), _func(func)
{}
//......
private:
uint16_t _port;
Sock _listensock;
func_t _func;
处理数据的过程放在外面的文件,也就是CalculatorServer.cc中
#include "TcpServer.hpp"
#include
using namespace tcpserver_ns;
Response calculate(const Request& req)
{
//代码运行到这里,一定保证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._code = 1;
else resp._result = req._x / req._y;
break;
case '%':
if(req._y == 0) resp._code = 2;
else resp._result = req._x / req._y;
break;
default:
resp._code = 3;
break;
}
return resp;
}
int main()
{
uint16_t port = 8888;
std::unique_ptr tsvr(new TcpServer(calculate, port));
tsvr->InitServer();
tsvr->Start();
return 0;
}
回到服务端文件TcpServer.cc中
void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
{
// 1. 读取数据
std::string str;
// 2. 假设已经拿到一个完整的报文
Request req;
req.Deserialize(str); //对读到的request字符串反序列化
// 3. 直接提取用户的请求数据
Response resp = _func(req);//业务逻辑
//_func调用了外部函数,既然是回调,就会把结果给返回回来,给到resp
//4. 给客户端返回响应,对计算完成的response结构序列化
std::string send_string;
resp.Serialize(&send_string);
//5. 发送到网络
}
还有其它部分需要改动
class ThreadData
{
public:
ThreadData(int sock, std::string ip, uint16_t port, TcpServer *tsvrp)
: _sock(sock), _tsvrp(tsvrp), _ip(ip), _port(port)
{}
~ThreadData() {}
public:
int _sock;
std::string _ip;
uint16_t _port;
TcpServer* _tsvrp;
};
//...
static void *ThreadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast*>(args);
// 强转成ThreadData类型就可以使用类里的函数来完成任务了
td->_tsvrp->ServiceIO(td->_sock, td->_ip, td->_port);
delete td;
return nullptr;
}
现在还有两个问题,一个是如何读取数据,一个是如何发送到网络。
我们当然可以用read系统调用,放到一个buffer数组中,然后接下来进行反序列化,业务处理,序列化,发送到网络中,但前提得是这个数据能够做反序列化。
用户层,也就是最上层应用层,发送一个数据,也就是用write时,是把数据放到了网络层TCP的发送缓冲区里,而一端read则是从TCP的接收缓冲区里读数据,数据write到发送缓冲区后,如何发送,何时发送,由TCP决定,所以TCP是传输控制协议。read和write本质是拷贝到对应缓冲区,TCP发送数据是从自己的发送缓冲区到对方的接收缓冲区,本质也是数据的拷贝。一端可以同时接收数据和发送数据,所以TCP协议是全双工的。TCP的工作很多,缓冲区有很多请求和数据,如果不读,那么就会一直积压在那里,那么某一次去读时,就不能保证读到的就是用户所需要的,所以光靠这个是无法读到正确数据的,所以保证这个工作放在了协议那里,由协议来管理数据,让用户能拿到正确的数据。
所以Protocol.hpp需要改。 假设报文是这样的: “7”\r\n"10 + 20"\r\n,这就相当于报头 + 有效载荷,所以请求/响应 = 报头\r\n有效载荷\r\n,只是请求和响应的有效载荷不同。
//给网络版本计算机定制协议
namespace protocol_ns
{
#define SEP " "
#define SEP_LEN strlen(SEP)//不能用sizeof
#define HEADER_SEP "\r\n"
#define HEADER_SEP_LEN strlen("\r\n")
//假设报文是这样的: "7"\r\n"10 + 20"\r\n,这就相当于报头 + 有效载荷
//请求/响应 = 报头\r\n有效载荷\r\n,只是请求和响应的有效载荷不同
std::string AddHeader(const std::string& str)
{
std::string s = std::to_string(str.size());
s += HEADER_SEP;
s += str;
s += HEADER_SEP;
return s;
}
报头添加完成了,但读取方如何确定读取的是一个正确的,按照协议规定的数据?我们先写好一个逻辑,之后还会改
void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
{
// 1. 正确地读取字符串报文
std::string inbuffer;
std::string package;
if(ReadPackage(sock, inbuffer, &package))
{
;
}
// 2. 拿到有效载荷
package = RemoveHeader(package);
// 3. 拿到一个完整的报文
Request req;
req.Deserialize(package); //对读到的request字符串反序列化
// 4. 直接提取用户的请求数据
Response resp = _func(req);//业务逻辑
//_func调用了外部函数,既然是回调,就会把结果给返回回来,给到resp
// 5. 给客户端返回响应,对计算完成的response结构序列化
std::string send_string;
resp.Serialize(&send_string);
// 6. 发送到网络
}
写读取报文的函数
int ReadPackage(int sock, std::string& inbuffer, std::string* package)
{
//读取 ———— 字符串"7"\r\n"10 + 20"\r\n
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer - 1), 0);
if(s <= 0) return -1;
buffer[s] = 0;
inbuffer += buffer;//此时inbuffer里就有了这样的字符串: "7"\r\n"10 + 20"\r\n
//分析
auto pos = inbuffer.find(HEADER_SEP);
if(pos == std::string::npos) return 0;//没找到\r\n那么就不是正确的字符串,不动inbuffer里的内容,退出
std::string lenStr = inbuffer.substr(0, pos);//获取头部字符串
int len = Util::toInt(lenStr);//得到了长度7,也就是有效载荷长度
int targetPackagelen = lenStr.size() + len + 2 * HEADER_SEP_LEN;//接收到的有报文的字符串长度就是这个
if(inbuffer.size() < targetPackagelen) return 0;
*package = inbuffer.substr(0, targetPackagelen);//package保存了"7"\r\n"10 + 20"\r\n,去掉其它符号的工作交给RemoveHeader
inbuffer.erase(0, targetPackagelen);//只有到这里才改变inbuffer里的内容
return len;//len就是有效载荷的长度
}
那么TcpServer.hpp那边
void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
{
std::string inbuffer;
while (true)
{
// 1. 正确地读取字符串报文
std::string package;
int n = ReadPackage(sock, inbuffer, &package);
if (n == -1) break;
else if (n == 0) continue;
else
{
// 2. 已有字符串"7"\r\n"10 + 20"\r\n,希望得到有效载荷"10 + 20"
package = RemoveHeader(package, n);
// 3. 拿到一个完整的报文
Request req;
req.Deserialize(package); // 对读到的request字符串反序列化
// 4. 直接提取用户的请求数据
Response resp = _func(req); // 业务逻辑
//_func调用了外部函数,既然是回调,就会把结果给返回回来,给到resp
// 5. 给客户端返回响应,对计算完成的response结构序列化
std::string send_string;
resp.Serialize(&send_string);
// 6. 发送到网络
}
}
close(sock);
}
接下来写去除报头的函数
std::string RemoveHeader(const std::string& str, int len)
{
std::string res = str.substr(str.size() - HEADER_SEP_LEN - len, len);
return res;
}
那么服务端那里package = RemoveHeader(package, n)就得到了有效载荷,然后进行反序列化,业务处理,序列化,再发送到网络。
void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
{
std::string inbuffer;
while (true)
{
// 1. 正确地读取字符串报文
std::string package;
int n = ReadPackage(sock, inbuffer, &package);
if (n == -1) break;
else if (n == 0) continue;
else
{
// 2. 已有字符串"7"\r\n"10 + 20"\r\n,希望得到有效载荷"10 + 20"
package = RemoveHeader(package, n);
// 3. 拿到一个完整的报文
Request req;
req.Deserialize(package); // 对读到的request字符串反序列化
// 4. 直接提取用户的请求数据
Response resp = _func(req); // 业务逻辑
//_func调用了外部函数,既然是回调,就会把结果给返回回来,给到resp
// 5. 给客户端返回响应,对计算完成的response结构序列化
std::string send_string;
resp.Serialize(&send_string);
// 6. 发送到网络
send_string = AddHeader(send_string);
send(sock, send_string.c_str(), send_string.size(), 0);
}
}
close(sock);
}
客户端有两个文件,TcpClient.hpp和CalculatorClient.cc两个文件。为了方便,Sock.hpp和TcpServer.hpp这样更改
void Close()
{
if(_sock != defaultfd) close(_sock);
}
~Sock()
{}
~TcpServer()
{
_listensock.Close();
}
private:
uint16_t _port;
Sock _listensock;
func_t _func;
CalculatorClient.cc这样写
#include "TcpClient.hpp"
#include "Sock.hpp"
#include
#include "Protocol.hpp"
using namespace protocol_ns;
static 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(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
Sock sock;
sock.Socket();
int n = sock.Connect(serverip, serverport);
if(n != 0) return 1;
std::string buffer;
while(true)
{
Request req;//用户输入,填充req的三个成员变量
//1. 序列化
std::string sendString;
req.Serialize(&sendString);
//2. 添加报头
sendString = AddHeader(sendString);
//3. 发送数据
send(sock.Fd(), sendString.c_str(), sendString.size(), 0);
//4. 获取响应
std::string package;
int n = 0;
START:
n = ReadPackage(sock.Fd(), buffer, &package);
if (n == 0) goto START;
else if (n < 0) break;
// 5. 去掉报头
package = RemoveHeader(package, n);
// 6. 反序列化
Response resp;
resp.Deserialize(package);
std::cout << "result: " << resp._result << "[code: " << resp._code << "]" << std::endl;
}
sock.Close();
return 0;
}
现在就可以正常运行了,不过还剩一个输入内容的问题没解决,我们定义一个函数,返回结果给req,返回类型就是Request。
#include "TcpClient.hpp"
#include "Sock.hpp"
#include
#include "Protocol.hpp"
using namespace protocol_ns;
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}
enum
{
Left,
Oper,
Right
};
Request ParseLine(std::string& line)
{
std::string left, right;
char op;
int status = Left;
int i = 0;
while(i < line.size())
{
switch(status)
{
case Left:
if(isdigit(line[i])) left.push_back(line[i++]);
else status = Oper;
break;
case Oper:
op = line[i++];
status = Right;
break;
case Right:
if(isdigit(line[i])) right.push_back(line[i++]);
break;
}
}
Request req;
std::cout << "left: " << left << std::endl;
std::cout << "right: " << right << std::endl;
std::cout << "op: " << op << std::endl;
req._x = std::stoi(left);
req._y = std::stoi(right);
req._op = op;
return req;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
Sock sock;
sock.Socket();
int n = sock.Connect(serverip, serverport);
if(n != 0) return 1;
std::string buffer;
while(true)
{
std::cout << "Enter# " << std::endl;//希望用户这样输入就行:1+1, 1*1
std::string line;
std::getline(std::cin, line);
Request req = ParseLine(line);//用户输入,填充req的三个成员变量
std::cout << "test: " << req._x << req._op << req._y << std::endl;
//1. 序列化
std::string sendString;
req.Serialize(&sendString);
//2. 添加报头
sendString = AddHeader(sendString);
//3. 发送数据
send(sock.Fd(), sendString.c_str(), sendString.size(), 0);
//4. 获取响应
std::string package;
int n = 0;
START:
n = ReadPackage(sock.Fd(), buffer, &package);
if (n == 0) goto START;
else if (n < 0) break;
// 5. 去掉报头
package = RemoveHeader(package, n);
// 6. 反序列化
Response resp;
resp.Deserialize(package);
std::cout << "result: " << resp._result << "[code: " << resp._code << "]" << std::endl;
}
sock.Close();
return 0;
}
这样整体就写完了
上面是我们在自己写序列化反序列化,是为了学习它的具体工作。实际应用中,有很多成熟的方案,通常都是用这些,而不是自己写。
protobuf,json,xml。这篇博客用json来写,json把结构化数据转化成kv的数据格式,这篇用一个比较简单的方式来使用json。
修改Protocol.hpp,用条件编译,如果定义了一个宏,那就用自己的方法,没有就用别的方法。不过在使用json之前,需要先安装
yum install -y jsoncpp-devel
安装好后,ls /usr/include/jsoncpp/命令就会查看到json这个目录。头文件是jsoncpp/json/json.h,这是一条路径,json目录我们只需要json.h头文件就可以。详细解析在代码中。
//#define MYSELF 1
//Request
bool Serialize(std::string* outstr)//序列化:结构体转字符串
{
*outstr = "";
#ifdef MYSELF
std::string x_string = std::to_string(_x);
std::string y_string = std::to_string(_y);
// 手动序列化
*outstr = x_string + SEP + _op + SEP + y_string;
#else
Json::Value root;//Value是一个万能对象,接受任何一个kv类型
root["x"] = _x;
root["y"] = _y;//所有放进去的会自动转为string类型
root["op"] = _op;
Json::FastWriter writer;//FastWriter用来序列化,把结构化的数据转为字符串类型
*outstr = writer.write(root);
#endif
return true;
}
bool Deserialize(const std::string& instr)//反序列化:字符串转结构体
{
#ifdef MYSELF
std::vector result;
Util::StringSplit(instr, SEP, &result);
if (result.size() != 3)
return false;
_x = Util::toInt(result[0]);
_y = Util::toInt(result[2]);
if (result[1].size() == 1)
return false; // 协议规定
_op = result[1][0]; // 因为是字符,所以只要一个符号即可
std::cout << "_x: \n"
<< _x << "_y: \n"
<< _y << "_op: " << _op << std::endl;
#else
Json::Value root;
Json::Reader reader;//Reader用来反序列化
reader.parse(instr, root);
_x = root["x"].asInt();//拿到的是字符串,要转成int类型
_y = root["y"].asInt();
//_op虽然是char,但它在计算机里就是整数,序列化时放进root的就是整数类型,反序列化时转成int类型,然后编译器会根据char类型自动解释成char类型
_op = root["op"].asInt();
#endif
return true;
}
//Response
bool Serialize(std::string* outstr)
{
*outstr = "";
#ifdef MYSELF
std::string res_string = std::to_string(_result);
std::string code_string = std::to_string(_code);
// 手动序列化
*outstr = res_string + SEP + code_string;
#else
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
*outstr = writer.write(root);
#endif
return true;
}
bool Deserialize(const std::string& instr)
{
#ifdef MYSELF
std::vector result;
Util::StringSplit(instr, SEP, &result);
if (result.size() != 2)
return false;
_result = Util::toInt(result[0]);
_code = Util::toInt(result[1]);
std::cout << "_result: \n"
<< _result << "_code: " << _code << std::endl;
#else
Json::Value root;
Json::Reader reader;
reader.parse(instr, root);
_result = root["result"].asInt();
_code = root["code"].asInt();
#endif
return true;
}
因为用了第三方库,所以makefile要加上一句
.PHONY:all
all:calserver calclient
calclient:CalculatorClient.cc -ljsoncpp
g++ -o $@ $^ -std=c++11
calserver:CalculatorServer.cc -lpthread -ljsoncpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f calclient calserver
如果想用自己的方案,就把定义的宏解开注释,就可以用了。序列化中把FastWriter改成StyledWriter,打印出来的格式不一样,写的代码还是一样。
这篇的重点就是协议,序列化和反序列化,协议就是Prorocol.hpp的Request和Response,里面定义了成员变量,如果不是按照要求的数据,就会退出返回,不处理;序列化和反序列化就是这两个类里的两个接口,根据协议规定来做这方面工作。
协议不只有一个,还可以定多个协议放在一起,符合哪个协议就去找哪个协议对应的方法,比如在报文前加上协议号,多创建几个类去实现方法。
结束。