应⽤层⾃定义协议与序列化
目录
1.序列化和反序列化
2.⽹络版计算器实现
2.1 Socket封装
2.2 序列化和反序列化
2.2.1 Jsoncpp
2.2.2 序列化
2.2.3 反序列化
2.3 定制协议
2.4 任务类
2.5 服务端实现
2.6 客户端实现
1.序列化和反序列化
对于网络双方通信,发送方经过应用层,传输层,网络层,数据链路层,物理层这五层才能发送消息,对于一段消息,可能有多个字段,例如:姓名,性别,年龄等,之后OS会对这些多个字段进行序列化,也就是把这些多个字段进行合并成一个字段,对于接受方就要进行反序列化,把一个字段的信息转化为原本多个字段的信息
在TCP协议中,利用解read、write这种文件操作的接口进行网络通信传输,本质上是拷贝,在TCP协议传输层中有两个缓冲区,一种是发送缓冲区,一种是接受缓冲区,当使用read、write这种文件操作的接口时,就会把信息拷贝到缓冲区,之后实际数据什么时候发,发多少,出错了怎么办,由TCP控制,所以TCP叫做传输控制协议,因此在任何⼀台主机上,TCP连接既有发送缓冲区,⼜有接受缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双⼯
由于TCP中实际数据什么时候发,发多少出错了怎么办,由TCP控制,而在TCP中要读到完整的信息,也是用接收方自己确定,而这种就是TCP面向字节流的特性
2.⽹络版计算器实现
2.1 Socket封装
首先可以对套接字(Socket)进行封装
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include "Common.hpp"
#include "Log.hpp"
#include "inerAddr.hpp"using namespace LogModuls;
namespace SocketModuls
{const static int gbacklog = 16;// 模版方法模式// 基类socket, 大部分方法,都是纯虚方法class Socket{public:virtual ~Socket() {}virtual void SocketDrDie()=0;//创建套接字virtual void Bind(uint16_t port)=0;//绑定端口virtual void ListenDiDie(int backlog)=0;//监听套接字virtual std::shared_ptr<Socket> Accept(inerAddr *addr)=0;//接受连接virtual void Close()=0;//关闭套接字virtual int Recv(std::string *out) = 0;//读取数据virtual int Send(const std::string &message) = 0;//发送数据virtual int Connect(const std::string &ip,uint16_t port) = 0;//连接服务器public://服务端构建套接字void BuildTcpServerSocketMethod(uint16_t port, int backlog = gbacklog){SocketDrDie();Bind(port);ListenDiDie(backlog);}//客户端构建套接字void BuildTcpClientSocketMethod(){SocketDrDie();}};const static int defaultfd=-1;class TcpSocket : public Socket{public:TcpSocket():_sockfd(defaultfd){}TcpSocket(int fd):_sockfd(fd){}virtual ~TcpSocket() {}virtual void SocketDrDie() override//创建套接字{_sockfd=::socket(AF_INET,SOCK_STREAM,0);if(_sockfd<0){LOG(LogLevel::ERROR)<<"create socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO)<<"create socket success";}virtual void Bind(uint16_t port) override//绑定端口{inerAddr addr(port);int n=::bind(_sockfd,addr.NetAddrPtr(),addr.NetAddrLen());if(n<0){LOG(LogLevel::ERROR)<<"bind socket error";exit(BIND_ERR);}LOG(LogLevel::INFO)<<"bind socket success";}virtual void ListenDiDie(int backlog) override//监听套接字{int n=::listen(_sockfd,backlog);if(n<0){LOG(LogLevel::ERROR)<<"listen socket error";exit(LISTEN_ERR);}LOG(LogLevel::INFO)<<"listen socket success";}//接受连接std::shared_ptr<Socket> Accept(inerAddr *addr) override//addr为输出参数{struct sockaddr_in client_addr;socklen_t client_addr_len=sizeof(client_addr);int client_fd=::accept(_sockfd,(struct sockaddr*)&client_addr,&client_addr_len);if(client_fd<0){LOG(LogLevel::ERROR)<<"accept socket error";exit(ACCEPT_ERR);}addr->SetAddr(client_addr);return std::make_shared<TcpSocket>(client_fd);}//读取数据int Recv(std::string *out)override{char buf[1024];ssize_t n=::recv(_sockfd,buf,sizeof(buf)-1,0);//相当于readif(n>0){buf[n]=0;*out+=buf;}return n;}//发送数据int Send(const std::string &message)override{return send(_sockfd,message.c_str(),message.size(),0);}void Close()//关闭套接字{if(_sockfd>0)::close(_sockfd);}int Connect(const std::string &ip,uint16_t port) override//连接服务器{inerAddr addr(ip,port);return ::connect(_sockfd,addr.NetAddrPtr(),addr.NetAddrLen()); }private:int _sockfd;};
}
2.2 序列化和反序列化
在上文了解到,无论对于客户端还是服务端都需要进行序列化和反序列化,此时可以利用Jsoncpp来进行
2.2.1 Jsoncpp
特性1. 简单易⽤:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。2. ⾼性能:Jsoncpp 的性能经过优化,能够⾼效地处理⼤量 JSON 数据。3. 全⾯⽀持:⽀持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。4. 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,⽅便开发者调试。
安装ubuntu:sudo apt-get install libjsoncpp-devCentos: sudo yum install jsoncpp-devel
2.2.2 序列化
优点:将 Json::Value 对象直接转换为格式化的JSON字符串。
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";std::string s = root.toStyledString();std::cout << s << std::endl;return 0;
}
转换后的格式如下
{"name" : "joe","sex" : "男"
}
优点:提供了更多的定制选项,如缩进、换⾏符等。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>intmain()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::StreamWriterBuilder wbuilder; // StreamWriter的⼯⼚std::unique_ptr<Json::StreamWriter>writer(wbuilder.newStreamWriter());std::stringstream ss;writer->write(root, &ss);std::cout << ss.str() << std::endl;return 0;
}
$./ test.exe{"name" : "joe","sex" : "男"
}
3.使⽤ Json::FastWriter :
优点:⽐ StyledWriter 更快,因为它不添加额外的空格和换⾏符。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>intmain()
{Json::Value root;root["name"] = "joe";root["sex"] = "男";Json::FastWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;return 0;
}
$./ test.exe{"name" : "joe", "sex" : "男"}
2.2.3 反序列化
优点:提供详细的错误信息和位置,⽅便调试。
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{// JSON 字符串std::string json_string = "{\"name\":\"张三\", \"age\":30,\"city\":\"北京\"}";// 解析 JSON 字符串Json::Reader reader;Json::Value root;// 从字符串中读取 JSON 数据bool parsingSuccessful = reader.parse(json_string, root);if (!parsingSuccessful){// 解析失败,输出错误信息std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;return 1;}// 访问 JSON 数据std::string name = root["name"].asString();int age = root["age"].asInt();std::string city = root["city"].asString();// 输出结果std::cout << "Name: " << name << std::endl;std::cout << "Age: " << age << std::endl;std::cout << "City: " << city << std::endl;return 0;
}Name : 张三
Age : 30
City : 北京
2.3 定制协议
对于客户端和服务端来说,他们双方都需要进行反序列化和序列化
class Request//客户端发出请求
{
public:Request(){}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}std::string ToString(){// 序列化Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;std::string str = writer.write(root);return str;}bool ParseFromString(std::string &str){// 反序列化Json::Reader reader;Json::Value root;if (!reader.parse(str, root)){return false;}_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}int X() { return _x; }int Y() { return _y; }char Oper() { return _oper; }private:int _x;int _y;char _oper;
};class Response//服务端发送给客户端的应答
{
public:Response(){}Response(int result, int code) : _result(result), _code(code){}std::string ToString(){// 序列化Json::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter writer;std::string str = writer.write(root);return str;}bool ParseFromString(std::string &str){// 反序列化Json::Reader reader;Json::Value root;if (!reader.parse(str, root)){return false;}_result = root["result"].asInt();_code = root["code"].asInt();return true;}void SetResult(int res){_result = res;}void SetCode(int code){_code = code;}void ShowResult(){std::cout << "计算结果是: " << _result << "[" << _code << "]" << std::endl;}private:int _result; // 运算结果,无法区分清楚应答是计算结果,还是异常值int _code; // 0:sucess, 1,2,3,4->不同的运算异常的情况
};
由于客户端发给服务端是一个字符串的形式,因此可以在字符串加上一些限定符和字符串的长度来进行封装
const std::string sep = "\r\n"; // 协议分隔符// 封装报头std::string Encode(const std::string &jsonstr){// 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\nstd::string len = std::to_string(jsonstr.size());return len + sep + jsonstr + sep; // 应用层封装报头}
同时,由于Tcp是字节流的,因此要对字符串(报文)进行判断,是否是一个完整的报文
// 判断是否有完整的报文bool Decode(std::string &data, std::string *jsonstr){// 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\nsize_t pos = data.find(sep);if (pos == std::string::npos){return false; // 让调用方继续从内核中读取数据}std::string lenstr = data.substr(0, pos);int len = std::stoi(lenstr);int json_len = len + lenstr.size() + 2 * sep.size(); // 完整报头的长度if (data.size() < json_len){return false; // 让调用方继续从内核中读取数据}*jsonstr = data.substr(pos + sep.size(), len);data.erase(0, json_len); // 移除已解析的数据return true;}
之后就是客户端发送请求和服务端处理请求,协议代码完整如下
const std::string sep = "\r\n"; // 协议分隔符
using func_t = std::function<Response(Request &req)>; // 让上层调用者传入处理函数class Protocol//协议
{
public:Protocol(){}Protocol(func_t func) : _func(func){}// 封装报头std::string Encode(const std::string &jsonstr){// 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\nstd::string len = std::to_string(jsonstr.size());return len + sep + jsonstr + sep; // 应用层封装报头}// 判断是否有完整的报文bool Decode(std::string &data, std::string *jsonstr){// 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\nsize_t pos = data.find(sep);if (pos == std::string::npos){return false; // 让调用方继续从内核中读取数据}std::string lenstr = data.substr(0, pos);int len = std::stoi(lenstr);int json_len = len + lenstr.size() + 2 * sep.size(); // 完整报头的长度if (data.size() < json_len){return false; // 让调用方继续从内核中读取数据}*jsonstr = data.substr(pos + sep.size(), len);data.erase(0, json_len); // 移除已解析的数据return true;}// 服务端处理请求void GetRequest(std::shared_ptr<Socket> &sock, inerAddr &client){// 从套接字中读取数据std::string data;while (true){int n = sock->Recv(&data);if (n > 0){std::string jsonstr;while (Decode(data, &jsonstr)){// 此时jsonstr是完整的报文Request req;bool parse_ret = req.ParseFromString(jsonstr);if (!parse_ret)continue;Response resp = _func(req); // 调用业务处理函数LOG(LogLevel::INFO) << "client:" << client.getIP() << ":" << client.getPort() << "Request:" << req.X() << req.Oper() << req.Y();// 序列化std::string resp_str = resp.ToString();// 添加报头std::string send_data = Encode(resp_str);// 发送sock->Send(send_data.c_str());}}else if (n == 0){LOG(LogLevel::INFO) << "client:" << client.getIP() << ":" << client.getPort() << "Quit!";break;}else{LOG(LogLevel::WARN) << "client:" << client.getIP() << ":" << client.getPort() << "recv error";break;}}}// 客户端发送请求bool GetResponse(std::shared_ptr<Socket> &client, std::string &res_buff, Response *resp){while (true){int n = client->Recv(&res_buff);if (n > 0){std::string jsonstr;while (Decode(res_buff, &jsonstr)){// 反序列化resp->ParseFromString(jsonstr);}return true;}else if (n == 0){LOG(LogLevel::ERROR) << "client disconnect";return false;}else{LOG(LogLevel::ERROR) << "recv error";return false;}}}std::string BuildRequestString(int x, int y, char oper){// 1. 构建一个完整的请求Request req(x, y, oper);// 2. 序列化std::string json_req = req.ToString();return Encode(json_req);}~Protocol(){}private:func_t _func;
};
2.4 任务类
对计算任务处理进行封装
#pragma once#include "Protocol.hpp"//协议
#include <iostream>
class Cal
{
public:Response Execute(Request &req){Response resp(0, 0); // code: 0表示成功switch (req.Oper()){case '+':resp.SetResult(req.X() + req.Y());break;case '-':resp.SetResult(req.X() - req.Y());break;case '*':resp.SetResult(req.X() * req.Y());break;case '/':{if (req.Y() == 0){resp.SetCode(1); // 1除零错误}else{resp.SetResult(req.X() / req.Y());}}break;case '%':{if (req.Y() == 0){resp.SetCode(2); // 2 mod 0 错误}else{resp.SetResult(req.X() % req.Y());}}break;default:resp.SetCode(3); // 非法操作break;}return resp;}
};
2.5 服务端实现
#include "Socket.hpp"//套接字封装
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>using namespace SocketModuls;
using namespace LogModuls;
using ioservice_t=std::function<void(std::shared_ptr<Socket> &sock,inerAddr &client)>;class TcpServer
{public:TcpServer(uint16_t port,ioservice_t ioservice):m_port(port),m_isRun(false),m_ioservice(ioservice),m_socket(std::make_unique<TcpSocket>()){m_socket->BuildTcpSocketMethod(m_port);}void Start(){m_isRun = true;while (m_isRun){inerAddr client;//建立连接auto sock=m_socket->Accept(&client);if (sock==nullptr){continue;}LOG(LogLevel::INFO)<<"accept a client";//处理连接多进程pid_t pid=fork();if(pid<0){LOG(LogLevel::ERROR)<<"fork error";exit(FORK_ERR);}else if(pid==0){m_socket->Close();if(fork()>0)exit(OK);m_ioservice(sock,client);exit(OK);}else{sock->Close();pid_t rid = ::waitpid(pid, nullptr, 0);}}m_isRun = false;}~TcpServer(){}private:uint16_t m_port;bool m_isRun;std::unique_ptr<Socket> m_socket;ioservice_t m_ioservice;
};
#include "Protocol.hpp"
#include "Tcpserver.hpp"
#include"NetCal.hpp"//任务类
#include <memory>void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}// ./tcpserver 8080
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}//顶层std::unique_ptr<Cal> cal=std::make_unique<Cal>();//协议层std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{return cal->Execute(req);});//服务层std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),[&protocol](std::shared_ptr<Socket> &sock, inerAddr &client){protocol->GetRequest(sock, client);});tsvr->Start();return 0;
}
2.6 客户端实现
#include "Socket.hpp"
#include "Common.hpp"
#include "Protocol.hpp"
#include <iostream>
#include <string>
#include <memory>using namespace SocketModuls;void GetDataFromStdin(int *x, int *y, char *oper)
{std::cout << "Please Enter x: ";std::cin >> *x;std::cout << "Please Enter y: ";std::cin >> *y;std::cout << "Please Enter oper: ";std::cin >> *oper;
}
int main(int argc,char* argv[])
{if(argc!=3){std::cerr<<"Usage: "<<argv[0]<<" ip port"<<std::endl;exit(USAGE_ERR);}std::string ip=argv[1];uint16_t port=atoi(argv[2]);std::shared_ptr<Socket> client=std::make_shared<TcpSocket>();client->BuildTcpClientSocketMethod();if(client->Connect(ip,port)!=0){std::cerr<<"Connect error"<<std::endl;exit(CONNECT_ERR);}std::unique_ptr<Protocol> protocol=std::make_unique<Protocol>();std::string resp_buff;while(true){int x, y;char oper;GetDataFromStdin(&x, &y, &oper);std::string req_str=protocol->BuildRequestString(x, y, oper);//发送client->Send(req_str);//接收Response resp;bool res=protocol->GetResponse(client, resp_buff,&resp);if(res==false)break;//打印结果resp.ShowResult();}client->Close();return 0;
}