✨个人主页:熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
上一弹已经写好服务端代码,并讲解了序列化使用到的json库,这弹开始写协议,并完成网络计算器的代码!
1、Protocol.hpp
该文件实现序列化与反序列使用到的类和相关函数(加报头解报头)!
1.1、Request类
该类是向服务器发送请求的类 ,需要三个成员变量,_x,_y,_oper(运算符号),统一的计算格式是 _x _oper _y,内部主要是序列化(发送)和反序列化(接受)函数!
1.1.1、基本结构
该类有三个成员变量,_x,_y,_oper(运算符号),序列化,反序列化,构造,析构及其他获取成员变量与打印的函数!
class Request
{
public:
Request()
{}
// 序列化 将结构化转成字符串
bool Serialize(std::string *out);
// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in);
void Print();
~Request();
int X();
int Y();
char Oper();
private:
int _x;
int _y;
char _oper; // + - * / % // x oper y
};
1.1.2、构造析构函数
为了方便后面的使用,此处实现两个构造函数,一个无参,一个带参函数,析构函数无需处理!
Request()
{}
Request(int x,int y,char oper):_x(x),_y(y),_oper(oper)
{}
~Request()
{}
1.1.3、序列化函数
序列化 即将结构化转成字符串,并将字符串以输出型参数传出!
// 序列化 将结构化转成字符串
bool Serialize(std::string *out)
{
// 1.自己做: "x oper y" (麻烦)
// 2.使用现成的库, xml,json(jsoncpp库), protobuf
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
std::string s = writer.write(root);
*out = s;
return true;
}
1.1.4、反序列化函数
反序列化 即将字符串转成结构化,参数传入字符串!
// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in,root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return true;
}
打印函数
void Print()
{
std::cout << _x << std::endl;
std::cout << _y << std::endl;
std::cout << _oper << std::endl;
}
1.1.5、获取成员变量函数
因为成员变量是私有的,外部访问使用成员函数!
int X()
{
return _x;
}
int Y()
{
return _y;
}
char Oper()
{
return _oper;
}
1.1.6、设置成员变量值函数
cpp
void SetValue(int x, int y, char oper)
{
_x = x;
_y = y;
_oper = oper;
}
1.2、Response类
该类是向客户端发送结果的类 ,需要三个成员变量,_result(计算结果),_code(自定义错误码[**0: success 1: div error 2: 非法操作 ]),_desc(错误码描述),内部主要是序列化(发送)和反序列化(接受)函数**!
1.2.1、基本结构
该类有三个成员变量,_result(计算结果),_code(自定义错误码****),_desc(错误码描述),序列化,反序列化,构造,析构和打印函数!
注意:为了方便类外访问该类成员变量,该成员变量是公有的!
class Response
{
public:
Response();
// 序列化 将结构化转成字符串
bool Serialize(std::string *out);
// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in);
~Response();
public:
int _result;
int _code; // 0: success 1: div error 2: 非法操作
std::string _desc;
};
1.2.2、构造析构函数
构造函数直接手动初始化 (结果和错误码初始化为0,描述默认初始化为success),析构函数无需处理!
Response():_result(0),_code(0),_desc("success")
{}
~Response()
{}
1.2.3、序列化函数
序列化 即将结构化转成字符串,并将字符串以输出型参数传出!
// 序列化 将结构化转成字符串
bool Serialize(std::string *out)
{
// 使用现成的库, xml,json(jsoncpp库), protobuf
Json::Value root;
root["result"] = _result;
root["code"] = _code;
root["desc"] = _desc;
Json::FastWriter writer;
std::string s = writer.write(root);
*out = s;
return true;
}
1.2.4、反序列化函数
反序列化 即将字符串转成结构化,参数传入字符串!
// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in,root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return true;
}
1.2.5、打印函数
将成员变量以字符串形式打印出来即可!
void PrintResult()
{
std::cout << "result: " << _result << ", code: " << _code
<< ", desc: " << _desc << std::endl;
}
1.3、Factory类
因为Request类和Response类可能频繁创建,因此我们可以设计一个工厂类 ,内部设计两个创建类的静态函数(没有this指针,外部直接调用函数即可)!
cpp
class Factory
{
public:
static std::shared_ptr<Request> BuildRequestDefault()
{
return std::make_shared<Request>();
}
static std::shared_ptr<Response> BuildResponseDefault()
{
return std::make_shared<Response>();
}
};
1.4、添加报头
在实际的网络通信中,传的不仅仅是序列化之后的字符串,还会有报头信息 ,此处我们也设计一下报头信息,格式如下:
1、"len"\r\n"{json}"\r\n --- 完整的报文
2、len 有效载荷的长度
3、\r\n(第一个): 区分len 和 json 串
4、\r\n(第二个): 暂时没有其他用,打印方便,debug
cpp
static const std::string sep = "\r\n"; // 分隔符
// 添加报头
std::string Encode(const std::string &jsonstr)
{
int len = jsonstr.size();
std::string lenstr = std::to_string(len);
return lenstr + sep + jsonstr + sep;
}
1.5、解析报头
将发送过来的有报头的信息解析成有效信息 ,即去掉前面的长度和分割符与有效信息后面的分隔符!
注意:可能没有一个有效信息或者有多个有效信息!
cpp
static const std::string sep = "\r\n"; // 分隔符
// 不能带const
// "le
// "len"\r\n"{j [)
// "len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
std::string Decode(std::string &packagestream)
{
// 分析
auto pos = packagestream.find(sep); // 报文流
if (pos == std::string::npos)
return std::string(); // 没找到返回空
std::string lenstr = packagestream.substr(0, pos);
int len = std::stoi(lenstr); // json长度
// 计算一个完整的报文应该是多长
int total = lenstr.size() + len + 2 * sep.size();
// 传进来的字符串长度小于报文总长,说明没有一个完整的有效信息,返回空
if (packagestream.size() < total)
return std::string();
// 提取
std::string jsonstr = packagestream.substr(pos + sep.size(), len);
packagestream.erase(0, total); // 从0位置删除total长度
return jsonstr;
}
2、Service.hpp
Service.hpp中的 IOService类 是用于通信的类,而且内部需要执行传入的回调函数,因此该类需要加一个执行方法的成员!
执行方法的声明:
参数是请求类的指针,返回值是应答类的指针!
using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;
2.1、构造析构函数
构造函数需要传入函数对象,用于初始化成员变量,析构函数无需处理!
cpp
IOService(process_t process) :_process(process)
{}
~IOService()
{}
2.2、IOExcute()
IOExcute()函数进行客户端与服务端的通信,并处理发送过来的信息(调用执行方法),有以下7个主要步骤!
1、接收消息
2、报文解析(保证获取至少获得一条有效信息,没有则继续接受消息)
3、反序列化(将字符串转成结构化)
4、业务处理(调用构造函数传入的回调函数)
5、序列化应答
6、添加len长度(报头)
7、发送回去
cpp
void IOExcute(SockSPtr sock, InetAddr &addr)
{
std::string packagestreamqueue; // 写在while循环外,存储信息
while (true)
{
// 1.负责读取
ssize_t n = sock->Recv(&packagestreamqueue);
if(n <= 0)
{
LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());
break;
}
std::cout << "--------------------------------------------" << std::endl;
std::cout << "packagestreamqueue: \n" << packagestreamqueue << std::endl;
// 我们能保证读到的是完整的报文? 不能!
// 2.报文解析,提取报头和有效载荷
std::string package = Decode(packagestreamqueue);
if(package.empty()) continue;
// 我们能保证读到的是一个完整的报文!!!
auto req = Factory::BuildRequestDefault();
std::cout << "package: \n" << package << std::endl;
// 3.反序列化
req->Deserialize(package); // 反序列化 将字符串转成结构化
// 4.业务处理
auto resp = _process(req); // 业务处理(通过请求,得到应答)
// 5.序列化应答
std::string respjson;
resp->Serialize(&respjson); // 序列化
std::cout << "respjson: \n" << respjson << std::endl;
// 6.添加len长度
respjson = Encode(respjson);
std::cout << "respjson add header done: \n" << respjson << std::endl;
// 7.发送回去
sock->Send(respjson);
}
此处有一个问题,如果第一次接收消息没有读到完整的报文就会继续接受消息,但是以我们前面写的接收消息函数会清空内容,因此我们需要做稍微的修改!
2.3、Recv()
Recv()函数是Socket.hpp文件中TcpServer类的成员函数,接收消息成功之后需要该为拼接旧的内容!
cpp
// 接收消息
ssize_t Recv(std::string *out) override
{
char inbuffer[4096];
ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
if (n > 0)
{
inbuffer[n] = 0;
// *out = inbuffer;
*out += inbuffer; // 调整(可能一次读取不成功 | 读取多次)
}
return n;
}
3、NetCal.hpp
NetCal.hpp文件中的NetCal类包含回调函数的具体实现!
3.1、构造析构函数
该类没有成员变量,构造析构函数无需处理!
cpp
NetCal()
{}
~NetCal()
{}
3.2、Calculator()
Calculator() 函数用于网络计算器的计算逻辑!
cpp
std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req)
{
auto resp = Factory::BuildResponseDefault();
switch (req->Oper())
{
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;
resp->_desc = "divc zero";
}
else
{
resp->_result = req->X() / req->Y();
}
}
break;
case '%':
{
if(req->Y() == 0)
{
resp->_code = 2;
resp->_desc = "mod zero";
}
else
{
resp->_result = req->X() % req->Y();
}
}
break;
default:
{
resp->_code = 3;
resp->_desc = "illegal operation";
}
break;
}
return resp;
}
4、ClientMain.cc
该文件用户创建TcpServer类对象,并调用执行函数运行客户端!
通信操作主要包括以下七步:
1、序列化
2、添加长度报头字段
3、发送数据
4、读取应答,response
5、报文解析,提取报头和有效载荷
6、反序列化
7、打印结果
cpp
#include <iostream>
#include <ctime>
#include "Socket.hpp"
#include "Protocol.hpp"
using namespace socket_ns;
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
SockSPtr sock = std::make_shared<TcpSocket>();
if (!sock->BuildClientSocket(serverip, serverport))
{
std::cerr << "connect error" << std::endl;
exit(1);
}
srand(time(nullptr) ^ getpid());
const std::string opers = "+-*/%&^!";
std::string packagestreamqueue;
while (true)
{
// 构建数据
int x = rand() % 10;
usleep(x * 1000);
int y = rand() % 10;
usleep(x * y * 100);
char oper = opers[y % opers.size()];
// 构建请求
auto req = Factory::BuildRequestDefault();
req->SetValue(x, y, oper);
// 1.序列化
std::string reqstr;
req->Serialize(&reqstr);
// 2.添加长度报头字段
reqstr = Encode(reqstr);
std::cout << "####################################" << std::endl;
std::cout << "requset string: \n" << reqstr << std::endl;
// 3.发送数据
sock->Send(reqstr);
while (true)
{
// 4.读取应答,response
ssize_t n = sock->Recv(&packagestreamqueue);
if (n <= 0)
{
break;
}
// 我们能保证读到的是完整的报文? 不能!
// 5.报文解析,提取报头和有效载荷
std::string package = Decode(packagestreamqueue);
if (package.empty())
continue;
std::cout << "package: \n" << package << std::endl;
// 6.反序列化
auto resp = Factory::BuildResponseDefault();
resp->Deserialize(package);
// 7.打印结果
resp->PrintResult();
break;
}
sleep(1);
}
sock->Close();
return 0;
}
5、完整代码
5.1、Protocol.hpp
cpp
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <jsoncpp/json/json.h>
static const std::string sep = "\r\n"; // 分隔符
// 设计一下协议的报头和报文的完整格式
// "len"\r\n"{json}"\r\n --- 完整的报文
// len 有效载荷的长度
// \r\n(第一个): 区分len 和 json 串
// \r\n(第二个): 暂时没有其他用,打印方便,debug
// 添加报头
std::string Encode(const std::string &jsonstr)
{
int len = jsonstr.size();
std::string lenstr = std::to_string(len);
return lenstr + sep + jsonstr + sep;
}
// 不能带const
// "le
// "len"\r\n"{j [)
// "len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
std::string Decode(std::string &packagestream)
{
// 分析
auto pos = packagestream.find(sep); // 报文流
if (pos == std::string::npos)
return std::string(); // 没找到返回空
std::string lenstr = packagestream.substr(0, pos);
int len = std::stoi(lenstr); // json长度
// 计算一个完整的报文应该是多长
int total = lenstr.size() + len + 2 * sep.size();
// 传进来的字符串长度小于报文总长,说明没有一个完整的有效信息,返回空
if (packagestream.size() < total)
return std::string();
// 提取
std::string jsonstr = packagestream.substr(pos + sep.size(), len);
packagestream.erase(0, total); // 从0位置删除total长度
return jsonstr;
}
// 协议
class Request
{
public:
Request()
{
}
Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
{
}
// 序列化 将结构化转成字符串
bool Serialize(std::string *out)
{
// 1.自己做: "x oper y" (麻烦)
// 2.使用现成的库, xml,json(jsoncpp库), protobuf
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
std::string s = writer.write(root);
*out = s;
return true;
}
// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return true;
}
void Print()
{
std::cout << _x << std::endl;
std::cout << _y << std::endl;
std::cout << _oper << std::endl;
}
~Request()
{
}
int X()
{
return _x;
}
int Y()
{
return _y;
}
char Oper()
{
return _oper;
}
void SetValue(int x, int y, char oper)
{
_x = x;
_y = y;
_oper = oper;
}
private:
int _x;
int _y;
char _oper; // + - * / % // x oper y
};
// class request resp = {30,0}
class Response
{
public:
Response() : _result(0), _code(0), _desc("success")
{
}
// 序列化 将结构化转成字符串
bool Serialize(std::string *out)
{
// 使用现成的库, xml,json(jsoncpp库), protobuf
Json::Value root;
root["result"] = _result;
root["code"] = _code;
root["desc"] = _desc;
Json::FastWriter writer;
std::string s = writer.write(root);
*out = s;
return true;
}
// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
if (!res)
return false;
_result = root["result"].asInt();
_code = root["code"].asInt();
_desc = root["desc"].asString();
return true;
}
void PrintResult()
{
std::cout << "result: " << _result << ", code: " << _code << ", desc: " << _desc << std::endl;
}
~Response()
{
}
public:
int _result;
int _code; // 0: success 1: div error 2: 非法操作
std::string _desc;
};
class Factory
{
public:
static std::shared_ptr<Request> BuildRequestDefault()
{
return std::make_shared<Request>();
}
static std::shared_ptr<Response> BuildResponseDefault()
{
return std::make_shared<Response>();
}
};
5.2、Service.hpp
cpp
#pragma once
#include <iostream>
#include <functional>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"
using namespace socket_ns;
using namespace log_ns;
using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;
class IOService
{
public:
IOService(process_t process) :_process(process)
{
}
void IOExcute(SockSPtr sock, InetAddr &addr)
{
std::string packagestreamqueue; // 写在while循环外,存储信息
while (true)
{
// 1.负责读取
ssize_t n = sock->Recv(&packagestreamqueue);
if(n <= 0)
{
LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());
break;
}
std::cout << "--------------------------------------------" << std::endl;
std::cout << "packagestreamqueue: \n" << packagestreamqueue << std::endl;
// 我们能保证读到的是完整的报文? 不能!
// 2.报文解析,提取报头和有效载荷
std::string package = Decode(packagestreamqueue);
if(package.empty()) continue;
// 我们能保证读到的是一个完整的报文!!!
auto req = Factory::BuildRequestDefault();
std::cout << "package: \n" << package << std::endl;
// 3.反序列化
req->Deserialize(package); // 反序列化 将字符串转成结构化
// 4.业务处理
auto resp = _process(req); // 业务处理(通过请求,得到应答)
// 5.序列化应答
std::string respjson;
resp->Serialize(&respjson); // 序列化
std::cout << "respjson: \n" << respjson << std::endl;
// 6.添加len长度
respjson = Encode(respjson);
std::cout << "respjson add header done: \n" << respjson << std::endl;
// 7.发送回去
sock->Send(respjson);
}
}
// 测试
// void IOExcute(SockSPtr sock, InetAddr &addr)
// {
// while (true)
// {
// std::string message;
// ssize_t n = sock->Recv(&message);
// if(n > 0)
// {
// LOG(INFO, "get message from client [%s],message: %s\n", addr.AddrStr().c_str(), message.c_str());
// std::string hello = "hello";
// sock->Send(hello);
// }
// else if(n == 0)
// {
// LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
// break;
// }
// else
// {
// LOG(ERROR, "read error\n", addr.AddrStr().c_str());
// break;
// }
// }
// }
~IOService()
{
}
private:
process_t _process;
};
5.3、ClientMain.cc
cpp
#include <iostream>
#include <ctime>
#include "Socket.hpp"
#include "Protocol.hpp"
using namespace socket_ns;
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
SockSPtr sock = std::make_shared<TcpSocket>();
if (!sock->BuildClientSocket(serverip, serverport))
{
std::cerr << "connect error" << std::endl;
exit(1);
}
srand(time(nullptr) ^ getpid());
const std::string opers = "+-*/%&^!";
std::string packagestreamqueue;
while (true)
{
// 构建数据
int x = rand() % 10;
usleep(x * 1000);
int y = rand() % 10;
usleep(x * y * 100);
char oper = opers[y % opers.size()];
// 构建请求
auto req = Factory::BuildRequestDefault();
req->SetValue(x, y, oper);
// 1.序列化
std::string reqstr;
req->Serialize(&reqstr);
// 2.添加长度报头字段
reqstr = Encode(reqstr);
std::cout << "####################################" << std::endl;
std::cout << "requset string: \n" << reqstr << std::endl;
// 3.发送数据
sock->Send(reqstr);
while (true)
{
// 4.读取应答,response
ssize_t n = sock->Recv(&packagestreamqueue);
if (n <= 0)
{
break;
}
// 我们能保证读到的是完整的报文? 不能!
// 5.报文解析,提取报头和有效载荷
std::string package = Decode(packagestreamqueue);
if (package.empty())
continue;
std::cout << "package: \n" << package << std::endl;
// 6.反序列化
auto resp = Factory::BuildResponseDefault();
resp->Deserialize(package);
// 7.打印结果
resp->PrintResult();
break;
}
sleep(1);
}
sock->Close();
return 0;
}
5.4、Makefile
cpp
.PHONY:all
all:calserver calclient
calserver:ServerMain.cc
g++ -o $@ $^ -std=c++14 -ljsoncpp
calclient:ClientMain.cc
g++ -o $@ $^ -std=c++14 -ljsoncpp
.PHONY:clean
clean:
rm -rf calserver calclient