抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

NETTY序列化与反序列化

对象序列化/反序列化简介

​ 在实际开发中,为了简化开发,通常请求和响应都使用使用Java对象表示,对使用者屏蔽底层的协议细节。例如一些RPC框架,支持把用户定义的Java对象当做参数去请求服务端,服务端响应也是一个Java对象。

​ 而前面我们讲解的案例都是以字符串作为请求和响应参数,实际上,Netty对于我们的自定义的Java对象作为请求响应参数也是支持的,其默认支持通过以下机制对Java对象进行序列化和反序列化:

  • ObjectEncoder/ObjectDecoder:使用JDK序列化机制编解码
  • ProtobufEncoder/ ProtobufDecoder:使用google protocol buffer进行编解码
  • MarshallingEncoder/MarshallingDecoder:使用JBoss Marshalling进行编解码
  • XmlDecoder:使用Aalto XML parser进行解码,将xml解析成Aalto XML parser中定义的Java对象,没有提供相应的编码器
  • JsonObjectDecoder:使用Json格式解码。当检测到匹配数量的”{“ 、”}”或”[””]”时,则认为是一个完整的json对象或者json数组。这个解码器只是将包含了一个完整Json格式数据的ByteBuf实例交给之后的ChannelInbounderHandler解析,因此我们需要依赖其他的JSON框架,如Gson、jackson、fastjson等。没有提供相应的编码器。

除了Netty默认值支持的这些序列化机制,事实上还有很多的其他的序列化框架,如:hessianKryoAvrofstmsgback、thrift、protostuff等。

在实际开发中,通常我们没有必要支持上述所有的序列化框架,支持部分即可。主要的选择依据如下表:

选择依据 说明
效率 即序列化和反序列化的性能。这方面Kryo、Avro、fst、hessian等都不错。
序列化后的占用字节数 对于同一个Java对象,不同的框架序列化后占用的字节数不同。例如JDK序列化体积较大,而Kryo的体积较小。体积过大的话,会增加网络带宽压力。
是否有可视化需求 json、xml序列化机制的结果能以文本形式展示;但是其他的框架大多是二进制的,因此可视化。
开发成本 一些序列化框架使用较为复杂,如thrift、protocol buffer;另外则很简单,如JDK序列化、Hessian等

​ 从本教程而言,主要是为了介绍如何在Netty中使用这些序列化框架,方式类似,因此不会对每一种都进行介绍。在后续的文章中,我们将会对:JDK序列化、Hessian序列化、protocol buffer序列化进行讲解。

通信协议格式要求

​ 另外一点需要注意的是,上面提到的这些序列化框架通常不能单独使用。例如发送方只是将Java对象序列化成二进制字节,对于接收方而言,则无法判断到底哪些字节可以构成一个完整的Java对象,也就无法反序列化。因此我们通常需要结合长度编码,可以使用上一节提到的LengthFieldBasedFrameDecoder/LengthFieldPrepender来协助完成。

最简单的通信协议格式如下:

1
2
3
+--------+----------+
| Length | Content |
+--------+----------+

Length:表示Content字段占用的字节数,Length本身占用的字节数我们可以指定为一个固定的值。

Content:对象经过序列化后二进制字节内容

​ 对于上述协议,通常我们只能选择支持一种序列化框架,如果要支持多个序列化框架,我们可以对通信协议格式稍作改造,增加一个字段来表示使用的序列化框架,如:

1
2
3
+--------+-------------+------------+
| Length | Serializer | Content |
+--------+-------------+------------+

​ 其中:Serializer我们可以指定使用1个字节表示,因此可以有256个值可选,我们用不同的值代表不同的框架。在编码时,选择好序列化框架后,进行序列化,并指定Serializer字段的值。在解码时,根据Serializer的值选择对应的框架进行反序列化;

JDK序列化

ObjectEncoder与ObjectDecoder简介

ObjectEncoder/ObjectDecoder使用JDK序列化机制编解码,因此我们可以使用Java对象作为请求和响应参数,限制是对象必须实现Serializable。JDK序列化机制的缺点是:序列化的性能以及序列化后对象占用的字节数比较多。优点是:这是对JDK默认支持的机制,不需要引入第三方依赖。

​ 如果仅仅是对象序列化,字节通过网络传输后,那么在解码时,无法判断到底多少个字节可以构成一个Java对象。因此需要结合长度编码,也就是添加一个Length字段,表示序列化后的字节占用的字节数。因此ObjectEncoder/ObjectDecoder采用的通信协议如下:

1
2
3
+--------+----------+
| Length | Content |
+--------+----------+
  • Length:表示Content字段占用的字节数,Length本身占用的字节数我们可以指定为一个固定的值
  • Content:对象经过JDK序列化后二进制字节内容

​ 乍一看,这与我们在上一节讲解的LengthFieldBasedFrameDecoder/LengthFieldPrepender采用的通信协议很类似。事实上ObjectDecoder本身就继承了LengthFieldBasedFrameDecoder。不过ObjectEncoder略有不同,其并没有继承LengthFieldPrepender,而是内部直接添加了Length字段。

ObjectEncoder源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Sharable
public class ObjectEncoder extends MessageToByteEncoder<Serializable> {
private static final byte[] LENGTH_PLACEHOLDER = new byte[4];
//当需要编码时,encode方法会被回调
//参数msg:就是我们需要序列化的java对象
//参数out:我们需要将序列化后的二进制字节写到ByteBuf中
@Override
protected void encode(ChannelHandlerContext ctx, Serializable msg, ByteBuf out) throws Exception {
int startIdx = out.writerIndex();
//ByteBufOutputStream是Netty提供的输出流,数据写入其中之后,可以通过其buffer()方法会的对应的ByteBuf实例
ByteBufOutputStream bout = new ByteBufOutputStream(out);
//JDK序列化机制的ObjectOutputStream
ObjectOutputStream oout = null;
try {
//首先占用4个字节,这就是Length字段的字节数,这只是占位符,后面为填充对象序列化后的字节数
bout.write(LENGTH_PLACEHOLDER);
//CompactObjectOutputStream是netty提供的类,其实现了JDK的ObjectOutputStream,顾名思义用于压缩
//同时把bout作为其底层输出流,意味着对象序列化后的字节直接写到了bout中
oout = new CompactObjectOutputStream(bout);
//调用writeObject方法,即表示开始序列化
oout.writeObject(msg);
oout.flush();
} finally {
if (oout != null) {
oout.close();
} else {
bout.close();
}
}
int endIdx = out.writerIndex();
//序列化完成,设置占位符的值,也就是对象序列化后的字节数量
out.setInt(startIdx, endIdx - startIdx - 4);
}
}

ObjectDecoder源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//注意ObjectDecoder继承了LengthFieldBasedFrameDecoder
public class ObjectDecoder extends LengthFieldBasedFrameDecoder {

private final ClassResolver classResolver;
public ObjectDecoder(ClassResolver classResolver) {
this(1048576, classResolver);
}
//参数maxObjectSize:表示可接受的对象反序列化的最大字节数,默认为1048576 bytes,约等于1M
//参数classResolver:由于需要将二进制字节反序列化为Java对象,需要指定一个ClassResolver来加载这个类的字节码对象
public ObjectDecoder(int maxObjectSize, ClassResolver classResolver) {
//调用父类LengthFieldBasedFrameDecoder构造方法,关于这几个参数的作用,参见之前章节的分析
super(maxObjectSize, 0, 4, 0, 4);
this.classResolver = classResolver;
}
//当需要解码时,decode方法会被回调
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
//首先调用父类的decode方法进行解码,会解析出包含可解析为java对象的完整二进制字节封装到ByteBuf中,同时Length字段的4个字节会被删除
ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if (frame == null) {
return null;
}
//构造JDK ObjectInputStream实例用于解码
ObjectInputStream ois = new CompactObjectInputStream(new ByteBufInputStream(frame, true), classResolver);
try {
//调用readObject方法进行解码,其返回的就是反序列化之后的Java对象
return ois.readObject();
} finally {
ois.close();
}
}
}

下面我们通过实际案例来演示ObjectEncoder与ObjectDecoder的使用。

使用案例

系列化类

首先我们分别编写两个Java对象Request/Response分别表示请求和响应。

1
2
3
4
5
6
7
8
9
10
public class Request implements Serializable{
private String request;
private Date requestTime;
//setters getters and toString
}
public class Response implements Serializable{
private String response;
private Date responseTime;
//setters getters and toString
}
客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class JdkSerializerClient {
public static void main(String[] args) throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap(); // (1)
b.group(workerGroup); // (2)
b.channel(NioSocketChannel.class); // (3)
b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ObjectEncoder());
ch.pipeline().addLast(new ObjectDecoder(new ClassResolver() {
public Class<?> resolve(String className) throws ClassNotFoundException {
return Class.forName(className);
}
}));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
// 在于server建立连接后,即发送请求报文
public void channelActive(ChannelHandlerContext ctx) {
Request request = new Request();
request.setRequest("i am request!");
request.setRequestTime(new Date());
ctx.writeAndFlush(request);
}
//接受服务端的响应
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Response response= (Response) msg;
System.out.println("receive response:"+response);
}
});
}
});
// Start the client.
ChannelFuture f = b.connect("127.0.0.1", 8080).sync(); // (5)
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}

​ 由于client既要发送Java对象Request作为请求,又要接受服务端响应的Response对象,因此在ChannelPipeline中,我们同时添加了ObjectDecoder和ObjectEncoder。

​ 另外我们自定义了一个ChannelInboundHandler,在连接建立时,其channelActive方法会被回调,我们在这个方法中构造一个Request对象发送,通过ObjectEncoder进行编码。服务端会返回一个Response对象,此时channelRead方法会被回调,由于已经经过ObjectDecoder解码,因此可以直接转换为Reponse对象,然后打印。

服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class JdkSerializerServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class) // (3)
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ObjectEncoder());
ch.pipeline().addLast(new ObjectDecoder(new ClassResolver() {
public Class<?> resolve(String className) throws ClassNotFoundException {
return Class.forName(className);
}
}));
// 自定义这个ChannelInboundHandler打印拆包后的结果
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Request request= (Request) msg;
System.out.println("receive request:"+request);
Response response = new Response();
response.setResponse("response to:"+request.getRequest());
response.setResponseTime(new Date());
ctx.writeAndFlush(response);
}
});
}
});
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(8080).sync(); // (7)
System.out.println("JdkSerializerServer Started on 8080...");
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}

Server端与Client端分析类似,这里不再赘述。

先后启动Server端与Client端,在Server端控制台我们将看到:

1
2
JdkSerializerServer Started on 8080...
receive request:Request{request='i am request!', requestTime=2018-9-9 18:47:30}

在Client端控制台我们将看到:

1
2
receive response:Response{response='response to:i am request!', responseTime=2018-9-9 18:48:36}

总结

​ 把ObjectEncoder/ObjectDecoder作为序列化/反序列化入门是非常合适的,因为其足够简单。同时通过这个案例,我们也发现了,通信协议中必须包含了一个Length字段,用于表示对象序列化后的字节数。事实上,这种模式也可以套用到我们接下来要介绍的其他序列化框架中。

JDK序列化的缺点

​ Java序列化从JDK1.1版本就已经提供,它不需要添加额外的类库,只需实现java.io.Serializable并生成序列ID即可,因此,它从诞生之初就得到了广泛的应用。

​ 但是在远程服务调用(RPC)时,很少直接使用Java序列化进行消息的编解码和传输,这又是什么原因呢?下面通过分析Java序列化的缺点来找出答案。

无法跨语言

​ 对于跨进程的服务调用,服务提供者可能会使用C十+或者其他语言开发,当我们需要和异构语言进程交互时Java序列化就难以胜任。由于Java序列化技术是Java语言内部的私有协议,其他语言并不支持,对于用户来说它完全是黑盒。对于Java序列化后的字节数组,别的语言无法进行反序列化,这就严重阻碍了它的应用。

序列化后的码流太大

​ JDK序列化后的二进制比本身要大。

序列化性能太低

​ 无论是序列化后的码流大小,还是序列化的性能,JDK默认的序列化机制表现得都很差。因此,我们边常不会选择Java序列化作为远程跨节点调用的编解码框架。

Protocol Buffer编解码

Google 的 protobuf 在业界非常流行,很多商业项目选择 protobuf 作为编码解码框架,这里我们首先介绍一下 Protobuf 的优点:

  1. 在 Google 内长期使用,产品成熟度高;
  2. 跨语言,支持多种语言,包括 c++,Java, 和 Python;
  3. 编码后的消息更小,更加有利于存储和传输;
  4. 编码的性能非常高;
  5. 支持不同协议版本的向前兼容;
  6. 支持定义可选和必选字段;

Protobuf基本使用

这一部分的内容主要源自Protocol Buffer的官方文档,地址如下:

https://developers.google.com/protocol-buffers/docs/javatutorial

​ 需要注意的是,这部分并不是一个在java中如何使用protocol buffer的完整的文档。更多的文档,请参考: Protocol Buffer Language Guide, the Java API Reference, the Java Generated Code Guide, and the Encoding Reference.

使用步骤

ProtocolBuf使用主要包括三个步骤:

  1. 在.proto文件中定义消息的格式

  2. 使用protocol buffer编译器根据.proto文件生成java类

  3. 使用Java protocol buffer api读写消息

定义.proto文件

proto是protocol的简写,因此定义proto文件实际上就是定义服务端与客户端通信协议。ProtocolBuffer提供了工具可以自动根据这个文件生成相应的java类,我们将在后面介绍。

​ 举例来说,假设我们有一个提供查询个人联系方式的Server,当接受到Client查询某个人联系方式的请求时,将相关信息返回。那么我们就可以定义一个类似以下的addressbook.proto文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package tutorial;

option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";

message AddressBook {
repeated Person person = 1;
}

message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}

repeated PhoneNumber phone = 4;
}

可以看到.proto文件的语法与C++或者java很类似。让我们来看看文件中的每一个部分都做了什么。

package

​ .proto文件以一个package声明开始,这用于阻止不同项目间的文件命名冲突。在java中,这个package用于指定生成的Java类的所属的包。

​ 在package声明之后的两个option: java_package和 java_outer_classname是java独有的。

java_package

​ 在指定了 java_package之后,生成的java类的包名则由java_package指定。不过即使指定java_package,我们依旧也应该声明package,来防止可能的根据这个文件生成非java源码的造成命名冲突的可能。

java_outer_classname

java_outer_classname 说明应该生成一个类,将.proto文件中定义的所有message生成的类,包含进此类中。如果没有显示的指定这个option,默认将会按照.proto文件的名称按照驼峰命名的法则生成这个类。例如my_proto.proto生成的类名为MyProto。在我们的案例中,生成的是AddressBookProtos.java。

message

​ 我们为每个希望序列化的数据结构定义一个message。上例中,在addressbook.proto文件中直接定义了两个message:AddressBook message和Person message。生成java类之后,将会在最外层类AddressBookProtos中定义两个内部类AddressBook和Person。你甚至可以在一个message内,嵌套定义其他的message。如你所见,PhoneNumber 定义在 Person中,那么生成的java类中,PhoneNumber类将会定义在Person类中。

​ 一个message是一系列不同类型的字段的集合。一些常见的简单数据类型都可以用来定义message中字段的类型,包括: bool, int32, float, double, 和 string

​ 你也可以在一个message中使用其他的message作为字段。可以看到,AddressBook message中引用了Person message, Person message中引用了 PhoneNumber message。

enum

​ 你也可以定义 enum 类型,如果你希望某个字段有一些预定义的值,例如上述文件指定了电话类型(PhoneType)必须是: MOBILE,HOME, or WORK中的一种。

字段修饰符

对于message中定义的每一个字段,都必须添加required、optional、repeated其中之一作为修饰符。

required:表明这个字段的值必须提供。当我们在构建一个message实例的时候,如果使用required修饰的字段没有提供值,protobuf会认为这个message不能初始化,将会抛出一个 UninitializedMessageException。尝试去解析一个没有被初始化的message,将会抛出 IOException。除了这两个特性,required字段与optional字段其他特性都是相同的。

optional:表明这个字段的值可以设置,也可以不设置。如果一个optional的字段没有设置值,一个默认值将会被使用(如果提供了默认值的话)。对于简单的数据类型,你可以指定自己的默认值,就像我们在案例中指定电话类型的type字段一样,否则,将会使用系统提供的默认值:数字类型默认值为0,字符串类型默认值是空串,布尔类型默认值是false。对于内嵌的message,默认值总是其默认实例或者称之为原型,也就是没有显示指定任何字段的值(可以理解为刚创建的一个java对象,没有对其进行任何设置)。

repeated:这个字段可以被重复多次(包含0)。在生成java代码时,对于repeat修饰的字段,将用一个List来表示。

​ 注意:对于required注释的使用要慎重,考虑到不同版本的兼容问题。如果一开始设置某个字段为required,后来又想修改。这就可能会导致老版本的不兼容。一些Google的工程师的总结:optional或者repeated通常是更好的选择,但这不是绝对的。

tag

​ 在addressbook.proto文件中,我们可以发现每个字段都有类似于 “ = 1”, “ = 2”这样的标记,在protobuf中称之为tag。注意这不是给字段赋予默认值,默认值的赋予是使用DEFAULT关键字,例如上例中给PhoneType字段设置了默认值。

​ tag的作用是表示在生成的java类中,这些字段的出现类中先后顺序,tag必须是唯一的,不能重复。对于Protocol Buffer而言,tag值为1到15的字段在编码时可以得到优化,只需要占用一个字节,tag范围是16-2047的字段在编码时将占用两个bytes。而Protocol Buffer可以支持的字段数量则为2的29次方减一。有鉴于此,我们在设计消息结构时,可以尽可能考虑让repeated类型的字段标签位于1到15之间,这样便可以有效的节省编码后的字节数量。

要查看.proto文件的完整语法,请参考 Protocol Buffer Language Guide。不要尝试去查找类似于java中的类继承这样的功能,protobuf不提供此类功能。

根据.proto文件生成java类

​ 现在我们已经有了.proto文件,下一步是根据这个文件生成类。每一个message都会生成一个对应的类,并且最终都包含在java_outer_classname 指定的类中。

​ 如果你还没有安装protobuf编译器,可以到 download the package下载。

​ 如果你使用的是Windows操作系统,可以下载:

​ protoc命令的用法如下:

1
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

​ $SRC_DIR是.proto文件所在目录,如果都不提供的话,默认就生成在当前目录 , $DST_DIR是生成的java类存储目录。

​ 最简单的情况,直接输入

1
protoc --java_out=.  addressbook.proto

​ 生成的文件位于:com/example/tutorial/AddressBookProtos.java

​ 现在我们来看一下protobuf为我们生成的类 AddressBookProtos.java。在这个类中,包含了我们在addressbook.proto 文件中指定的message生成的类。每一个类都有一个自己的 Builder ,我们可以用这些builder来创建这些类的实例。

​ 这些根据message生成的类和对应的builder都拥有访问message中每个字段的方法。messages类只拥有getters方法,而对应的builder同时拥有getters和setters方法。

​ 以下是生成的Person类字段的访问方法(实现被省略了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// required string name = 1;
public boolean hasName();
public String getName();

// required int32 id = 2;
public boolean hasId();
public int getId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();

// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);

​ 如你所见,protobuf为message中的每个字段生成了Java-Bean风格的getters,同时,针对每个字段还有一个has方法,如果这个字段的值被设置了,就会返回true,否则返回false。

​ 而 Person.Builder类拥有同样的getter方法和has方法外,还有额外的setter方法,和clear方法,可以修改字段的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();

// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();

// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);
public Builder addPhone(PhoneNumber value);
public Builder addAllPhone(Iterable<PhoneNumber> value);
public Builder clearPhone();

​ 当我们想修改一个字段的值的时候,就调用set方法,如果要移除这个字段的值,就调用clear方法。

​ 对于repeated注释的字段还有额外的方法– count方法(这是list.size()的便捷方法),针对list索引位置的get和set方法,添加一个元素到list的add方法,以及批量添加元素的addAll方法。

​ 注意这个字段的访问方法使用的也是驼峰命名的法则,这个是protobuf自动做的工作,以使得生成的代码更加符合java语言的变成风格。在proto文件中,对于多个单词组成的字段名,我们应该始终使用小写+下划线的方式,以使得生成代码风格良好的代码。阅读 style guide查看更多的细节。

​ 如果你想参考更多的生成Java代码的细节,请参考 Java generated code reference.。

​ 在Person类中,还生成了Java 5中引入的枚举, PhoneType

1
2
3
4
5
6
7
public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}

​ 而PhoneNumber,如你所想,是Person类的一个嵌套类。

使用Java protocol buffer api读写消息

​ protobuf根据.proto文件中message生成的类,都是不可变的( immutable)。一旦这样的一个对象被构建了,就不可以被修改(因为没有提供setter方法)。要构建类实例,我们必须首先创建Builder,然后为每个字段设置值,最后调用build方法来构建实例。

​ 为了方便开发,builder方法是基于链式风格设计的,以下是一个构建Person实例的案例:

1
2
3
4
5
6
7
8
9
10
Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhone(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.HOME))
.build();
标准的message生成类方法

每个message生成类和对应的Builder都有一些其他方法来帮助我们检查和操作这个类,包括:

  • **isInitialized()**:检查所有的required字段是否都被设置

  • **toString()**:返回可以阅读的message信息,以便于调试

  • **mergeFrom(Message other)**:只有builder拥有这个方法,合并其他的message中的内容到这个字段中。如果字段是required或者option,其他的message中内容会覆写原始message字段中的内容,如果字段是repeated,其他message中的字段会添加这个字段中。

  • **clear()**:只有builder具有这个方法,清空message中所有字段的值到初始状态。

完整的Message API文档请参考 complete API documentation for Message.

解析和序列化

最后,每一个生成的类都具有对应的读写为protobuf二进制格式的方法:

  • **byte[] toByteArray()**:序列化成字节数组

  • **static Person parseFrom(byte[] data)**:根据字节数组反序列化

  • **void writeTo(OutputStream output)**:序列化并写入到一个 OutputStream。

  • **static Person parseFrom(InputStream input)**:从一个 InputStream中反序列化

同样,这也只是序列化与反序列的部分方法,完整的API文档请参考 Message API reference。

​ 注意:protobuf生成的类并没有非常好的匹配面向对象设计的思想,其只是一个数据持有者(dumb data holders)。如果你想为已生成的类添加丰富的行为,更好的方法是把已生成的protocol buffer类封装在一个特定于应用程序的类。封装protocol buffers是一个好想法,特别是在你还没能完全掌握某个.proto 文件设计的思路的情况下。你可以通过一个类实现某个接口,接口中只定义必要的访问方法,从而隐藏部分你尚未能完全掌握的字段和方法。你永远不应该写一个类来继承protobuf 生成的类,这可能会打破protobuf的内部机制。

写消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;

class AddPerson {
// This function fills in a Person message based on user input.
static Person PromptForAddress(BufferedReader stdin,
PrintStream stdout) throws IOException {
Person.Builder person = Person.newBuilder();

stdout.print("Enter person ID: ");
person.setId(Integer.valueOf(stdin.readLine()));

stdout.print("Enter name: ");
person.setName(stdin.readLine());

stdout.print("Enter email address (blank for none): ");
String email = stdin.readLine();
if (email.length() > 0) {
person.setEmail(email);
}

while (true) {
stdout.print("Enter a phone number (or leave blank to finish): ");
String number = stdin.readLine();
if (number.length() == 0) {
break;
}

Person.PhoneNumber.Builder phoneNumber =
Person.PhoneNumber.newBuilder().setNumber(number);

stdout.print("Is this a mobile, home, or work phone? ");
String type = stdin.readLine();
if (type.equals("mobile")) {
phoneNumber.setType(Person.PhoneType.MOBILE);
} else if (type.equals("home")) {
phoneNumber.setType(Person.PhoneType.HOME);
} else if (type.equals("work")) {
phoneNumber.setType(Person.PhoneType.WORK);
} else {
stdout.println("Unknown phone type. Using default.");
}

person.addPhone(phoneNumber);
}

return person.build();
}

// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: AddPerson ADDRESS_BOOK_FILE");
System.exit(-1);
}

AddressBook.Builder addressBook = AddressBook.newBuilder();

// Read the existing address book.
try {
addressBook.mergeFrom(new FileInputStream(args[0]));
} catch (FileNotFoundException e) {
System.out.println(args[0] + ": File not found. Creating a new file.");
}

// Add an address.
addressBook.addPerson(
PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
System.out));

// Write the new address book back to disk.
FileOutputStream output = new FileOutputStream(args[0]);
addressBook.build().writeTo(output);
output.close();
}
}
读消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;

class ListPeople {
// Iterates though all people in the AddressBook and prints info about them.
static void Print(AddressBook addressBook) {
for (Person person: addressBook.getPersonList()) {
System.out.println("Person ID: " + person.getId());
System.out.println(" Name: " + person.getName());
if (person.hasEmail()) {
System.out.println(" E-mail address: " + person.getEmail());
}

for (Person.PhoneNumber phoneNumber : person.getPhoneList()) {
switch (phoneNumber.getType()) {
case MOBILE:
System.out.print(" Mobile phone #: ");
break;
case HOME:
System.out.print(" Home phone #: ");
break;
case WORK:
System.out.print(" Work phone #: ");
break;
}
System.out.println(phoneNumber.getNumber());
}
}
}

// Main function: Reads the entire address book from a file and prints all
// the information inside.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
System.exit(-1);
}

// Read the existing address book.
AddressBook addressBook =
AddressBook.parseFrom(new FileInputStream(args[0]));

Print(addressBook);
}
}

在Netty中使用ProtocolBuf

引入坐标

首先我们需要项目中引入maven依赖

1
2
3
4
5
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.0.0</version>
</dependency>
服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
ch.pipeline().addLast(new ProtobufDecoder(AddressBookProtos.Person.getDefaultInstance()));
ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
ch.pipeline().addLast(new ProtobufEncoder());
ch.pipeline().addLast(new AddressBookHandler());
}
});

​ 我们首先在ChannelPipeline中添加了ProtobufVarint32FrameDecoder,其主要用于半包处理

​ 随后添加ProtobufDecoder,它的参数类型是com.google.protobuf.MessageLite,实际上就是告诉ProtobufDecoder需要解码的目标类是什么,否则仅仅从字节数组中是无法判断出要解码的目标类型的,这里我们设置的是AddressBookProtos.Person类型实例,在.proto文件中所有的定义的message在生成的java类,例如这里的Person,都实现了MessageLite接口。

​ ProtobufEncoder类用于对输出的消息进行编码。

​ AddressBookHandler是我们自己定义处理类,在其channelRead方法参数中,Object msg就是解码后的Person,在返回数据时,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {

AddressBookProtos.Person req = (AddressBookProtos.Person) msg;
//...处理req
//返回响应
ctx.writeAndFlush(AddressBookProtos.AddressBook.newBuilder().
addPerson(AddressBookProtos.Person.newBuilder()
.setId(req.getId())
.setName(req.getName())
.setEmail("tianshouzhi@126.com")
.build()));
}
}

​ 可以看到,在Netty中使用了protoBuf之后,我们接受数据与响应数据的协议就是.proto文件生成java对象,这极大的简化了自定义协议的开发。

评论