自定义应用层通信协议

时间: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