消息队列与共享内存(四):protobuf反射机制
protobuf自描述消息
在上一篇文章当中我讲到了使用protobuf来序列化消息便于发送,那么如果直接把我的rapidmsg用在我的消息队列当中就是这样的情况:
1 | long messageId = test::JUST_TEST_REQUEST; |
在序列化成string之前需要自己设置很多的其他值。但是看这个程序其实有很多地方都不必自己动手设置。其实真正需要自己手动设置的位置也就只有body那一块:
1 | Body* body = new Body; |
理想的情况下直接把body这个指针传给一个接口,然后其余的工作都由接口来完成。
发送端自然可以这样做,因为发送端知道自己发送的消息类型。
但是接受端就不行了。其实应该这样说:如果接收端仅仅接受特定的消息类型的话,那么很简单就能写一个专门的接口来负责解析消息。但是如果接收端不仅仅接收一种消息呢?一般处理的策略就是在head当中加入body所属的message类型。在接受端先读head,根据head当中保存的message类型来构造一个实例(反射),再用这个实例来解析body。这种情况下需要的使用反射,而protobuf已经实现了反射功能。
一般的后台接口设计都是一个接口只接收一种请求,返回一种响应。接收多种请求,并返回多种响应的接口也有,就是路由器或者网关。比如说公司为了安全考虑,在客户端和服务器的中间写一个路由器
route_machine或者叫做网关(gateway),客户端发送过来的消息先做一层解析,然后将消息重新包装发送给服务器。从服务器当中取出响应,解析之后重新包装返回给客户端。这样做就可以在route_machine这一层把非法的消息给阻挡住。或者这一层可以用做缓存,当发生缓存不命中的情况下才发送消息给服务器,其余情况都是从缓存当中拿数据返回给客户端,做到降低服务器的流量。这两个例子都说明了接受多种请求的接口有实用场景,所以protobuf的反射功能值得学习。
我公司这边写了一个前置机FrontEnd,但是这个功能比较简单,就是从客户端那里拿到消息,读消息的头,拿到类型之后再转发给对应的后台进程。这个前置机的并没有实现更深度的转发,它的主要作用是隔离后台应用服务器,保证安全。还有一点就是屏蔽后台服务器对用户的复杂性。不论后台服务器部署怎么改变,通过前置机展现给用户的接口始终不变。
protobuf本身的反射
Google Protobuf 本身具有很强的反射(reflection)功能,可以根据 type name 创建具体类型的 Message 对象。
我自己也写过一篇文章来用C++模拟反射:使用C++的classFactory来模拟JAVA的反射机制。我写的玩具一般的程序在protobuf的反射机制面前真是弱爆了。
使用到了五个接口:
DescriptorPool::generated_pool()
在protobuf的descriptor.h头文件当中能找到gengerated_pool的使用方法:
1 | // Get a pointer to the generated pool. Generated protocol message classes |
在protobuf的官方网站是这样的:
1 | For internal use only: Gets a non-const pointer to the generated pool. |
已经很明白了,这个接口用来创建一个DescriptorPool,陈硕老师在他的文章当中说它包含了程序编译的时候所链接的全部 protobuf Message types,其实不仅仅包含message的类型,enum的类型也在里面,可以通过FindEnumValueByName()根据,因为enum类型在创建的时候也会有一个descriptor,这个在下文再谈。
由于它是static,所以可以被重复调用多次而不会创建多个 DescriptorPool 对象。
FindMessageTypeByName
同样在protobuf的descriptor.h头文件当中:
1 | // Find a top-level message type by name. Returns NULL if not found. |
MessageFactory::generated_factory()
在protobuf的message.h头文件中:
1 | // This factory is a singleton. The caller must not delete the object. |
找到 MessageFactory 对象,它能创建程序编译的时候所链接的全部 protobuf Message types。
此 Factory 是一个 Singleton,因此重复多次调用 generated_factory 函数不会创建多个 MessageFactory 对象.
GetPrototype()
在message.h当中:
1 | // This method may or may not be thread-safe depending on the implementation. |
根据上文所得到的Descriptor的值来获取Message,对同一个 Descriptor 多次调用 MessageFactory::GetPrototype 函数将返回同一个对象,也就是default instance,所以不能直接对它进行操作,需要使用New()这个接口来创建一个新的实例。
New()
在message.h当中:
1 | // Construct a new instance of the same type. Ownership is passed to the |
由于New() 是虚函数,所以能返回本对象的一份新实例,类型与本对象的真实类型相同,也就是说返回的类型虽然是Message*,但是可以通过dynamic_cast来转换成具体 Message Type 的类型指针。
New 函数构造的 message 对象必须在 MessageFactory 销毁前销毁。
用法
客户端不仅要传送protobuf序列化之后的字符串,还需要将message的类型也一并传送出去。比如说我这个程序:
1 | string type_name = "rapidmsg.IpAddress"; |
不要被反射这两个字蒙住眼睛,老是想着如何使用字符串来初始化一个实例。
protobuf当中是根据字符串找到Descriptor*,再根据Descriptor*来初始化一个实例。如果能直接获取到Descriptor*,那么连字符串也不需要了。
GetReflectioin
那么是不是不知道类型就不能获取到当中的值了呢?也不是,还是有办法的。
那就是——通过GetReflection能够操作message的各个字段。
虽然我不知道发送的message的类型到底是IpAddress还是IpPort还是什么其他的类型,但是有一件事情我是知道的,那就是在所有的类型当中都定义了同一个字段:required string ip = xxx(这个当然不是protobuf内置的,是需要自己写proto文件的时候就写好的,如果自己没有写好,那么这个方法也就没有作用)。
那么不管你发送什么样的类型的数据,我都能获取到ip这个字段的值。
1 | const ::google::protobuf::Reflection* reflection = new_obj->GetReflection(); |
protobuf不仅仅对每一个message有google::protobuf::Descriptor的描述符,而且看到对于每一个字段也有google::protobuf::FieldDescriptor的描述符。
适用场景
这篇文章介绍的protobuf反射机制适用的场景是:发送的消息格式不同。
比如说客户端发送的是序列化的CLIENT_REQUEST,或者CLIENT_TEST_REQUEST,我所说的发送的消息格式指的是序列化时候用的消息格式。
至于我的rapidmsg使用的是extend的方式,发送的消息格式全部都是RMessage的格式,仅仅只是在body中嵌套了不同的message,序列化的时候用的还是RMessage,所以就没有使用这个反射的必要了。
使用这种方法应用场景不多,而且需要使用GetExtension来获取body的时候,这个反射机制也帮不上忙,倒是可以用宏来做这件事情。
封装代码
有关Makefile需要注意的一点
由于我的rmessage_util.cpp用到了rapidmsg.pb.h,所以要保证make的时候先编译proto文件,后编译rmessage_util.cpp这个文件。所以不能简单地把我以前写的Makefile照搬过来直接用,而是先用函数把这两个给分开:
1 | #这是描述proto文件 |
扩展阅读
一种自动反射消息类型的 Google Protobuf 网络传输方案
self-describing message
尾声
使用方法就讲解到这里,我想我需要把阅读protobuf源代码的任务作为我的2016年度计划当中去。
