自定义应用层通信协议
基于传输层TCP协议,自定义实现一个应用层协议
一:回顾JsonCpp
C++通过JsonCpp读取Json文件
网络编程字节序转换问题
二:实现自定义应用层
(一)协议分类
1.按编码方式
二进制协议:比如网络通信运输层中的tcp协议。
明文的文本协议:比如应用层的http、redis协议。
混合协议(二进制+明文):比如苹果公司早期的APNs推送协议。
2.按协议边界
固定边界协议:能够明确得知一个协议报文的长度,这样的协议易于解析,比如tcp协议。
模糊边界协议:无法明确得知一个协议报文的长度,这样的协议解析较为复杂,通常需要通过某些特定的字节来界定报文是否结束,比如http协议。
(二)协议设计
本协议采用固定边界+混合编码策略。用于传输Json数据(命令)
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(500行不到)
五:补充协议头设计
(一)如果基于UDP实现,则需要在服务端设置应答(含有包序号、返回接受的数据大小...),以防止数据丢失
(二)协议头的其它设计方案
方案1:包含大多数信息,但是出现:如果length数据丢失或者移位....
方案2:设置开始标志(同我们设置的magic标识),符合标志以后,开始解析协议
(三)数据类型type