首页 > c/c++ > 手把手教你实现自定义的应用层协议

手把手教你实现自定义的应用层协议

2017年3月11日 1,227 人阅读 发表评论 阅读评论

1.简述

  • 互联网上充斥着各种各样的网络服务,在对外提供网络服务时,服务端和客户端需要遵循同一套数据通讯协议,才能正常的进行通讯;就好像你跟台湾人沟通用闽南语,跟广东人沟通就用粤语一样。
  • 实现自己的应用功能时,已知的知名协议(http,smtp,ftp等)在安全性、可扩展性等方面不能满足需求,从而需要设计并实现自己的应用层协议。

2.协议分类

2.1按编码方式

  • 二进制协议
    比如网络通信运输层中的tcp协议。
  • 明文的文本协议
    比如应用层的http、redis协议。
  • 混合协议(二进制+明文)
    比如苹果公司早期的APNs推送协议。

2.2按协议边界

  • 固定边界协议
    能够明确得知一个协议报文的长度,这样的协议易于解析,比如tcp协议。
  • 模糊边界协议
    无法明确得知一个协议报文的长度,这样的协议解析较为复杂,通常需要通过某些特定的字节来界定报文是否结束,比如http协议。

3.协议优劣的基本评判标准

  • 高效的
    快速的打包解包减少对cpu的占用,高数据压缩率降低对网络带宽的占用。
  • 简单的
    易于人的理解、程序的解析。
  • 易于扩展的
    对可预知的变更,有足够的弹性用于扩展。
  • 容易兼容的
    向前兼容,对于旧协议发出的报文,能使用新协议进行解析,只是新协议支持的新功能不能使用。
    向后兼容,对于新协议发出的报文,能使用旧协议进行解析,只是新协议支持的新功能不能使用。

4.自定义应用层协议的优缺点

4.1优点

  • 非知名协议,数据通信更安全,黑客如果要分析协议的漏洞就必须先破译你的通讯协议。
  • 扩展性更好,可以根据业务需求和发展扩展自己的协议,而已知的知名协议不好扩展。

4.2缺点

  • 设计难度高,协议需要易扩展,最好能向后向前兼容。
  • 实现繁琐,需要自己实现序列化和反序列化。

5.动手前的预备知识

5.1大小端

计算机系统在存储数据时起始地址是高地址还是低地址。

  • 大端
    从高地址开始存储。
  • 小端
    从低地址开始存储。
  • 图解
  • 判断
    这里以c/c++语言代码为例,使用了c语言中联合体的特性。
  1. #include <stdint.h>
  2. #include <iostream>
  3. using namespace std;
  4. bool bigCheck()
  5. {
  6. union Check
  7. {
  8. char a;
  9. uint32_t data;
  10. };
  11. Check c;
  12. c.data = 1;
  13. if (1 == c.a)
  14. {
  15. return false;
  16. }
  17. return true;
  18. }
  19. int main()
  20. {
  21. if (bigCheck())
  22. {
  23. cout << "big" << endl;
  24. }
  25. else
  26. {
  27. cout << "small" << endl;
  28. }
  29. return 0;
  30. }

5.2网络字节序

顾名思义就是数据在网络传送的字节流中的起始地址的高低,为了避免在网络通信中引入其他复杂性,网络字节序统一是大端的。

5.3本地字节序

本地操作系统的大小端,不同操作系统可能采用不同的字节序。

5.4内存对象与布局

任何变量,不管是堆变量还是栈变量都对应着操作系统中的一块内存,由于内存对齐的要求程序中的变量并不是紧凑存储的,例如一个c语言的结构体Test在内存中的布局可能如下图所示。

5.5序列化与反序列化

  • 将计算机语言中的内存对象转换为网络字节流,例如把c语言中的结构体Test转化成uint8_t data[10]字节流。
  • 将网络字节流转换为计算机语言中的内存对象,例如把uint8_t data[10]字节流转化成c语言中的结构体Test。

6.一个例子

6.1 协议设计

本协议采用固定边界+混合编码策略。

  • 协议头
    8字节的定长协议头。支持版本号,基于魔数的快速校验,不同服务的复用。定长协议头使协议易于解析且高效。
  • 协议体
    变长json作为协议体。json使用明文文本编码,可读性强、易于扩展、前后兼容、通用的编解码算法。json协议体为协议提供了良好的扩展性和兼容性。
  • 协议可视化图

6.2 协议实现

talk is easy,just code it,使用c/c++语言来实现。

6.2.1c/c++语言实现

  • 使用结构体MyProtoHead来存储协议头
  1. /*
  2. 协议头
  3. */
  4. struct MyProtoHead
  5. {
  6. uint8_t version; //协议版本号
  7. uint8_t magic; //协议魔数
  8. uint16_t server; //协议复用的服务号,标识协议之上的不同服务
  9. uint32_t len; //协议长度(协议头长度+变长json协议体长度)
  10. };
  1. /*
  2. 协议消息体
  3. */
  4. struct MyProtoMsg
  5. {
  6. MyProtoHead head; //协议头
  7. Json::Value body; //协议体
  8. };
  • 打包类
  1. /*
  2. MyProto打包类
  3. */
  4. class MyProtoEnCode
  5. {
  6. public:
  7. //协议消息体打包函数
  8. uint8_t * encode(MyProtoMsg * pMsg, uint32_t & len);
  9. private:
  10. //协议头打包函数
  11. void headEncode(uint8_t * pData, MyProtoMsg * pMsg);
  12. };
  • 解包类
  1. typedef enum MyProtoParserStatus
  2. {
  3. ON_PARSER_INIT = 0,
  4. ON_PARSER_HAED = 1,
  5. ON_PARSER_BODY = 2,
  6. }MyProtoParserStatus;
  7. /*
  8. MyProto解包类
  9. */
  10. class MyProtoDeCode
  11. {
  12. public:
  13. void init();
  14. void clear();
  15. bool parser(void * data, size_t len);
  16. bool empty();
  17. MyProtoMsg * front();
  18. void pop();
  19. private:
  20. bool parserHead(uint8_t ** curData, uint32_t & curLen,
  21. uint32_t & parserLen, bool & parserBreak);
  22. bool parserBody(uint8_t ** curData, uint32_t & curLen,
  23. uint32_t & parserLen, bool & parserBreak);
  24. private:
  25. MyProtoMsg mCurMsg; //当前解析中的协议消息体
  26. queue<MyProtoMsg *> mMsgQ; //解析好的协议消息队列
  27. vector<uint8_t> mCurReserved; //未解析的网络字节流
  28. MyProtoParserStatus mCurParserStatus; //当前解析状态
  29. };

6.2.2打包(序列化)

  1. void MyProtoEnCode::headEncode(uint8_t * pData, MyProtoMsg * pMsg)
  2. {
  3. //设置协议头版本号为1
  4. *pData = 1;
  5. ++pData;
  6. //设置协议头魔数
  7. *pData = MY_PROTO_MAGIC;
  8. ++pData;
  9. //设置协议服务号,把head.server本地字节序转换为网络字节序
  10. *(uint16_t *)pData = htons(pMsg->head.server);
  11. pData += 2;
  12. //设置协议总长度,把head.len本地字节序转换为网络字节序
  13. *(uint32_t *)pData = htonl(pMsg->head.len);
  14. }
  15. uint8_t * MyProtoEnCode::encode(MyProtoMsg * pMsg, uint32_t & len)
  16. {
  17. uint8_t * pData = NULL;
  18. Json::FastWriter fWriter;
  19. //协议json体序列化
  20. string bodyStr = fWriter.write(pMsg->body);
  21. //计算协议消息序列化后的总长度
  22. len = MY_PROTO_HEAD_SIZE + (uint32_t)bodyStr.size();
  23. pMsg->head.len = len;
  24. //申请协议消息序列化需要的空间
  25. pData = new uint8_t[len];
  26. //打包协议头
  27. headEncode(pData, pMsg);
  28. //打包协议体
  29. memcpy(pData + MY_PROTO_HEAD_SIZE, bodyStr.data(), bodyStr.size());
  30. return pData;
  31. }

6.2.3解包(反序列化)

  1. bool MyProtoDeCode::parserHead(uint8_t ** curData, uint32_t & curLen,
  2. uint32_t & parserLen, bool & parserBreak)
  3. {
  4. parserBreak = false;
  5. if (curLen < MY_PROTO_HEAD_SIZE)
  6. {
  7. parserBreak = true; //终止解析
  8. return true;
  9. }
  10. uint8_t * pData = *curData;
  11. //解析版本号
  12. mCurMsg.head.version = *pData;
  13. pData++;
  14. //解析魔数
  15. mCurMsg.head.magic = *pData;
  16. pData++;
  17. //魔数不一致,则返回解析失败
  18. if (MY_PROTO_MAGIC != mCurMsg.head.magic)
  19. {
  20. return false;
  21. }
  22. //解析服务号
  23. mCurMsg.head.server = ntohs(*(uint16_t*)pData);
  24. pData+=2;
  25. //解析协议消息体的长度
  26. mCurMsg.head.len = ntohl(*(uint32_t*)pData);
  27. //异常大包,则返回解析失败
  28. if (mCurMsg.head.len > MY_PROTO_MAX_SIZE)
  29. {
  30. return false;
  31. }
  32. //解析指针向前移动MY_PROTO_HEAD_SIZE字节
  33. (*curData) += MY_PROTO_HEAD_SIZE;
  34. curLen -= MY_PROTO_HEAD_SIZE;
  35. parserLen += MY_PROTO_HEAD_SIZE;
  36. mCurParserStatus = ON_PARSER_HAED;
  37. return true;
  38. }
  39. bool MyProtoDeCode::parserBody(uint8_t ** curData, uint32_t & curLen,
  40. uint32_t & parserLen, bool & parserBreak)
  41. {
  42. parserBreak = false;
  43. uint32_t jsonSize = mCurMsg.head.len - MY_PROTO_HEAD_SIZE;
  44. if (curLen < jsonSize)
  45. {
  46. parserBreak = true; //终止解析
  47. return true;
  48. }
  49. Json::Reader reader; //json解析类
  50. if (!reader.parse((char *)(*curData),
  51. (char *)((*curData) + jsonSize), mCurMsg.body, false))
  52. {
  53. return false;
  54. }
  55. //解析指针向前移动jsonSize字节
  56. (*curData) += jsonSize;
  57. curLen -= jsonSize;
  58. parserLen += jsonSize;
  59. mCurParserStatus = ON_PARSER_BODY;
  60. return true;
  61. }
  62. bool MyProtoDeCode::parser(void * data, size_t len)
  63. {
  64. if (len <= 0)
  65. {
  66. return false;
  67. }
  68. uint32_t curLen = 0;
  69. uint32_t parserLen = 0;
  70. uint8_t * curData = NULL;
  71. curData = (uint8_t *)data;
  72. //把当前要解析的网络字节流写入未解析完字节流之后
  73. while (len--)
  74. {
  75. mCurReserved.push_back(*curData);
  76. ++curData;
  77. }
  78. curLen = mCurReserved.size();
  79. curData = (uint8_t *)&mCurReserved[0];
  80. //只要还有未解析的网络字节流,就持续解析
  81. while (curLen > 0)
  82. {
  83. bool parserBreak = false;
  84. //解析协议头
  85. if (ON_PARSER_INIT == mCurParserStatus ||
  86. ON_PARSER_BODY == mCurParserStatus)
  87. {
  88. if (!parserHead(&curData, curLen, parserLen, parserBreak))
  89. {
  90. return false;
  91. }
  92. if (parserBreak) break;
  93. }
  94. //解析完协议头,解析协议体
  95. if (ON_PARSER_HAED == mCurParserStatus)
  96. {
  97. if (!parserBody(&curData, curLen, parserLen, parserBreak))
  98. {
  99. return false;
  100. }
  101. if (parserBreak) break;
  102. }
  103. if (ON_PARSER_BODY == mCurParserStatus)
  104. {
  105. //拷贝解析完的消息体放入队列中
  106. MyProtoMsg * pMsg = NULL;
  107. pMsg = new MyProtoMsg;
  108. *pMsg = mCurMsg;
  109. mMsgQ.push(pMsg);
  110. }
  111. }
  112. if (parserLen > 0)
  113. {
  114. //删除已经被解析的网络字节流
  115. mCurReserved.erase(mCurReserved.begin(), mCurReserved.begin() + parserLen);
  116. }
  117. return true;
  118. }

7.完整源码与测试

code is easy,just run it.

7.1源码

  1. #include <stdint.h>
  2. #include <stdio.h>
  3. #include <queue>
  4. #include <vector>
  5. #include <iostream>
  6. #include <string.h>
  7. #include <json/json.h>
  8. #include <arpa/inet.h>
  9. using namespace std;
  10. const uint8_t MY_PROTO_MAGIC = 88;
  11. const uint32_t MY_PROTO_MAX_SIZE = 10 * 1024 * 1024; //10M
  12. const uint32_t MY_PROTO_HEAD_SIZE = 8;
  13. typedef enum MyProtoParserStatus
  14. {
  15. ON_PARSER_INIT = 0,
  16. ON_PARSER_HAED = 1,
  17. ON_PARSER_BODY = 2,
  18. }MyProtoParserStatus;
  19. /*
  20. 协议头
  21. */
  22. struct MyProtoHead
  23. {
  24. uint8_t version; //协议版本号
  25. uint8_t magic; //协议魔数
  26. uint16_t server; //协议复用的服务号,标识协议之上的不同服务
  27. uint32_t len; //协议长度(协议头长度+变长json协议体长度)
  28. };
  29. /*
  30. 协议消息体
  31. */
  32. struct MyProtoMsg
  33. {
  34. MyProtoHead head; //协议头
  35. Json::Value body; //协议体
  36. };
  37. void myProtoMsgPrint(MyProtoMsg & msg)
  38. {
  39. string jsonStr = "";
  40. Json::FastWriter fWriter;
  41. jsonStr = fWriter.write(msg.body);
  42. printf("Head[version=%d,magic=%d,server=%d,len=%d],Body:%s",
  43. msg.head.version, msg.head.magic,
  44. msg.head.server, msg.head.len, jsonStr.c_str());
  45. }
  46. /*
  47. MyProto打包类
  48. */
  49. class MyProtoEnCode
  50. {
  51. public:
  52. //协议消息体打包函数
  53. uint8_t * encode(MyProtoMsg * pMsg, uint32_t & len);
  54. private:
  55. //协议头打包函数
  56. void headEncode(uint8_t * pData, MyProtoMsg * pMsg);
  57. };
  58. void MyProtoEnCode::headEncode(uint8_t * pData, MyProtoMsg * pMsg)
  59. {
  60. //设置协议头版本号为1
  61. *pData = 1;
  62. ++pData;
  63. //设置协议头魔数
  64. *pData = MY_PROTO_MAGIC;
  65. ++pData;
  66. //设置协议服务号,把head.server本地字节序转换为网络字节序
  67. *(uint16_t *)pData = htons(pMsg->head.server);
  68. pData += 2;
  69. //设置协议总长度,把head.len本地字节序转换为网络字节序
  70. *(uint32_t *)pData = htonl(pMsg->head.len);
  71. }
  72. uint8_t * MyProtoEnCode::encode(MyProtoMsg * pMsg, uint32_t & len)
  73. {
  74. uint8_t * pData = NULL;
  75. Json::FastWriter fWriter;
  76. //协议json体序列化
  77. string bodyStr = fWriter.write(pMsg->body);
  78. //计算协议消息序列化后的总长度
  79. len = MY_PROTO_HEAD_SIZE + (uint32_t)bodyStr.size();
  80. pMsg->head.len = len;
  81. //申请协议消息序列化需要的空间
  82. pData = new uint8_t[len];
  83. //打包协议头
  84. headEncode(pData, pMsg);