自定义应用层通信协议
时间:2020-11-21
本文章向大家介绍自定义应用层通信协议,主要包括自定义应用层通信协议使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
基于传输层TCP协议,自定义实现一个应用层协议
一:回顾JsonCpp
C++通过JsonCpp读取Json文件
网络编程字节序转换问题
二:实现自定义应用层
(一)协议分类
1.按编码方式
二进制协议:比如网络通信运输层中的tcp协议。
明文的文本协议:比如应用层的http、redis协议。
混合协议(二进制+明文):比如苹果公司早期的APNs推送协议。
2.按协议边界
固定边界协议:能够明确得知一个协议报文的长度,这样的协议易于解析,比如tcp协议。
模糊边界协议:无法明确得知一个协议报文的长度,这样的协议解析较为复杂,通常需要通过某些特定的字节来界定报文是否结束,比如http协议。
(二)协议设计
本协议采用固定边界+混合编码策略。
1.协议头
8字节的定长协议头。支持版本号,基于魔数的快速校验,不同服务的复用。定长协议头使协议易于解析且高效。
2.协议体
变长json作为协议体。json使用明文文本编码,可读性强、易于扩展、前后兼容、通用的编解码算法。json协议体为协议提供了良好的扩展性和兼容性
3.协议图
(三)设计协议结构
const uint8_t MY_PROTO_MAGIC = 8; //协议魔数:通过魔数进行简单对比校验,也可以像之前学的CRC校验替换 const uint32_t MY_PROTO_MAX_SIZE = 10*1024*1024; //10M协议中数据最大 const uint32_t MY_PROTO_HEAD_SIZE = 8; //协议头大小
//协议头部 struct MyProtoHead { uint8_t version; //协议版本号 uint8_t magic; //协议魔数 uint16_t server; //协议复用的服务号,用于标识协议中的不同服务,比如向服务器获取get 设置set 添加add ... 都是不同服务(由我们指定) uint32_t len; //协议长度(协议头部+变长json协议体=总长度) }; //协议消息体 struct MyProtoMsg { MyProtoHead head; //协议头 Json::Value body; //协议体 };
(四)实现协议封装函数
//协议封装类 class MyProtoEncode { public: //协议消息体封装函数:传入的pMsg里面只有部分数据,比如Json协议体,服务号,我们对消息编码后会修改长度信息,这时需要重新编码协议 uint8_t* encode(MyProtoMsg* pMsg, uint32_t& len); //返回长度信息,用于后面socket发送数据 private: //协议头封装函数 void headEncode(uint8_t* pData,MyProtoMsg* pMsg); };
//----------------------------------协议头封装函数---------------------------------- //pData指向一个新的内存,需要pMsg中数据对pData进行填充 void MyProtoEncode::headEncode(uint8_t* pData,MyProtoMsg* pMsg) { //设置协议头版本号为1 *pData = 1; ++pData; //向前移动一个字节位置到魔数 //设置协议头魔数 *pData = MY_PROTO_MAGIC; //用于简单校验数据,只要发送方和接受方的魔数号一致,则接受认为数据正常 ++pData; //向前移动一个字节位置,到server服务字段(16位大小) //设置协议服务号,服务号,用于标识协议中的不同服务,比如向服务器获取get 设置set 添加add ... 都是不同服务(由我们指定) //外部设置,存放在pMsg中,其实可以不用修改,直接跳过该地址 *(uint16_t*)pData = pMsg->head.server; //原文是打算转换为网络字节序(但是没必要)网络中不会查看应用层数据的 pData+=2; //向前移动两个字节,到len长度字段 //设置协议头长度字段(协议头+协议消息体),其实在消息体编码中已经被修正了,这里也可以直接跳过 *(uint32_t*)pData = pMsg->head.len; //原文也是进行了字节序转化,无所谓了。反正IP网络层也不看 } //协议消息体封装函数:传入的pMsg里面只有部分数据,比如Json协议体,服务号,版本号,我们对消息编码后会修改长度信息,这时需要重新编码协议 //len返回长度信息,用于后面socket发送数据 uint8_t* MyProtoEncode::encode(MyProtoMsg* pMsg, uint32_t& len) { uint8_t* pData = NULL; //用于开辟新的空间,存放编码后的数据 Json::FastWriter fwriter; //读取Json::Value数据,转换为可以写入文件的字符串 //协议Json体序列化 string bodyStr = fwriter.write(pMsg->body); //计算消息序列化以后的新长度 len = MY_PROTO_HEAD_SIZE + (uint32_t)bodyStr.size(); pMsg->head.len = len; //一会编码协议头部时,会用到 //申请一块新的空间,用于保存消息(这里可以不用,直接使用原来空间也可以) pData = new uint8_t[len]; //编码协议头 headEncode(pData,pMsg); //函数内部没有通过二级指针修改pData的数据,修改的是临时数据 //打包协议体 memcpy(pData+MY_PROTO_HEAD_SIZE,bodyStr.data(),bodyStr.size()); return pData; //返回消息首部地址 }
(五)实现协议解析函数
typedef enum MyProtoParserStatus //协议解析的状态 { ON_PARSER_INIT = 0, //初始状态 ON_PARSER_HEAD = 1, //解析头部 ON_PARSER_BODY = 2, //解析数据 }MyProtoParserStatus;
//协议解析类 class MyProtoDecode { private: MyProtoMsg mCurMsg; //当前解析中的协议消息体 queue<MyProtoMsg*> mMsgQ; //解析好的协议消息队列 vector<uint8_t> mCurReserved; //未解析的网络字节流,可以缓存所有没有解析的数据(按字节) MyProtoParserStatus mCurParserStatus; //当前接受方解析状态 public: void init(); //初始化协议解析状态 void clear(); //清空解析好的消息队列 bool empty(); //判断解析好的消息队列是否为空 void pop(); //出队一个消息 MyProtoMsg* front(); //获取一个解析好的消息 bool parser(void* data,size_t len); //从网络字节流中解析出来协议消息,len是网络中的字节流长度,通过socket可以获取 private: bool parserHead(uint8_t** curData,uint32_t& curLen, uint32_t& parserLen,bool& parserBreak); //用于解析消息头 bool parserBody(uint8_t** curData,uint32_t& curLen, uint32_t& parserLen,bool& parserBreak); //用于解析消息体 };
//----------------------------------协议解析类---------------------------------- //初始化协议解析状态 void MyProtoDecode::init() { mCurParserStatus = ON_PARSER_INIT; } //清空解析好的消息队列 void MyProtoDecode::clear() { MyProtoMsg* pMsg=NULL; while(!mMsgQ.empty()) { pMsg = mMsgQ.front(); delete pMsg; mMsgQ.pop(); } } //判断解析好的消息队列是否为空 bool MyProtoDecode::empty() { return mMsgQ.empty(); } //出队一个消息 void MyProtoDecode::pop() { mMsgQ.pop(); } //获取一个解析好的消息 MyProtoMsg* MyProtoDecode::front() { return mMsgQ.front(); } //从网络字节流中解析出来协议消息,len由socket函数recv返回 bool MyProtoDecode::parser(void* data,size_t len) { if(len<=0) return false; uint32_t curLen = 0; //用于保存未解析的网络字节流长度(是对vector) uint32_t parserLen = 0; //保存vector中已经被解析完成的字节流,一会用于清除vector中数据 uint8_t* curData = NULL; //指向data,当前未解析的网络字节流 curData = (uint8_t*)data; //将当前要解析的网络字节流写入到vector中 while(len--) { mCurReserved.push_back(*curData); ++curData; } curLen = mCurReserved.size(); curData = (uint8_t*)&mCurReserved[0]; //获取数据首地址 //只要还有未解析的网络字节流,就持续解析 while(curLen>0) { bool parserBreak = false; //解析头部 if(ON_PARSER_INIT == mCurParserStatus || //注意:标识很有用,当数据没有完全达到,会等待下一次接受数据以后继续解析头部 ON_PARSER_BODY == mCurParserStatus) //可以进行头部解析 { if(!parserHead(&curData,curLen,parserLen,parserBreak)) return false; if(parserBreak) break; //退出循环,等待下一次数据到达,一起解析头部 } //解析完成协议头,开始解析协议体 if(ON_PARSER_HEAD == mCurParserStatus) { if(!parserBody(&curData,curLen,parserLen,parserBreak)) return false; if(parserBreak) break; } //如果成功解析了消息,就把他放入消息队列 if(ON_PARSER_BODY == mCurParserStatus) { MyProtoMsg* pMsg = NULL; pMsg = new MyProtoMsg; *pMsg = mCurMsg; mMsgQ.push(pMsg); } if(parserLen>0) { //删除已经被解析的网络字节流 mCurReserved.erase(mCurReserved.begin(),mCurReserved.begin()+parserLen); } return true; } } //用于解析消息头 bool MyProtoDecode::parserHead(uint8_t** curData,uint32_t& curLen, uint32_t& parserLen,bool& parserBreak) { if(curLen < MY_PROTO_HEAD_SIZE) { parserBreak = true; //由于数据没有头部长,没办法解析,跳出即可 return true; //但是数据还是有用的,我们没有发现出错,返回true。等待一会数据到了,再解析头部。由于标志没变,一会还是解析头部 } uint8_t* pData = *curData; //从网络字节流中,解析出来协议格式数据。保存在MyProtoMsg mCurMsg; //当前解析中的协议消息体 //解析出来版本号 mCurMsg.head.version = *pData; pData++; //解析出用于校验的魔数 mCurMsg.head.magic = *pData; pData++; //判断校验信息 if(MY_PROTO_MAGIC != mCurMsg.head.magic) return false; //数据出错 //解析服务号 mCurMsg.head.server = *(uint16_t*)pData; pData+=2; //解析协议消息体长度 mCurMsg.head.len = *(uint32_t*)pData; //判断数据长度是否超过指定的大小 if(mCurMsg.head.len > MY_PROTO_MAX_SIZE) return false; //将解析指针向前移动到消息体位置,跳过消息头大小 (*curData) += MY_PROTO_HEAD_SIZE; curLen -= MY_PROTO_HEAD_SIZE; parserLen += MY_PROTO_HEAD_SIZE; mCurParserStatus = ON_PARSER_HEAD; return true; } //用于解析消息体 bool MyProtoDecode::parserBody(uint8_t** curData,uint32_t& curLen, uint32_t& parserLen,bool& parserBreak) { uint32_t JsonSize = mCurMsg.head.len - MY_PROTO_HEAD_SIZE; //消息体的大小 if(curLen<JsonSize) { parserBreak = true; //数据还没有完全到达,我们还要等待一会数据到了,再解析消息体。由于标志没变,一会还是解析消息体 return true; } Json::Reader reader; //Json解析类 if(!reader.parse((char*)(*curData), (char*)((*curData)+JsonSize),mCurMsg.body,false)) //false表示丢弃注释 return false; //解析数据到body中 //数据指针向前移动 (*curData)+=JsonSize; curLen -= JsonSize; parserLen += JsonSize; mCurParserStatus = ON_PARSER_BODY; return true; }
(六)实现对应用层封装、解析的测试
int main(int argc,char* argv[]) { uint32_t len=0; uint8_t* pData = NULL; MyProtoMsg msg1; MyProtoMsg msg2; MyProtoDecode myDecode; MyProtoEncode myEncode; //------放入第一个消息 msg1.head.server = 1; msg1.body["op"] = "set"; msg1.body["key"] = "id"; msg1.body["value"] = "6666"; pData = myEncode.encode(&msg1,len); myDecode.init(); if(!myDecode.parser(pData,len)) { cout<<"parser msg1 failed!"<<endl; } else { cout<<"parser msg1 successful!"<<endl; } //------放入第二个消息 msg2.head.server = 2; msg2.body["op"] = "get"; msg2.body["key"] = "id"; pData = myEncode.encode(&msg2,len); if(!myDecode.parser(pData,len)) { cout<<"parser msg2 failed!"<<endl; } else { cout<<"parser msg2 successful!"<<endl; } //------解析两个消息 MyProtoMsg* pMsg = NULL; while(!myDecode.empty()) { pMsg = myDecode.front(); printMyProtoMsg(*pMsg); myDecode.pop(); } return 0; }
文件结构:
编译:
g++ testApp.cpp ./myproto.cpp ./lib_json/*.cpp -I ./ -o test
三:实现传输层TCP编程
(一)TCP回顾
(二)客户端代码实现
#include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <string.h> #include "myproto.h" int myprotoSend(int sock); int main(int argc,char* argv[]) { if(argc != 3) { printf("USage:%s ip port\n", argv[0]); return 0; } //开始创建socket int sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(sock < 0) { printf("socket create failure\n"); return -1; } //使用connect与服务器地址,端口连接,需要定义服务端信息:地址结构体 struct sockaddr_in server; server.sin_family = AF_INET; //IPV4 server.sin_port = htons(atoi(argv[2])); //atoi将字符串转数字 server.sin_addr.s_addr = inet_addr(argv[1]); //不直接使用htonl,因为传入的是字符串IP地址,使用inet_addr正好对字符串IP,转网络大端所用字节序 unsigned int len = sizeof(struct sockaddr_in); //获取socket地址结构体长度 if(connect(sock,(struct sockaddr*)&server,len)<0) { printf("socket connect failure\n"); return -2; } //连接成功,进行数据发送-------------这里可以改为循环发送 len = myprotoSend(sock); close(sock); return 0; } int myprotoSend(int sock) //-----------这里改为字符串解析,发送自己解析的Json数据 { uint32_t len=0; uint8_t* pData = NULL; MyProtoMsg msg1; MyProtoEncode myEncode; //------放入消息 msg1.head.server = 1; msg1.body["op"] = "set"; msg1.body["key"] = "id"; msg1.body["value"] = "6666"; pData = myEncode.encode(&msg1,len); return send(sock,pData,len,0); }
补充:如果不进行解析,直接按照一般的服务端接收程序接收我们的自定义数据:
其中47是输出的应用层数据大小(协议头+协议体),但是没有对协议进行解码,所以无法显示!!
(三)服务器端实现
#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<stdlib.h> #include<unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include "myproto.h" int startup(char* _port,char* _ip); int myprotoRecv(int sock,char* buf,int max_len); int main(int argc,char* argv[]) { if(argc!=3) { printf("Usage:%s local_ip local_port\n",argv[0]); return 1; } //获取监听socket信息 int listen_sock = startup(argv[2],argv[1]); //设置结构体,用于接收客户端的socket地址结构体 struct sockaddr_in remote; unsigned int len = sizeof(struct sockaddr_in); while(1) { //开始阻塞方式接收客户端链接 int sock = accept(listen_sock,(struct sockaddr*)&remote,&len); if(sock<0) { printf("client accept failure!\n"); continue; } //开始接收客户端消息 printf("get connect from %s:%d\n",inet_ntoa(remote.sin_addr),ntohs(remote.sin_port)); //inet_ntoa将网络地址转换成“.”点隔的字符串格式 char buf[1024]; len = myprotoRecv(sock,buf,1024); //len复用,这里作为接收长度------这里可以改为循环 close(sock); } return 0; } int startup(char* _port,char* _ip) { int sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(sock < 0) { printf("socket create failure!\n"); exit(-1); } //绑定服务端的地址信息,用于监听当前服务的某网卡、端口 struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(atoi(_port)); local.sin_addr.s_addr = inet_addr(_ip); int len = sizeof(local); if(bind(sock,(struct sockaddr*)&local,len)<0) { printf("socket bind failure!\n"); exit(-2); } //开始监听sock,设置同时并发数量 if(listen(sock,5)<0) //允许最大连接数量5 { printf("socket listen failure!\n"); exit(-3); } return sock; //返回文件句柄 } int myprotoRecv(int sock,char* buf,int max_len) { unsigned int len; len = recv(sock,buf,sizeof(char)*max_len,0); MyProtoDecode myDecode; myDecode.init(); if(!myDecode.parser(buf,len)) { cout<<"parser msg failed!"<<endl; } else { cout<<"parser msg successful!"<<endl; } //------解析消息 MyProtoMsg* pMsg = NULL; while(!myDecode.empty()) { pMsg = myDecode.front(); printMyProtoMsg(*pMsg); myDecode.pop(); } return len; } /* inet_addr 将字符串形式的IP地址 -> 网络字节顺序 的整型值 inet_ntoa 网络字节顺序的整型值 ->字符串形式的IP地址 */
四:编译测试自定义协议
(一)编译TCP程序
g++ tcpServer.cpp ./myproto.cpp ./lib_json/*.cpp -I ./ -o ts g++ tcpClient.cpp ./myproto.cpp ./lib_json/*.cpp -I ./ -o tc
(二)进行测试
完成自定义协议!!!
(三)全部代码见:GitHub
原文地址:https://www.cnblogs.com/ssyfj/p/14016931.html
- 使用ASP.NET MVC2+PDF.NET 构建一个简单的新闻管理程序 示例过程
- 【开源】QuickPager ASP.NET2.0分页控件V2.0.0.3 【增加了使用说明】
- Android中Java和JavaScript交互
- Android UI控件系列:TabWidget(切换卡)
- 在Linux系统运行WinForm程序
- 将ZIP文件添加到程序集资源文件然后在运行时解压文件
- Android中App安装位置详解
- Java面试题系列之基础部分(二)——每天学5个问题
- Java面试题系列之基础部分(四)——每天学5个问题
- 使用ORM框架,必须迁就数据库的设计吗?
- 使用OQL+SQLMAP解决ORM多表复杂的查询问题
- PostgreSQL的.NET驱动程序Npgsql中参数对象的一个Bug
- 和Emoji相关的那些开源项目
- PostgreSQL的PDF.NET驱动程序构建过程
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 面试再问 HashMap,求你把这篇文章发给他!
- FestIN:一款功能强大的S3 Buckets数据内容搜索工具
- 一天一大 leet(地下城游戏)难度:困难-Day20200712
- 【MongoDB】mongodb4.4版本新特性
- 一天一大 leet(数组中的第 K 个最大元素)难度:中等 DAY-29
- 线程之生产者消费者模式
- Redis学习笔记 -- 2
- 一天一大 leet(单词拆分)难度:中等 DAY-25
- 多线程必考的「生产者 - 消费者」模型,看乔戈里这篇文章就够了
- 一天一大 leet(三角形最小路径和)难度:中等-Day20200715
- 一天一大 leet(将有序数组转换为二叉搜索树)难度:简单-Day20200703
- 一天一大 leet(缺失的第一个正数)难度:困难DAY-27
- 【玩转Redis面试第3讲】一次性将Redis RDB持久化和AOF持久化讲透
- 一天一大 leet(用两个栈实现队列)难度:简单 DAY-30
- SpringBoot实战:整合Redis、mybatis,封装RedisUtils工具类等(附源码)