请各位保持头脑清醒,
读些好书,做点有用的事,
快快乐乐地生活。
--- 斯蒂芬·金 《肖申克的救赎》---
从零开始掌握序列化
1 知识回顾
前面两篇文章学习中基础知识和三层结构的实现
我们学习了:
序列化与反序列化:
- 必要性:协议的本质是双方都认识的结构化数据,为了传输结构化的数据就需要进行序列化,为了从数据流中获取结构化数据就要进行反序列化!
- 本质:序列化的本质是将结构化的数据转换成字符串,将字符串发送给客户端。客户端根据协议进行反序列化获取到结构化数据!
- 序列化与反序列化的方法有很多种,可以自行编写也可以使用第三方库,比如JSON库
并且重新理解了TCP协议:
TCP协议
- 支持全双工通信:传输层会创建两个缓冲区:发送缓冲区和接收缓冲区。发送和接收是分开的,所以天然支持全双工
- 通信函数本质:read , write , send , recv本质上都是拷贝函数!他们都是讲数据拷贝到缓冲区中,并不关心缓冲区中的数据何时以何种方式发送给对方,系统负责缓冲区的刷新!
- 传输层是属于OS的,传输缓冲区的本质和文件缓冲区一样,在操作系统看起来都是向文件中进行刷新写入,用户不需要考虑!
最重要的是将Socket进行了程序重构,具体的细节在TCP协议中讲解过。这样将通信功能彻底解耦出来:
- 将socket系列操作分类封装,设计为基类,派生出Tcp和Udp两种具体的Socket!
- 基类都需要进行创建socket文件 、进行绑定、 进入listen 、获取链接、 申请链接…由于两种类的操作方式不一致,所以基类只需要进行一个声明就可以,具体实现在派生类中完成!
- 依照基础的方法进行组合就可以实现生成服务器端Socket和客户端Socket!
对应网络计算器的需求,我们设计了Request和Response两个结构体作为通信的协议!并且我们通过JSON库来进行协议内部的序列化与反序列化!为了保证可以获取完整的结构化数据,我们设计了独特的报文结构:
len\r\n{json}\r\n
这样可以保证从数据流中获取完整的报文结构!!!
2 服务器框架
服务器的框架是基于这样的三层结构实现的:
- 传输层
TcpServer
:负责从Socket文件中获取链接,传输层不需要进行IO,获取到连接就让会话层通过连接获取数据! - 会话层
Service
:根据传输层给的连接,从Sockfd文件中读取数据,解析出报文结构中的数据字符串,然后通过协议分离出结构化数据。该层只负责数据的解析,数据的处理交给应用层进行! - 应用层
Process
:应用层是具有的业务逻辑,根据会话层解析出的数据,进行数据处理!这里使用的是网络计算器的业务逻辑,也就是执行加减乘除运算!
基于这样的结构我们上层的服务器代码逻辑是很好写的:
#include"TcpServer.hpp"#include"Service.hpp"#include"NetCal.hpp"intmain(int argc,char*argv[]){if(argc !=2){
std::cerr <<"Usage: "<< argv[0]<<" local-port"<< std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);//业务层
NetCal cal;
Service ser(std::bind(&NetCal::Calculator ,&cal , std::placeholders::_1));//IO层
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::bind(&Service::IOExecute,&ser,
std::placeholders::_1,
std::placeholders::_2),
port);//进入通信循环!
tsvr->Loop();return0;}
可以看到我们只是使用了两次的bind绑定就实现了三层结构的实现,十分非简洁明了。只需等待客户端传入数据即可!
3 客户端框架
客户端的框架和服务端类似:
- 首先客户端在执行程序时需要传入服务器的IP地址和端口号!
- 然后通过封装的Socket类创建客户端Socket文件!对于IP地址和端口号的处理都封装在了类方法中,使用起来十分简单快捷!
#include<iostream>#include"Socket.hpp"#include"Protocol.hpp"usingnamespace socket_ns;intmain(int argc,char*argv[]){// 根据参数获取服务器IP 与 端口号if(argc !=3){
std::cerr <<"Usage: "<< argv[0]<<" server-ip server-port"<< std::endl;exit(0);}
std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);// 工厂建立TcpSocket链接
SockSPtr sock = std::make_shared<TcpSocket>();if(!sock->BuildClientSocket(ip, port)){
std::cerr <<"connect error!"<< std::endl;exit(1);}
std::string packagestream;//业务逻辑while(true){}return0;}
接下来我们来进行客户端数据通信的逻辑:
- 基础数据的获取这里为了快捷直接使用随机数进行初始化!
- 发送数据:根据协议快速构建Request,然后对其进行序列化,然后加入报头形成完整报文,发送给服务器
- 接收数据:从Socket文件中读取数据流,去除报头,检查是否具有完整报文,有完整报文就进行反序列化得到Response,打印结果即可!!!
intmain(int argc,char*argv[]){//...srand(time(nullptr));
std::string arr ="+-*/%&^!";
std::string packagestream;int cnt =3;while(cnt--){// 传入数据int x =rand()%50;usleep(1000);int y =rand()%50;char oper = arr[y % arr.size()];// 1. 构建requestauto req =Factory::BuildRequestDefault();
req->SetValue(x, y, oper);// 2. 进行序列化
std::string jsonstr;
req->Serialize(&jsonstr);
std::cout <<"jsonstr: "<< jsonstr << std::endl;// 3. 添加报头
std::string reqstr =Encode(jsonstr);// 4. 发送数据
sock->Send(reqstr);// 5. 接收数据while(true){
ssize_t n = sock->Recv(&packagestream);if(n <=0)break;// 6. 去除报头
std::string resstr =Decode(packagestream);
std::cout <<"resstr: "<< resstr << std::endl;if(resstr.empty())continue;auto res =Factory::BuildResponseDefault();// 7. 反序列化
std::cout <<"----------------"<<std::endl;
res->Deserialize(resstr);
res->PrintResult();break;}}return0;}
这样客户端逻辑就写好了!!!
4 运行测试
我们进行一下简单的测试首先注意因为我们使用JSON库编译时要加入对应的编译动态库选项:
.PHONY:all
all:calserver calclient
calserver:ServerMain.cc
g++ -o$@ $^ -std=c++14 -lpthread-ljsoncpp
calclient:ClientMain.cc
g++ -o$@ $^ -std=c++14 -ljsoncpp
.PHONY:clean
clean:
rm-rf calserver calclient
编译之后我们来看运行效果:
参照对应的ASCII码表:
运算符ACSII码+43-4542/47%37&3842!33
可以验证结果是正确的!!!
这样网络计算机的小项目就完成了!!!
版权归原作者 叫我龙翔 所有, 如有侵权,请联系我们删除。