原文 2014-03-23 21:57:49 发表于 CSDN,这里对以前写的文章做下收录。
protobuf是google的一个序列化框架,类似XML,JSON,其特点是基于二进制,比XML、JSON表示同样一段内容要短小得多,还可以定义一些可选字段,广泛用于服务端与客户端通信。文章将着重介绍在erlang中如何使用protobuf。
首先google没有提供对erlang语言的直接支持,所以这里使用到的第三方的protobuf库(erlang_protobuffs)
使用protobuf
定义一个protobuf结构,保存为test.proto,如下:
message Person {
required int32 age = 1;
required string name = 2;
}
message Family {
repeated Person person = 1;
}
编译这个protobuf结构,生成相应的erlang代码:
% 生成相应的erl和hrl文件
protobuffs_compile:scan_file_src("test.proto").
% 生成相应的beam和hrl文件
protobuffs_compile:scan_file("test.proto").
下面我们以例子简单说明如何使用:
-module(test).
-compile([export_all]).
-include("test_pb.hrl").
encode() ->
Person = #person{age=25, name="John"},
test_pb:encode_person(Person).
decode() ->
Data = encode(),
test_pb:decode_person(Data).
encode_repeat() ->
RepeatData =
[
#person{age=25, name="John"},
#person{age=23, name="Lucy"},
#person{age=2, name="Tony"}
],
Family = #family{person=RepeatData},
test_pb:encode_family(Family).
decode_repeat() ->
Data = encode_repeat(),
test_pb:decode_family(Data).
运行代码,如下:
6> c(test).
{ok,test}
7> test:encode().
<<8,25,18,4,74,111,104,110>>
8> test:decode().
{person,25,"John"}
9> test:encode_repeat().
<<10,8,8,25,18,4,74,111,104,110,10,8,8,23,18,4,76,117,99,
121,10,8,8,2,18,4,84,111,110,...>>
10> test:decode_repeat().
{family,[{person,25,"John"},
{person,23,"Lucy"},
{person,2,"Tony"}]}
改进protobuf
改进版的protobuf代码及例子在这里。
改进调用方法
原来调用比较别扭,每个协议都是不同的方法名:
encode() ->
Person = #person{age=25, name="John"},
test_pb:encode_person(Person).
decode() ->
Data = encode(),
test_pb:decode_person(Data).
现在改成了统一的方法名:
encode() ->
Person = #person{age=25, name="John"},
test_pb:encode(Person). %% 或 test_pb:encode(person, Person)
decode() ->
Data = encode(),
test_pb:decode(person, Data).
这样,很方便代码整合,只要协议映射好,封包解包就可以统一在网关层处理。
改进空协议
message tos{
}
友好兼容空结构,原来空协议会报以下的警告信息
2> test_pb:encode_tos(#tos{}).
<<>>
3> c(test_pb).
src/test_pb.erl:33: Warning: variable 'Record' is unused
{ok,test_pb}
改进调用效率
改进了序列化和反序列化的效率,encode测试效率提升10 ~ 20%, decode测试效率提升10% ~ 40%
decode优化较大,实际有些情况不止40%,取均值,部分代码优化到协议编译期,所以调用就省事多了。
总结一些效率改进的技巧
获取record名字
[RecName | _] = tuple_to_list(Data) %% 改成了: erlang:element(1,Data)
函数参数匹配优化
test(Bytes) when is_binary(Bytes) ->
ok.
%% 改成了
test(<<Bytes/binary>>) ->
ok.
当匹配类型多时就会明显,guard模块对于多参数或多类型匹配尤为不利,erlang在编译期不能做优化。
分割二进制
Bytes = list_to_binary("0123456789"),
split_binary(Bytes, 3),
<<B1:3/binary, B2/binary>> = Bytes,%% 测试效率没erlang:split_binary/2高
{B1, B2}.
二进制合并
11> A= <<>>.
<<>>
12> B= <<"1333">>.
<<"1333">>
15> C= <<1,3,4>>.
<<1,3,4>>
16> iolist_to_binary([B,C,A]).
<<49,51,51,51,1,3,4>>
17> <<B/binary,C/binary,A/binary>>.
<<49,51,51,51,1,3,4>>
18> c(loop).
{ok,loop}
19> loop:test_iolist().
1000000 loops, using time: 281ms
20> loop:test_bin_match().
1000000 loops, using time: 94ms
case匹配优化
case Data of
{double, C} -> ok;
{float, C} -> ok;
{int, C} -> ok;
{string, C} -> ok;
_ -> ok
end
case Data of
{C, double} -> ok;
{C, float} -> ok;
{C, int} -> ok;
{C, string} -> ok;
_ -> ok
end
第一种匹配效率较高,case匹配类似函数参数匹配,固定不变的内容放匹配表达式左边
比较erlang原生的二进制转换
最后,比较 erlang:term_to_binary/1 与 erlang:binary_to_term/1 的效率
测试结果发现erlang原生的二进制转换的效率超高,但是数据没压缩,不适合直接使用。实际使用需要配合 zlib:zip/1,这个是压缩工具,可能还需要稍微优化一下
zip(Data) ->
Z = zlib:open(),
Bs =
try
zlib:deflateInit(Z, best_speed, deflated, -15, 1, default),
B = zlib:deflate(Z, Data, finish),
zlib:deflateEnd(Z),
B
after
zlib:close(Z)
end,
iolist_to_binary(Bs).
测试结果:
erlang:term_to_binary/1(配合zlib:zip/1)时间开销比 protobuf 多15%。
erlang:binary_to_term/1(配合zlib:unzip/1)时间开销差不多是 protobuf 的50%。
项目Git地址:
https://github.com/chenweiqi/erl_protobuffs/
2015/6/11 修复字段默认值没有生效bug
2015/6/11 修复proto文件换行无法编译bug

老司机,666