Protobuf应用广泛,尤其作为网络通讯协议最为普遍。本文将详细描述几个让人眼前一亮的protobuf协议设计,对准备应用或已经应用protobuf的开发者会有所启发,甚至可以直接拿过去用。 这里描述的协议设计被用于生产环境的即时通讯、埋点数据采集、消息推送、redis和mysql数据代理。
Bwar从2013年开始应用protobuf,2014年设计了用于mysql数据代理的protobuf协议,2015年设计了用于即时通讯的protobuf协议。高性能C++ IoC网络框架Nebula https://github.com/Bwar/Nebula把这几个protobuf协议设计应用到了极致。
1. TCP通讯协议设计
本协议设计于2015年,用于一个生产环境的IM和埋点数据采集及实时分析,2016年又延伸发展了基于protobuf3的版本并用于开源网络框架Nebula。基于protobuf2和protobuf3的有较少差别,这里分开讲解两个版本的协议设计。
1.1. protobuf2.5版Msg
2015年尚无protobuf3的release版本,protobuf2版本的fixed32类型是固定占用4个字节的,非常适合用于网络通讯协议设计。Bwar设计用于IM系统的协议包括两个protobuf message:MsgHead和MsgBody,协议定义如下:
syntax = "proto2";
/**
* @brief 消息头
*/
message MsgHead
{
required fixed32 cmd = 1 ; ///< 命令字(压缩加密算法占高位1字节)
required fixed32 msgbody_len = 2; ///< 消息体长度(单个消息体长度不能超过65535即8KB)
required fixed32 seq = 3; ///< 序列号
}
/**
* @brief 消息体
* @note 消息体主体是body,所有业务逻辑内容均放在body里。session_id和session用于接入层路由,
* 两者只需要填充一个即可,首选session_id,当session_id用整型无法表达时才使用session。
*/
message MsgBody
{
required bytes body = 1; ///< 消息体主体
optional uint32 session_id = 2; ///< 会话ID(单聊消息为接收者uid,个人信息修改为uid,群聊消息为groupid,群管理为groupid)
optional string session = 3; ///< 会话ID(当session_id用整型无法表达时使用)
optional bytes additional = 4; ///< 接入层附加的数据(客户端无须理会)
}
解析收到的字节流时先解固定长度(15字节)的MsgHead(protobuf3.0之后的版本必须在cmd、msgbody_len、seq均不为0的情况下才是15字节),再通过MsgHead里的msgbody_len判断消息体是否接收完毕,若接收完毕则调用MsgBody.Parse()解析。MsgBody里的设计在下一节详细说明。
MsgHead在实际的项目应用中对应下面的消息头并可以相互转换:
#pragma pack(1)
/**
* @brief 与客户端通信消息头
*/
struct tagClientMsgHead
{
unsigned char version; ///< 协议版本号(1字节)
unsigned char encript; ///< 压缩加密算法(1字节)
unsigned short cmd; ///< 命令字/功能号(2字节)
unsigned short checksum; ///< 校验码(2字节)
unsigned int body_len; ///< 消息体长度(4字节)
unsigned int seq; ///< 序列号(4字节)
};
#pragma pack()
转换代码如下:
E_CODEC_STATUS ClientMsgCodec::Encode(const MsgHead& oMsgHead, const MsgBody& oMsgBody, loss::CBuffer* pBuff)
{
tagClientMsgHead stClientMsgHead;
stClientMsgHead.version = 1; // version暂时无用
stClientMsgHead.encript = (unsigned char)(oMsgHead.cmd() >> 24);
stClientMsgHead.cmd = htons((unsigned short)(gc_uiCmdBit & oMsgHead.cmd()));
stClientMsgHead.body_len = htonl((unsigned int)oMsgHead.msgbody_len());
stClientMsgHead.seq = htonl(oMsgHead.seq());
stClientMsgHead.checksum = htons((unsigned short)stClientMsgHead.checksum);
...
}
E_CODEC_STATUS ClientMsgCodec::Decode(loss::CBuffer* pBuff, MsgHead& oMsgHead, MsgBody& oMsgBody)
{
LOG4_TRACE("%s() pBuff->ReadableBytes() = %u", __FUNCTION__, pBuff->ReadableBytes());
size_t uiHeadSize = sizeof(tagClientMsgHead);
if (pBuff->ReadableBytes() >= uiHeadSize)
{
tagClientMsgHead stClientMsgHead;
int iReadIdx = pBuff->GetReadIndex();
pBuff->Read(&stClientMsgHead, uiHeadSize);
stClientMsgHead.cmd = ntohs(stClientMsgHead.cmd);
stClientMsgHead.body_len = ntohl(stClientMsgHead.body_len);
stClientMsgHead.seq = ntohl(stClientMsgHead.seq);
stClientMsgHead.checksum = ntohs(stClientMsgHead.checksum);
LOG4_TRACE("cmd %u, seq %u, len %u, pBuff->ReadableBytes() %u",
stClientMsgHead.cmd, stClientMsgHead.seq, stClientMsgHead.body_len,
pBuff->ReadableBytes());
oMsgHead.set_cmd(((unsigned int)stClientMsgHead.encript << 24) | stClientMsgHead.cmd);
oMsgHead.set_msgbody_len(stClientMsgHead.body_len);
oMsgHead.set_seq(stClientMsgHead.seq);
...
}
}
<br/>
1.2. protobuf3版Msg
protobuf3版的MsgHead和MsgBody从IM业务应用实践中发展而来,同时满足了埋点数据采集、实时计算、消息推送等业务需要,更为通用。正因其通用性和高扩展性,采用proactor模型的IoC网络框架Nebula才会选用这个协议,通过这个协议,框架层将网络通信工作从业务应用中完全独立出来,基于Nebula框架的应用开发者甚至可以不懂网络编程也能开发出高并发的分布式服务。
MsgHead和MsgBody的protobuf定义如下:
syntax = "proto3";
// import "google/protobuf/any.proto";
/**
* @brief 消息头
* @note MsgHead为固定15字节的头部,当MsgHead不等于15字节时,消息发送将出错。
* 在proto2版本,MsgHead为15字节总是成立,cmd、seq、len都是required;
* 但proto3版本,MsgHead为15字节则必须要求cmd、seq、len均不等于0,否则无法正确进行收发编解码。
*/
message MsgHead
{
fixed32 cmd = 1; ///< 命令字(压缩加密算法占高位1字节)
fixed32 seq = 2; ///< 序列号
sfixed32 len = 3; ///< 消息体长度
}
/**
* @brief 消息体
* @note 消息体主体是data,所有业务逻辑内容均放在data里。req_target是请求目标,用于
* 服务端接入路由,请求包必须填充。rsp_result是响应结果,响应包必须填充。
*/
message MsgBody
{
oneof msg_type
{
Request req_target = 1; ///< 请求目标(请求包必须填充)
Response rsp_result = 2; ///< 响应结果(响应包必须填充)
}
bytes data = 3; ///< 消息体主体
bytes add_on = 4; ///< 服务端接入层附加在请求包的数据(客户端无须理会)
string trace_id = 5; ///< for log trace
message Request
{
uint32 route_id = 1; ///< 路由ID
string route = 2; ///< 路由ID(当route_id用整型无法表达时使用)
}
message Response
{
int32 code = 1; ///< 错误码
bytes msg = 2;