目录
基于传输层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.协议图
(三)设计协议结构
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(500行不到)
作者:山上有风景
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。