基于传输层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; }


文件结构:

自定义应用层通信协议_字节流_02

编译:


g++ testApp.cpp ./myproto.cpp ./lib_json/*.cpp -I ./ -o test


自定义应用层通信协议_数据_03

三:实现传输层TCP编程

(一)​​TCP回顾​

自定义应用层通信协议_字符串_04

自定义应用层通信协议_#include_05

(二)客户端代码实现


#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); }


补充:如果不进行解析,直接按照一般的服务端接收程序接收我们的自定义数据:

自定义应用层通信协议_#include_06

其中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


自定义应用层通信协议_json_07

(二)进行测试

自定义应用层通信协议_数据_08

自定义应用层通信协议_数据_09

完成自定义协议!!!

(三)全部代码见:​​GitHub​​(500行不到)

五:补充协议头设计

(一)如果基于UDP实现,则需要在服务端设置应答(含有包序号、返回接受的数据大小...),以防止数据丢失

(二)协议头的其它设计方案

方案1:包含大多数信息,但是出现:如果length数据丢失或者移位....

自定义应用层通信协议_#include_10

方案2:设置开始标志(同我们设置的magic标识),符合标志以后,开始解析协议

自定义应用层通信协议_数据_11

自定义应用层通信协议_数据_12

(三)数据类型type

自定义应用层通信协议_数据_13