我是靠谱客的博主 土豪黑猫,这篇文章主要介绍如何自定义传输协议?基于传输层TCP协议,自定义实现一个应用层协议一:回顾JsonCpp二:实现自定义应用层三:实现传输层TCP编程四:编译测试自定义协议,现在分享给大家,希望可以做个参考。

目录

基于传输层TCP协议,自定义实现一个应用层协议
一:回顾JsonCpp
  C++通过JsonCpp读取Json文件
  网络编程字节序转换问题
二:实现自定义应用层
  (一)协议分类
  (二)协议设计
  (三)设计协议结构
  (四)实现协议封装函数
  (五)实现协议解析函数
  (六)实现对应用层封装、解析的测试
三:实现传输层TCP编程
  (一)TCP回顾
  (二)客户端代码实现
  (三)服务器端实现
四:编译测试自定义协议
  (一)编译TCP程序
  (二)进行测试
  (三)全部代码见:GitHub(500行不到)

基于传输层TCP协议,自定义实现一个应用层协议

一:回顾JsonCpp

C++通过JsonCpp读取Json文件

网络编程字节序转换问题

二:实现自定义应用层

(一)协议分类

1.按编码方式

二进制协议:比如网络通信运输层中的tcp协议。

明文的文本协议:比如应用层的http、redis协议。

混合协议(二进制+明文):比如苹果公司早期的APNs推送协议。

2.按协议边界

固定边界协议:能够明确得知一个协议报文的长度,这样的协议易于解析,比如tcp协议。

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

(二)协议设计

本协议采用固定边界+混合编码策略。用于传输Json数据(命令)

1.协议头

8字节的定长协议头。支持版本号,基于魔数的快速校验,不同服务的复用。定长协议头使协议易于解析且高效。

2.协议体

变长json作为协议体。json使用明文文本编码,可读性强、易于扩展、前后兼容、通用的编解码算法。json协议体为协议提供了良好的扩展性和兼容性

3.协议图

(三)设计协议结构

复制代码
1
2
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; //协议头大小

复制代码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//协议头部 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; //协议体 };

复制代码

(四)实现协议封装函数

复制代码

复制代码
1
2
3
4
5
6
7
8
9
10
//协议封装类 class MyProtoEncode { public: //协议消息体封装函数:传入的pMsg里面只有部分数据,比如Json协议体,服务号,我们对消息编码后会修改长度信息,这时需要重新编码协议 uint8_t* encode(MyProtoMsg* pMsg, uint32_t& len); //返回长度信息,用于后面socket发送数据 private: //协议头封装函数 void headEncode(uint8_t* pData,MyProtoMsg* pMsg); };

复制代码

复制代码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//----------------------------------协议头封装函数---------------------------------- //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; //返回消息首部地址 }

复制代码

(五)实现协议解析函数

复制代码

复制代码
1
2
3
4
5
6
typedef enum MyProtoParserStatus //协议解析的状态 { ON_PARSER_INIT = 0, //初始状态 ON_PARSER_HEAD = 1, //解析头部 ON_PARSER_BODY = 2, //解析数据 }MyProtoParserStatus;

复制代码

复制代码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//协议解析类 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); //用于解析消息体 };

复制代码

复制代码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//----------------------------------协议解析类---------------------------------- //初始化协议解析状态 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; }

复制代码

(六)实现对应用层封装、解析的测试

复制代码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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; }

复制代码

文件结构:

编译:

复制代码
1
g++ testApp.cpp ./myproto.cpp ./lib_json/*.cpp -I ./ -o test

三:实现传输层TCP编程

(一)TCP回顾

(二)客户端代码实现

复制代码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#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 portn", argv[0]); return 0; } //开始创建socket int sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(sock < 0) { printf("socket create failuren"); 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 failuren"); 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是输出的应用层数据大小(协议头+协议体),但是没有对协议进行解码,所以无法显示!!

(三)服务器端实现

复制代码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#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_portn",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:%dn",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程序

复制代码
1
2
3
g++ tcpServer.cpp ./myproto.cpp ./lib_json/*.cpp -I ./ -o ts g++ tcpClient.cpp ./myproto.cpp ./lib_json/*.cpp -I ./ -o tc

(二)进行测试

完成自定义协议!!!

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

作者:山上有风景
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

最后

以上就是土豪黑猫最近收集整理的关于如何自定义传输协议?基于传输层TCP协议,自定义实现一个应用层协议一:回顾JsonCpp二:实现自定义应用层三:实现传输层TCP编程四:编译测试自定义协议的全部内容,更多相关如何自定义传输协议?基于传输层TCP协议内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(100)

评论列表共有 0 条评论

立即
投稿
返回
顶部