周日本来要去爬山的,但是没去成,突然想写点东西,但本人文采不好,只能闲扯一点技术方面的文章,整理了下有道笔记,然后最近一直在开发protobuf的协议接口,就写写ProtoBuf相关的东西吧。
本文精髓:
protobuf的消息设计
消息分发设计Message Dispatch
针对程序升级的proto设计
文章末尾对着三点做详细说明。
最近一段时间在写linux服务端接口程序,刚开始如果按照需求的话,大概有十个接口左右,但是后面慢慢分解需求后,其实真正就只有五个接口。
编码工作早早完成,进入到测试阶段,一般情况可能会等web实现完成后,然后借助web client,在做调试,但是这期间工作效率不高,而且存在很多问题,可能后台接口没实现好,也有可能web没实现好。作为linux后台服务开发,需要会模拟客户单发送数据,如果接口很简单的话,可以直接使用telnet工具。
现在最为流行的后台服务端通信的协议有:JSON、XML、ProtoBuf,当协议为这三种的时候,简单的telnet就不能胜任了,使用JSON和ProtoBuf的话,先借助工具做序列化工作,使用XML的话,也要事先编写好XML。
为了更加高效的对后台服务接口做好单元测试,也为了在web开发调试的时候,提供稳定的后台服务接口,自己利用MFC写了一个小的调试工具
参数设置里面输入的是json串,因为这个有很多工具方便序列化,如:https://www.bejson.com/jsoneditoronline/
这个工具很简单,从界面就三个输入,IP、port、消息类型,主要工作就是模拟客户端向服务端发送消息:
需要借用服务端的proto协议文件
序列化protobuf
根据消息类型做消息分发
这里用到protobuf,先大概说一下它的使用与原理
1,介绍安装
直接去百度,这里就跳过
2,编写 .proto文件
来个例子:
package lm;
message helloworld
{
required int32 id = 1; // ID
required string str = 2; // str
optional int32 opt = 3; //optional field
}
限定修饰符 + 数据类型 + 字段名称 = 字段编码值
尽量养成良好测编程习惯,对proto文件命名与消息名做规范的命名
在上例中,package 名字叫做 lm,定义了一个消息 helloworld,该消息有三个成员,类型为 int32 的 id,另一个为类型为 string 的成员 str。opt 是一个可选的成员,即消息中可以不包含该成员。
3,编译.proto文件
写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。本例中我们将使用 C++。假设您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
命令将生成两个文件:
lm.helloworld.pb.h , 定义了 C++ 类的头文件
lm.helloworld.pb.cc , C++ 类的实现文件
在生成的头文件中,定义了一个 C++ 类 helloworld
4,序列化
在第三部编译生成的cc文件中,有一系列的SerializeToXXX方法,如SerializeToArray,可以根据具体情况用这一系列方法进行序列化。
5,反序列化
在第3个步骤编译生成的cc文件中,有一系列的ParseFromXXX方法,如ParseFromArray,可以根据具体情况用这一系列方法进行反序列化。
掌握以上几个步骤基本就能搞定proto的开发了,进阶的话可以去掌握proto的数据类型以及requried、optional、repeated限定修饰符,和message的嵌套(message嵌套可以设计出更多复杂的协议,满足更复杂的需求)。
protobuf的优劣自己去百度,JSON,XML我也有用过,但是相对来说,谷歌的protobuf是我用起来最方便高效的。
这是网上的一个测试结果:http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking
回到文章开头部分说的精髓,现在逐一到来
一,proto的设计:
一般设计规则如下
message Request
{
required fixed64 msgtype = 1;
required bytes bodys = 2;
}
一个消息类型加一个消息体,但这不能满足复杂的业务需求,所以复杂的系统里面一般拆成这样:
message Header
{
required fixed64 msgtype = 1;
}
message HelloworldRequest
{
required int32 id = 1; // ID
required string str = 2; // str
optional int32 opt = 3; //optional field
}
一个消息类型(单独定义一个文件)对应一个请求消息,一个请求消息对应一个接口,消息里面的字段对应接口所需要的参数,这是常用的设计方法,能满足所有的业务需求。
二,消息分发的设计
这里的message dispatch是指程序根据不同的msgtype,反序列化proto和做不同的业务逻辑处理。
古老而又传统的设计是采用switch case来做(我曾经看到有在用if else的),这种方法有很多不足之处,我这里举几个很常见的:
1,代码臃肿,随着消息类型的增加,会有n多的case
2,假设case里漏掉了break;那就不妙了。
3,代码维护差,每次增加msgtype,除了实现对应的业务逻辑处理,还要到消息入口增加对应的case。
在c语言里面,有一种很实用的办法,那就是函数指针,很多开源的和上层应用的回调函数或方法的底层都是封装了c语言的函数指针,这里提到函数指针,我简单介绍一下(本文没有用很大篇幅来说明函数指针,掌握其基本定义,慢慢学会衍生到复杂的概念):
从概念上说,函数指针是指向函数的指针变量,它本质上是一个指针变量。
其广泛的定义是: int (*f) (int x);
复制和调用: int func(int x); f = func;
那么函数指针在Message Dispath 如何设计呢,还是来一个简单的例子:
定义一个结构体
struct SMsgCmd {
int msgtype; /*msg type*/
int (*func)(const char *argv); /* handler * 函数指针/
};
定义一个结构体数组,并初始化
static struct SMsgCmd commands[] = {
{ 1, Request1},
{ 2, Request1},
};
在服务程序的消息入口处,遍历改数组即可
for ()
{
if (msgtype == commands[i].msgtype){
(*commands[i].func)(argv);
}
}
在c++11里面,可以利用std::function 和std::bind,其原理跟函数指针一个道理。
三,针对程序升级的proto设计
程序升级是常有的事,但我们升级的时候需要考虑兼容性,之前有看到过同个版本号如
if (version == 1){
}
else if (version > 2)
.........
这样做的缺陷我就不在过多的说明了。
对于proto的协议来说,我们只要做到以下几点,就会完美兼容新旧版本
1,不要随意添加或删除 required限定词修饰的字段
2,不要随意改变现有字段编码值
3,若需要新增字段,请用optional限定词修饰
本文到此就算结束了,若有错误之处,请多多指教!
欢迎关注本人微信公众号:lzyTalk江湖,不只是谈江湖,还会分享很多技术干货哦!