news 2026/4/15 12:38:52

应用层自定义协议

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
应用层自定义协议
  • 自定义协议和序列化
    • 面向字节流
    • 全双工
    • 自定义协议
    • 序列化
  • IO模块
    • socket封装
    • Server
    • service
  • Cal
  • Client
  • 完整代码

接下来我们实现一个网络计算器功能。

自定义协议和序列化

前面我们的UDP套接字编程和TCP套接字编程都实现了应用层,但没有实现协议。是否会疑惑有没有协议有何不同?

事实上我们这里要详细谈一谈何为面向字节流,以及引出的为什么read、write、recv、send等函数支持全双工。毕竟我们之前实现的TCP的echo时都是read之后直接write,你们不觉得很诡异吗?难道不会覆盖别的用户的数据?

面向字节流


我们之前说过,所谓协议就是大家都约定好的、都看得懂的结构化数据。但是我们传输过程是面向字节流的,因此这些结构化数据要全部转化成字节串,这个过程称为序列化。随后获取数据之后,也要从序列化数据反序列化得到相应的结构化数据,这个过程称为反序列化

全双工


如上图,这些系统调用能支持全双工的根本原因就是他们有两个内核缓冲区。接收和发送调用的是不同的缓冲区,因此不会发生相互覆盖的现象。

所以我们得出以下结论:

  1. read、write、recv、send本质就是拷贝函数
  2. 发送数据的本质是:从发送方的发送缓冲区把数据通过协议栈和网络拷贝给接收方的接收缓冲区
  3. tcp协议支持全双工和传输控制的原因如上
  4. 每个缓冲区都有人写入和读取,这其实就是一个生产者消费者模型
  5. 那么IO函数阻塞目的就是维持同步

自定义协议


我们的发送和接收消息过程是这样,但我们有个问题。
这些信息在缓冲区中什么时候发送、发送多少、出错了怎么办?
这些都是由TCP控制的。

但是这样面向字节流传输数据就会出现一些问题,因为我们写入缓冲区的都是一些字节流,TCP不会知道完整报文是多大,所以读取的时候会出现报文不完整的现象。
因此我们要确保读取的时候是完整的报文,否则不做处理。
那么这样该如何实现呢?
这就要依靠我们应用层的协议了,我们要给报文加上报头,通过报头确定我们的报文大小,然后读取报文。

例如我们设计一个很简单的报头:
“len”\r\n
这里len是有效载荷的大小,\r\n则是标识符。Find到标识符证明读取到了报头,那么我们再根据len来读取有效载荷:

根据上面逻辑,我们写入数据和读取数据前应该做:

序列化

上面添加报头和去报头都是针对字节流的,那么我们如何将结构化数据序列化呢?
这个过程并不困难,我们也可以自己实现。
当然也可以调用已经实现的库函数,这里调用jsoncpp来序列化和反序列化。

Jsoncpp 是一个用于处理JSON数据的C++库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。

特性:

  1. 简单易用:Jsoncpp提供了直观的API,使得处理JSON数据变得简单。
  2. 高性能:Jsoncpp的性能经过优化,能够高效地处理大量JSON数据。
  3. 全面支持:支持JSON标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和null。
  4. 错误处理:在解析JSON数据时,Jsoncpp提供了详细的错误信息和位置,方便开发者调试。

那么我们要现在Ubuntu上安装这个第三方库:

sudoapt-getinstalllibjsoncpp-dev

具体函数调用接口我们以后再详谈,这里直接使用吧。

可以看到我们序列化的逻辑非常直观。
反序列化逻辑依旧显然:

就这样我们实现结构化的发送数据和接收数据封装:
Request:

classRequest{public:Request(){}Request(intx,inty,charoper):_x(x),_y(y),_oper(oper){}~Request(){}boolSerialize(std::string*out){Json::Value root;root["x"]=_x;root["y"]=_y;root["oper"]=_oper;Json::FastWriter writer;std::string s=writer.write(root);*out=s;returntrue;}boolDeserialize(conststd::string&in){Json::Value root;Json::Reader reader;boolres=reader.parse(in,root);_x=root["x"].asInt();_y=root["y"].asInt();_oper=root["oper"].asInt();returnres;}voidPrint(){std::cout<<_x<<std::endl;std::cout<<_y<<std::endl;std::cout<<_oper<<std::endl;}intX(){return_x;}intY(){return_y;}charOper(){return_oper;}voidSetValue(intx,inty,charoper){_x=x;_y=y;_oper=oper;}private:int_x;int_y;char_oper;};

Response:

classResponse{public:Response():_result(0),_code(0),_desc("success"){}boolSerialize(std::string*out){Json::Value root;root["result"]=_result;root["code"]=_code;root["desc"]=_desc;Json::FastWriter writer;std::string s=writer.write(root);*out=s;returntrue;}boolDeserialize(conststd::string&in){Json::Value root;Json::Reader reader;boolres=reader.parse(in,root);_result=root["result"].asInt();_code=root["code"].asInt();_desc=root["desc"].asString();returnres;}voidPrintResult(){std::cout<<"result: "<<_result<<", code: "<<_code<<", desc: "<<_desc<<std::endl;}~Response(){}public:int_result;int_code;std::string _desc;};

这里为了方便操作,我们将Response的成员变量设成公有的。

然后我们还可以实现一个工厂模式快速生成智能指针:

IO模块

socket封装

在实现IO模块前,我们可以先对Socket进行封装,毕竟我们用socket很多都是固定的方法。
我们这里采用模板方法类的形式封装不同协议下的套接字类:

其中using SockSPtr = std::shared_ptr<Socket>;
接下来我们实现Tcp的套接字封装:
CreateSocketOrDie

voidCreateSocketOrDie()override{_sockfd=::socket(AF_INET,SOCK_STREAM,0);if(_sockfd<0){LOG(FATAL,"socket create error\n");exit(SOCKET_ERROR);}LOG(INFO,"socket create success,sockfd:%d\n",_sockfd);}

CreateBindOrDie

voidCreateBindOrDie(uint16_tport)override{structsockaddr_inlocal;memset(&local,0,sizeof(local));local.sin_family=AF_INET;local.sin_port=htons(port);local.sin_addr.s_addr=INADDR_ANY;if(::bind(_sockfd,(structsockaddr*)&local,sizeof(local))<0){LOG(FATAL,"bind error\n");exit(BIND_ERROR);}LOG(INFO,"bind success,sockfd:%d\n",_sockfd);}

CreateListenOrDie

voidCreateListenOrDie(intbacklog=gblcklog)override{if(::listen(_sockfd,gblcklog)<0){LOG(FATAL,"listen error\n");exit(LISTEN_ERR);}LOG(INFO,"listen success\n");}

Accepter

SockSPtrAccepter(InetAddr*cliaddr)override{structsockaddr_inclient;socklen_t len=sizeof(client);intsockfd=::accept(_sockfd,(structsockaddr*)&client,&len);if(sockfd<0){LOG(WARNING,"accept error\n");returnnullptr;}*cliaddr=InetAddr(client);LOG(INFO,"get a new link ,client info :%s,sockfd is :%d\n",cliaddr->AddrStr().c_str(),sockfd);returnstd::make_shared<TcpSocket>(sockfd);}

Conntecor

boolConntecor(conststd::string&peerip,uint16_tpeerport)override{structsockaddr_inserver;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(peerport);::inet_pton(AF_INET,peerip.c_str(),&server.sin_addr);if(::connect(_sockfd,(structsockaddr*)&server,sizeof(server))<0){returnfalse;}returntrue;}

Recv

ssize_tRecv(std::string*out)override{charbuffer[4096];ssize_t n=::recv(_sockfd,buffer,sizeof(buffer)-1,0);if(n>0){buffer[n]=0;// 注意是+=,对应读取报文的逻辑*out+=buffer;}returnn;}

需要注意是+=,对应读取报文的逻辑。
Send

ssize_tSend(conststd::string&in)override{return::send(_sockfd,in.c_str(),in.size(),0);}

然后我们还可以实现一些集成的方法:

Server

封装完Socket就可以实现服务端逻辑了。这个和以前我们实现的类似,就是改用我们封装过的Socket罢了。
首先还是简单的初始化:

然后我们实现的是多线程版本,还需要封装一个内部类:

classThreadData{public:SockSPtr _sockfd;TcpServer*_self;InetAddr _addr;public:ThreadData(SockSPtr sockfd,TcpServer*self,constInetAddr&addr):_sockfd(sockfd),_self(self),_addr(addr){}};

最后就是执行逻辑:

很好,那么我们待会实现具体的service逻辑。先处理一下Main函数逻辑:

那么接下来我们先实现service的读取报文逻辑。

service

首先我们要确定的我们的回调函数:

传入指令,然后返回对应的结果。
然后根据我们刚才实现的顺序进行io:

voidIOExcute(SockSPtr sock,InetAddr&addr){std::string packagestreamqueue;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;}//2.报文解析,提取报头和有效载荷std::string package=Decode(packagestreamqueue);if(package.empty())continue;//此时读到了完整报文autoreq=Factory::BuildRequestDefault();//3.反序列化req->Deserialize(package);//4.业务处理autoresp=_process(req);//5.序列化应答std::string respjson;resp->Serialize(&respjson);//6.添加报头respjson=Encode(respjson);//7.发送回去sock->Send(respjson);}}

Cal

接下来就是业务处理逻辑。
这个其实很简单,我们学c语言的时候就会做了。

classNetCal{public:NetCal(){}~NetCal(){}std::shared_ptr<Response>Calculator(std::shared_ptr<Request>req){autoresp=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="div 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;}returnresp;}};

Client

最后我们实现客户端代码,也是很简单的逻辑

#include<iostream>#include<memory>#include<unistd.h>#include"Socket.hpp"#include"Protocol.hpp"usingnamespacesocket_ns;intmain(intargc,char*argv[]){if(argc!=3){std::cerr<<"Usage:"<<argv[0]<<"server-ip server-port"<<std::endl;exit(0);}std::string serverip=argv[1];uint16_tserverport=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());conststd::string opers="+-*/%&^!";intcnt=3;std::string packagestreamqueue;while(true){// 构建数据intx=rand()%10;usleep(x*1000);inty=rand()%10;usleep(x*y*100);charoper=opers[y%opers.size()];// 构建请求autoreq=Factory::BuildRequestDefault();req->SetValue(x,y,oper);// 1. 序列化std::string reqstr;req->Serialize(&reqstr);// 2. 添加长度报头字段reqstr=Encode(reqstr);std::cout<<"####################################"<<std::endl;std::cout<<"request string: \n"<<reqstr<<std::endl;// 3. 发送数据sock->Send(reqstr);while(true){// 4. 读取应答,responsessize_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. 反序列化autoresp=Factory::BuildResponseDefault();resp->Deserialize(package);// 7. 打印结果resp->PrintResult();break;}sleep(1);}sock->Close();return0;}

运行结果:

完整代码

完整代码奉上

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/13 5:39:49

8个AI论文工具,助继续教育学生轻松完成写作!

8个AI论文工具&#xff0c;助继续教育学生轻松完成写作&#xff01; AI 工具如何让论文写作更高效 在当前的继续教育环境中&#xff0c;学生和科研工作者面临越来越多的写作任务&#xff0c;而传统的写作方式往往耗时耗力。随着 AI 技术的不断进步&#xff0c;AI 工具正逐渐成为…

作者头像 李华
网站建设 2026/4/15 10:16:16

国内仿真云平台哪家强?该如何选择?

在数字化与智能化浪潮的推动下&#xff0c;仿真技术已成为工业设计、科学研究和教育培训等领域的重要工具。随着云计算技术的普及&#xff0c;仿真云平台凭借其弹性计算、高效协作和成本优化等优势&#xff0c;受到越来越多企业与机构的青睐。然而&#xff0c;面对国内市场上众…

作者头像 李华
网站建设 2026/4/9 23:39:19

设备OAuth2令牌过期致认证失败 后来启用自动刷新+双令牌热备

&#x1f493; 博客主页&#xff1a;塔能物联运维的CSDN主页 目录 物联网运维&#xff1a;当我的扫地机器人开始叛逆 物联网运维的日常崩溃 运维自动化&#xff1a;让扫地机器人学会自愈 真实案例&#xff1a;某次失败的智能马桶维修 我的物联网运维三原则 那些年我们踩过的坑 …

作者头像 李华
网站建设 2026/4/12 20:06:21

文章里用了AI生成图片会被谷歌降权吗?

谷歌并不会单纯因为图片是AI生成而惩罚内容​​&#xff0c;真正触发降权的是错误的使用方式。例如&#xff1a;同一套AI模板反复配图、图片加载过慢拖累用户体验&#xff0c;或图文完全脱节被判定为“低质内容”。本文基于谷歌《网页质量指南》和实际流量数据测试&#xff0c;…

作者头像 李华
网站建设 2026/4/14 18:50:03

GraniStudio:相机采图例程

1.文件运行 导入工程 双击运行桌面GraniStudio.exe。 通过引导界面导入相机采图例程&#xff0c;点击导入按钮。 打开相机采图例程所在路径&#xff0c;选中相机采图.gsp文件&#xff0c;点击打开&#xff0c;完成导入。 2.功能说明 实现海康相机连接以及单次采图显示。 注意…

作者头像 李华
网站建设 2026/4/14 8:41:02

模块化智能革命:Deepoc开发板如何成为智慧厨房的“万能AI引擎”

当一块小小的开发板能让任何厨电秒变“智能厨师”&#xff0c;我们正在见证智能家居进入模块化、普惠化的新阶段在智能家居领域&#xff0c;一个核心矛盾始终存在&#xff1a;用户期待的是真正懂需求的智能体验&#xff0c;而市场上大多是无法理解上下文、功能僵化的“伪智能”…

作者头像 李华