Contents
  1. 1. 介绍
  2. 2. 安装
  3. 3. 使用
  4. 4. pb与libevent
  5. 5. 应用
  6. 6. 深入应用
  7. 7. 总结

介绍

Protobuf项目链接

protobuf(以下简称pb)也是现下网络编程必备的第三方库之一了,关于它的介绍官方是这样说的:

What is it?
Protocol Buffers are a way of encoding structured data in an efficient yet extensible format. Google uses Protocol Buffers for almost all of its internal RPC protocols and file formats.

所以它应用在网络编程可以视作序列化数据的工具,另外最后一句话显然是给用户打强心剂的赶脚?

关于序列化工具,可以大致像下面这样的过程来理解:

dataA, dataB, dataC… => protobuf => bytedata
=> NETWORK =>
bytedata => protobuf => dataA, dataB, dataC…

这种工具相信应该有不少版本,比如我们工地就是自己实现的类似工具,不过它还额外借助了xml来处理序列关系,这样就又多个解析xml的工作出来,实现起来看上去怪麻烦的。

鉴于google一贯的水准,有理由相信pb在这项工作上表现不会令人失望。

因为几个月前已经试着把pb集成到自己的项目里去,所以这次只能算是回顾,各方面都比较顺利。

安装

安装pb和安装大多数靠谱的第三方库一样,总体来说还是简单方便的。无非是下载源码包,解压后找到官方提供给vs用户的sln文件,打开,编译。

这样就能得到一个protoc.exe和libprotobuf.lib,基本的安装工作也就算是完成了。

使用

libprotobuf在使用上也基本没有需要特别的地方,进行基本的工程配置之后就能正常链接生成output了。

需要特别提一下的是protoc.exe和.proto。

这也是pb的一大特色所在了,它使我们的通信数据格式从特定程序语言中解放出来,在.proto中用标准化的语法来描述。然后protoc.exe再根据你的需要把.proto翻译城不同语言版本的消息定义。

.proto提供了非常简洁实用的语法基本上能满足各种消息格式定义的需求,另外能够翻译成多个程序语言在实际开发中也很有用处,进行消息通信的双方都需要知道消息结构,如果自己来为每个语言写一个版本,维护消息一致性将是很繁琐无趣的工作。

通过插件扩展,现在pb几乎已经支持绝大多数市面上常见的程序语言。

官方文档的链接

pb与libevent

为了更快的可以搭出pb的测试环境,我把前天写的libevent的封装稍作下修改,并且工程改为编译成lib。

pb和libevent一起用的时候,因为二者本身分工明确,所以代码结构上也会比较好安排。比如说可以明确的知道,libevent的封装需要提供给pb层一个send接口和一个onrecv的事件回调。

实现这一点之后,我们还需要分别在调用send前实现一个pb_encoder层,用来进行pbmsg => byte的转换,对外层应用提供SendMsg的接口。

以及onrecv后实现一个pb_decoder层,用来进行byte => pbmsg转换,之后传递给dispatcher进行消息处理函数调用。

这样逻辑层就只需要和具体的消息格式打交道了,写起来会清晰很多。

应用

pb虽然解决了消息序列化成bytes的问题,但是在接收pb数据端,一开始望着bytes并不知道应该转换成什么msgtype。

而pb的msgtype进行转换的接口——ParseFromArray,又要求传递bytes的长度。所以为了解决这两个问题(类型和长度),发送端需要在pbmsg的bytes前加上msgid和len的数据。

这样对端在得到onrecv事件后,可以先把len读进来,然后判定包是否已经完全读进来,是的话再读msgid和解析成msg,等解析成功后再从recvbuf中把这部分数据丢掉。

应该是一个比较常见的需求,不过之前扫了下文档貌似没找到栗子,最后是从SO上看来的一个发送端栗子(经过自己修改后):

const uint32 _BUF_SIZE = 1024;

template <typename HolderType>
template <typename PBMsgType>
void TPBMsgSender<HolderType>::SendMsg(const PBMsgType& msg)
{
	auto is = static_cast<ISend*>(static_cast<HolderType*>(this));

	uint32 msgid = PtlMapper::GetIdByType<PBMsgType>::res;
	uint32 msglen = msg.ByteSize();
	uint32 prefixlen = sizeof(msgid) + sizeof(msglen);
	uint32 bufferlen = prefixlen + msglen;

	static uint8 buf[_BUF_SIZE];
	google::protobuf::io::ArrayOutputStream array_output(buf, bufferlen);
	google::protobuf::io::CodedOutputStream coded_output(&array_output);

	coded_output.WriteLittleEndian32(bufferlen);
	coded_output.WriteLittleEndian32(msgid);

	msg.SerializeToCodedStream(&coded_output);

	is->Send((const char*)buf, bufferlen);
}

接收端是自己琢磨的:

template <typename HolderType>
void TPBMsgReceiver<HolderType>::OnRecv(const uint8* data, size_t size)
{
	using google::protobuf::io::CodedInputStream;

	static uint32 buflen = 0;
	static uint32 msgid = 0;
	static uint32 prefixlen = sizeof(buflen) + sizeof(msgid);

	if (size < sizeof(buflen))
	{
		return;
	}

	CodedInputStream::ReadLittleEndian32FromArray(data, &buflen);

	if (buflen > size)
	{
		return;
	}
	
	CodedInputStream::ReadLittleEndian32FromArray(data+sizeof(buflen), &msgid);

	HandleFunc func = GetHandleFunc(msgid);
	if (!func)
	{
		cerr << "invalid msg id: " << msgid << endl;
		return;
	}
	
	(this->*func)(data+prefixlen, buflen - prefixlen);
}

栗子写的仓促,还有些不完善,就像前面说的,recv这块的处理是需要返回结果的,不然libevent那边过早把数据丢掉可能会导致很多msg没办法传递给逻辑层。这在本机小数据量下不明显,但估计放到服务器上正式去用的时候问题就大了。

这个栗子有时间会完善起来,这里先贴出来做个未雨绸缪的bug分析。

dispatcher的实现改天单独写。

深入应用

虽然使用第三方库可以很大程度上改善项目质量,但我觉得这对于真正网络应用来说还是不够的,比如说还没谈到的至少就包括数据校验、filter、加解密等。

这些我也还不太清楚是否在libevent或者pb上已经提供了支持,还需要之后有时间再进做进一步了解。

总结

虽然pb+libevent完成了基本网络框架90%的工作,但怎样组织才能给提供给逻辑层一个舒适效率的接口也是很花功夫去思考的!


pb测试用的代码已经传到了这里 https://github.com/ooomceg/examples

Contents
  1. 1. 介绍
  2. 2. 安装
  3. 3. 使用
  4. 4. pb与libevent
  5. 5. 应用
  6. 6. 深入应用
  7. 7. 总结