• 自定义应用层通信协议


    基于传输层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
    ", argv[0]);
            return 0;
        }
    
        //开始创建socket
        int sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sock < 0)
        {
            printf("socket create failure
    ");
            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
    ");
            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
    ",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!
    ");
                continue;
            }
            //开始接收客户端消息
            printf("get connect from %s:%d
    ",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!
    ");
            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!
    ");
            exit(-2);
        }
    
        //开始监听sock,设置同时并发数量
        if(listen(sock,5)<0) //允许最大连接数量5
        {
            printf("socket listen failure!
    ");
            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行不到)

  • 相关阅读:
    【vue】vue +element 搭建项目,vue-cli 如何打包上线
    【移动端】单位em相关资料
    管道 |、|&、tee
    重定向
    Bash快捷键
    man 与 help
    linux磁盘分区、格式化、挂载
    目录(cd mkdir rmdir rm pwd ls) 文件(ln touch mv rm cat more head rail) 文件权限(chmod chown chgrp) 文件通配符(* ? [])
    用户环境变量 shell变量 别名
    用户、组和身份认证
  • 原文地址:https://www.cnblogs.com/ssyfj/p/14016931.html
Copyright © 2020-2023  润新知