Google Protobuf 简介
protobuf是google提供的一个开源序列化框架,类似于XML,JSON这样的数据表示语言,其最大的特点是基于二进制,因此比传统的XML表示高效短小得多。虽然是二进制数据格式,但并没有因此变得复杂,开发人员通过按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类,可以支持php、java、c++、python等语言环境。通过将这些类包含在项目中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列化工作。
protobuf在google中是一个比较核心的基础库,作为分布式运算涉及到大量的不同业务消息的传递,如何高效简洁的表示、操作这些业务消息在google这样的大规模应用中是至关重要的。而protobuf这样的库正好是在效率、数据大小、易用性之间取得了很好的平衡。
简单来讲,它具有以下特点:
- 语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
- 高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
- 扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程
数据交互xml、json、protobuf格式比较
-
json: 一般的web项目中,最流行的主要还是json。因为浏览器对于json数据支持非常好,有很多内建的函数支持。
-
xml: 在webservice中应用最为广泛,但是相比于json,它的数据更加冗余,因为需要成对的闭合标签。json使用了键值对的方式,不仅压缩了一定的数据空间,同时也具有可读性。
-
protobuf:是后起之秀,是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为profobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。
Protobuf 使用
Protobuf 有两个大版本,proto2 和 proto3,同比 python 的 2.x 和 3.x 版本,如果是新接触的话,同样建议直接入手 proto3 版本。所以下文的描述都是基于 proto3 的。
proto3 相对 proto2 而言,简言之就是支持更多的语言(Ruby、C#等)、删除了一些复杂的语法和特性、引入了更多的约定等,详情请浏览管网https://developers.google.cn/protocol-buffers/docs
1.安装
windows 安装
- protoc 下载:[官方下载地址],然后将 bin 路径添加到 path 环境变量下去
- 查看是否安装成功:控制台输入
protoc --version
,控制台输出版本信息代表成功,如:libprotoc 3.7.1
ideal 安装插件
- ideal 插件库搜索安装 Protobuf Support 即可
- 此插件可以不用安装,但是这有助于一些源码阅读的便利性和一些编码提示
linux 安装
- 到GitHub下载源码,执行解压命令后,进入解压后的目录
- 执行./autogen,生成configure
- 执行./configure --prefix=/usr/local/,protobuf配置安装的路径,生成Makefile
- 执行 make(编译用到C++11,保证g++的版本>=4.7)
- 执行make check
- 修改配置
- (1) vim /etc/profile,添加
- export PATH=$PATH:/usr/local/protobuf/bin/
- export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/
- 保存执行,source /etc/profile;同时在~/.profile中添加上面两行代码,否则会出现登录用户找不到protoc命令
- (2) 配置动态链接库
- vim /etc/ld.so.conf,在文件中添加/usr/local/protobuf/lib(注意: 在新行处添加),然后执行命令: ldconfig
- 安装完成
- 执行protoc --version,会出现当前libporoto的版本信息
2.应用
以java语言为例,编写一个Protobuf程序需要三个步骤
- 在
.proto
文件中定义消息格式。- 使用协议Protobuf编译器生成相应的消息的类代码。
- 使用Java-protobuf API写入和读取消息。
第一步:编写.proto文件
syntax = "proto3"; // 协议版本,proto3必须指定
import "google/protobuf/any.proto"; // 引用外部的message,可以是本地的,也可以是google提供的lib依赖
package smw; // 包名,其他 proto 在引用此 proto 的时候,就可以使用 smw.PersonTest 来使用,
option java_package = "com.smw.protobuf"; // 生成类的包名,注意:会在指定路径下按照该包名的定义来生成文件夹
option java_outer_classname="PersonProto"; // 生成类的最外层类名,注意:下划线的命名会在编译的时候被自动改为驼峰命名
message PersonTest {
int32 id = 1; // int 类型
string name = 2; // string 类型
string email = 3;
Sex sex = 4; // 枚举类型
repeated PhoneNumber phone = 5; // 引用下面定义的 PhoneNumber 类型的 message
map<string, string> tags = 6; // map 类型
repeated google.protobuf.Any details = 7; // 使用 google 的 any 类型
// 定义一个枚举
enum Sex {
DEFAULT = 0;
MALE = 1;
Female = 2;
}
// 定义一个 message
message PhoneNumber {
string number = 1;
PhoneType type = 2;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
}
}
如上所示,Protobuf 协议格式为:
message xxx {
// 字段规则:required -> 字段只能也必须出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
// 类型:int32、int64、sint32、sint64、string、32-bit ....
// 字段编号:1 ~ 536870911(除去 19000 到 19999 之间的数字)
字段规则 类型 名称 = 字段编号;
}
消息格式很简单,每个消息类型拥有一个或多个特定的数字字段,每个字段拥有一个名字和一个值类型。值类型可以是数字(整数或浮点)、布尔型、字符串、原始字节或者其他ProtocolBuffer类型,还允许数据结构的分级。
(1)限定修饰符
Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。
Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。---因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
Repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。
proto3版本中,已经取消required,optional。
(2)数据类型
Protobuf定义了一套基本数据类型。几乎都可以映射到C++\Java等语言的基础数据类型
protobuf 数据类型 | 描述 | 打包 | C++语言映射 |
bool | 布尔类型 | 1字节 | bool |
double | 64位浮点数 | N | double |
float | 32为浮点数 | N | float |
int32 | 32位整数、 | N | int |
uin32 | 无符号32位整数 | N | unsigned int |
int64 | 64位整数 | N | __int64 |
uint64 | 64为无符号整 | N | unsigned __int64 |
sint32 | 32位整数,处理负数效率更高 | N | int32 |
sing64 | 64位整数 处理负数效率更高 | N | __int64 |
fixed32 | 32位无符号整数 | 4 | unsigned int32 |
fixed64 | 64位无符号整数 | 8 | unsigned __int64 |
sfixed32 | 32位整数、能以更高的效率处理负数 | 4 | unsigned int32 |
sfixed64 | 64为整数 | 8 | unsigned __int64 |
string | 只能处理 ASCII字符 | N | std::string |
bytes | 用于处理多字节的语言字符、如中文 | N | std::string |
enum | 可以包含一个用户自定义的枚举类型uint32 | N(uint32) | enum |
message | 可以包含一个用户自定义的消息类型 | N | object of class |
N 表示打包的字节并不是固定。而是根据数据的大小或者长度。
例如int32,如果数值比较小,在0~127时,使用一个字节打包。
关于枚举的打包方式和uint32相同。
关于message,类似于C语言中的结构包含另外一个结构作为数据成员一样。
关于 fixed32 和int32的区别。fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高。根据项目的实际情况,一般选择fixed32,如果遇到对传输数据量要求比较苛刻的环境,可以选择int32.
(3)字段名称
字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的。
protobuf建议字段的命名采用以下划线分割的驼峰式。例如 first_name 而不是firstName.
(4)字段编码值
编码值的取值范围为 1~2^32(4294967296)。
其中 1~15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低(相对于1-15),当然一般情况下相邻的2个值编码效率的是相同的,除非2个值恰好实在4字节,12字节,20字节等的临界区。比如15和16.
1900~2000编码值为Google protobuf 系统内部保留值,建议不要在自己的项目中使用。
protobuf 还建议把经常要传递的值把其字段编码设置为1-15之间的值。
消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。
第二步:编译成java类
进入 proto 文件所在路径,输入下面 protoc 命令(后面有三部分参数),然后将编译得出的 java 文件拷贝到项目中即可(此 java 文件可以理解成使用的数据对象):
// $SRC_DIR: .proto 所在的源目录
// --java_out: 生成 java 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto
protoc -I=./ --java_out=./ ./person.proto
或
protoc -proto_path=./ --java_out=./ ./person.proto
第三步:调用java接口,进行序列化与反序列化的读取
maven依赖
<!-- protobuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.7.1</version>
</dependency>
序列化和反序列化有多种方式,可以是 byte[],也可以是 inputStream 等
package com.smw.protobuf;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* @ClassName: ProtoTest
* @Description: ProtoBuf 测试
* @Author: Jet.Chen
* @Date: 2019/5/8 9:55
* @Version: 1.0
**/
public class ProtoTest {
public static void main(String[] args) {
try {
/** Step1:生成 personTest 对象 */
// personTest 构造器
PersonProtos.PersonTest.Builder personBuilder = PersonProtos.PersonTest.newBuilder();
// personTest 赋值
personBuilder.setName("Jet Chen");
personBuilder.setEmail("ckk505214992@gmail.com");
personBuilder.setSex(PersonTestProtos.PersonTest.Sex.MALE);
// 内部的 PhoneNumber 构造器
PersonProtos.PersonTest.PhoneNumber.Builder phoneNumberBuilder = PersonProtos.PersonTest.PhoneNumber.newBuilder();
// PhoneNumber 赋值
phoneNumberBuilder.setType(PersonProtos.PersonTest.PhoneNumber.PhoneType.MOBILE);
phoneNumberBuilder.setNumber("17717037257");
// personTest 设置 PhoneNumber
personBuilder.addPhone(phoneNumberBuilder);
// 生成 personTest 对象
PersonProtos.PersonTest personTest = personBuilder.build();
/** Step2:序列化和反序列化 */
// 方式一 byte[]:
// 序列化
byte[] bytes = personTest.toByteArray();
// 反序列化
PersonProtos.PersonTest personTestResult = PersonProtos.PersonTest.parseFrom(bytes);
System.out.println(String.format("反序列化得到的信息,姓名:%s,性别:%d,手机号:%s", personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));
// 方式二 ByteString:
// 序列化
ByteString byteString = personTest.toByteString();
System.out.println(byteString.toString());
// 反序列化
PersonProtos.PersonTest personTestResult = PersonProtos.PersonTest.parseFrom(byteString);
System.out.println(String.format("反序列化得到的信息,姓名:%s,性别:%d,手机号:%s", personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));
// 方式三 InputStream
// 粘包,将一个或者多个protobuf 对象字节写入 stream
// 序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
personTest.writeDelimitedTo(byteArrayOutputStream);
// 反序列化,从 steam 中读取一个或者多个 protobuf 字节对象
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
PersonProtos.PersonTest personTestResult = PersonProtos.PersonTest.parseDelimitedFrom(byteArrayInputStream);
System.out.println(String.format("反序列化得到的信息,姓名:%s,性别:%d,手机号:%s", personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
关于默认值
proto3 中,数据的默认值不再支持自定义,而是由程序自行推倒:
- string:默认值为空
- bytes:默认值为空
- bools:默认值为 false
- 数字类型:默认值为 0
- 枚举类型: 默认为定义的第一个元素,并且编号必须为 0
- message 类型:默认值为 DEFAULT_INSTANCE,其值相当于空的 message
Protobuf与netty整合
客户端传递一个User对象给服务端(User对象包括姓名,年龄,密码)
客户端接收客户端的User对象并且将其相应的银行账户等信息反馈给客户端
定义的.proto文件如下:
syntax ="proto3";
package com.smw.netty.sixthexample;
option optimize_for = SPEED;
option java_package = "com.smw.test";
option java_outer_classname="DataInfo";
message RequestUser{
string user_name = 1;
int32 age = 2;
string password = 3;
}
message ResponseBank{
string bank_no = 1;
double money = 2;
string bank_name=3;
}
使用Protobuf编译器进行编译,生成DataInfo对象,
服务器端代码:
package com.smw.test;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class ProtoServer {
public static void main(String[] args) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup wokerGroup = new NioEventLoopGroup();
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ProtoServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
wokerGroup.shutdownGracefully();
}
}
}
服务端ProtoServerInitializer(初始化连接):
package com.smw.test;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
public class ProtoServerInitializer extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//解码器,通过Google Protocol Buffers序列化框架动态的切割接收到的ByteBuf
pipeline.addLast(new ProtobufVarint32FrameDecoder());
//服务器端接收的是客户端RequestUser对象,所以这边将接收对象进行解码生产实列
pipeline.addLast(new ProtobufDecoder(DataInfo.RequestUser.getDefaultInstance()));
//Google Protocol Buffers编码器
pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
//Google Protocol Buffers编码器
pipeline.addLast(new ProtobufEncoder());
pipeline.addLast(new ProtoServerHandler());
}
}
自定义服务端的处理器:
package com.smw.test;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class ProtoServerHandler extends SimpleChannelInboundHandler<DataInfo.RequestUser> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DataInfo.RequestUser msg) throws Exception {
System.out.println(msg.getUserName());
System.out.println(msg.getAge());
System.out.println(msg.getPassword());
DataInfo.ResponseBank bank = DataInfo.ResponseBank.newBuilder().setBankName("中国工商银行")
.setBankNo("6222222200000000000").setMoney(560000.23).build();
ctx.channel().writeAndFlush(bank);
}
}
客户端:
package com.smw.test;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class ProtoClient {
public static void main(String[] args) throws Exception{
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try{
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
.handler(new ProtoClientInitializer());
ChannelFuture channelFuture = bootstrap.connect("localhost",8899).sync();
channelFuture.channel().closeFuture().sync();
}finally {
eventLoopGroup.shutdownGracefully();
}
}
}
客户端初始化连接(ProtoClientInitializer)
package com.smw.test;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
public class ProtoClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//解码器,通过Google Protocol Buffers序列化框架动态的切割接收到的ByteBuf
pipeline.addLast(new ProtobufVarint32FrameDecoder());
//将接收到的二进制文件解码成具体的实例,这边接收到的是服务端的ResponseBank对象实列
pipeline.addLast(new ProtobufDecoder(DataInfo.ResponseBank.getDefaultInstance()));
//Google Protocol Buffers编码器
pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
//Google Protocol Buffers编码器
pipeline.addLast(new ProtobufEncoder());
pipeline.addLast(new ProtoClientHandler());
}
}
自定义客户端处理器:
package com.smw.test;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class ProtoClientHandler extends SimpleChannelInboundHandler<DataInfo.ResponseBank> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DataInfo.ResponseBank msg) throws Exception {
System.out.println(msg.getBankNo());
System.out.println(msg.getBankName());
System.out.println(msg.getMoney());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DataInfo.RequestUser user = DataInfo.RequestUser.newBuilder()
.setUserName("zhihao.miao").setAge(27).setPassword("123456").build();
ctx.channel().writeAndFlush(user);
}
}
运行服务器端和客户端,服务器控制台打印:
七月 03, 2017 11:12:03 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xa1a63b58, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0x08c534f3, L:/127.0.0.1:8899 - R:/127.0.0.1:65448]
七月 03, 2017 11:12:03 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xa1a63b58, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
zhihao.miao
27
123456
客户端控制台打印:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
6222222200000000000
中国工商银行
560000.23