前言
上篇文章我们讲了关于关于编解码的知识以及使用 Java 序列化作为编解码作为解析框架。本来是想一篇长文记录将编解码以及其应用给涵盖的。但是发现实时分开来写会比较明确,就当是做一个记录吧。
Google Protobuf
什么是 Protobuf ? 我沿用了谷歌开发者中心的一句话
Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.
上面的话翻译过来就是,Protobuf是一个跨语言 / 跨平台,扩展性可序列化的数据结构。Protobuf 是 Google 的语言无关、平台无关、可扩展的序列化结构化数据的机制。
它将数据结构以 .proto 文件进行描述,通过代码生成工具可以生成对应数据结构的 POJO 对象和 Protobuf 相关的方法和属性。您只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码,轻松地将结构化数据写入和读取到各种数据流中,并使用多种语言。
那么支持的语言有哪些呢? 它支持 C++ / C# / Dart / Go / Java 以及 Python。可以说目前大多数主流行的语言都支持了。跨语言的好处我也说过,就是在异构语言系统中能够无缝切换的进行数据交换,这是 Java 序列化所无法比拟的优势。例外,Protobuf 还有其他的优势,例如
- 序列化后体积更小,因为它序列化后是二进制,更适合网络传输
- 消息格式升级的兼容性还不错
- 序列化和反序列化速度都很快
那么,现在我们来使用 Netty 和 Protobuf 来做一个简单的 demo 吧。
图书查询系统
我们接下来的 demo 是一个简单的图书查询系统,业务流程以下
客户端发送查询的的书名给服务端,服务端返回该书籍查询的基本信息
构造对象
使用 Protobuf 来构造对象主要有以下几步:
- 下载用于编译
proto.exe文件 - 将需要构造的对象的信息写入
protobuf文件 - 通过
proto.exe来生成一个Java POJO对象 - 使用
POJO进行应用开发
下载 Protobuf
下载 Protobuf 我们可以到 Github 来下载去下 https://github.com/protocolbuffers/protobuf/releases。
上面有 OS 系统,有 window 系统,以及 linux 系统支持,所以可以根据系统来挑选对应的版本。
我这里选择的是 window 系统来进行开发,所以我的版本是 protoc-3.12.3-win64.zip。
创建对象的 .proto 文件
我们需要将需要构造的对象的信息写入 Protobuf 文件。首先我们构造的是 SubscribeReq 请求类。
SubscribeReq.proto
package netty;
option java_package="codec.protobuf";
option java_outer_classname="SubscribeReqProto";
message SubscribeReq{
required int32 subReqID = 1;
required string userName = 2;
required string productName = 3;
required string address = 4;
}
我解释一下,package 指的是生成后生成的文件夹;java_package 指的是生成的 POJO 对象当前所处于你的项目包之下;而 java_outer_classname 指的是生成的 POJO 对象的实际类名。
上面构造的客户端请求服务端的请求实体类,接下来是构造服务端应答客户端的回答类。
SubscribeResp.proto
package netty;
option java_package="codec.protobuf";
option java_outer_classname="SubscribeRespProto";
message SubscribeResp{
required int32 subReqID = 1;
required int32 respCode = 2;
required string desc = 3;
}
构造完毕后,我们继续往下走!
使用 proto.exe 生成对象
生成这一步其实是通过命令行(Window 的 cmd),使用 proto.exe 来对指定的 .proto 文件进行生成 POJO 对象。
由于是在 window 的整合环境,所以我们需要打开 cmd。然后使用命令来到你刚下载的 protobuf 文件夹下面。通过以下命令生成 POJO 对象
protoc.exe -I=$SRC_DIR --java_out=$DST_DIR +$SRC_DIR/target.proto
其中 java_out 是 java 输出的路径,而紧随后面的 $SRC_DIR/target.proto 表示的是关于目标的 proto。
那么现在我们对我们使用的两个文件进行编译处理。为了方便,我直接把两个文件 copy 到与 proto.exe 同级目录下。打开cmd 然后进入 proto.exe 的相对目录下。
protoc.exe --java_out=./ ./SubscribeReq.proto
简单解释下,编译的当前路径下的 SubscribeReq.proto ,并将生成的结果输出到当前文件夹。 接着是应答文件的编译
protoc.exe --java_out=./ ./SubscribeResp.proto
接下来你可以看下与 ./proto.exe 同级目录下有没有生成对应的文件。
使用 POJO 进行应用开发
现在开始正式的业务开发。首先我们需要将生成的 POJO 复制到项目的目录下。
环境依赖
<dependencies>
<!-- netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.50.Final</version>
</dependency>
<!-- protobuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.12.2</version>
</dependency>
</dependencies>
服务端
首先是服务器的启动类。
public class SubReqServer {
public void bind(int port) throws Exception {
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
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( SubscribeReqProto.SubscribeReq.getDefaultInstance()));
ch.pipeline().addLast(
new ProtobufVarint32LengthFieldPrepender());
ch.pipeline().addLast(new ProtobufEncoder());
ch.pipeline().addLast(new SubReqServerHandler());
}
});
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new SubReqServer().bind(8080);
}
}
然后是服务器的处理类
@Sharable
public class SubReqServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
SubscribeReqProto.SubscribeReq req = (SubscribeReqProto.SubscribeReq) msg;
if ("netty".equalsIgnoreCase(req.getUserName())) {
System.out.println("Service accept client subscribe req : ["
+ req.toString() + "]");
ctx.writeAndFlush(resp(req.getSubReqID()));
}
}
private SubscribeRespProto.SubscribeResp resp(int subReqID) {
SubscribeRespProto.SubscribeResp.Builder builder = SubscribeRespProto.SubscribeResp
.newBuilder();
builder.setSubReqID(subReqID);
builder.setRespCode(0);
builder.setDesc("Netty with protobuf");
return builder.build();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();// 发生异常,关闭链路
}
}
客户端
我们来写客户端的启动类
public class SubReqClient {
public void connect(int port, String host) throws Exception {
// 配置客户端NIO线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(
new ProtobufVarint32FrameDecoder());
ch.pipeline().addLast(
new ProtobufDecoder(
SubscribeRespProto.SubscribeResp
.getDefaultInstance()));
ch.pipeline().addLast(
new ProtobufVarint32LengthFieldPrepender());
ch.pipeline().addLast(new ProtobufEncoder());
ch.pipeline().addLast(new SubReqClientHandler());
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 当代客户端链路关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
new SubReqClient().connect(8080, "127.0.0.1");
}
}
然后是我们写客户端的处理类
public class SubReqClientHandler extends ChannelInboundHandlerAdapter {
public SubReqClientHandler() {
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i = 0; i < 10; i++) {
ctx.write(subReq(i));
}
ctx.flush();
}
private SubscribeReqProto.SubscribeReq subReq(int i) {
SubscribeReqProto.SubscribeReq.Builder builder = SubscribeReqProto.SubscribeReq
.newBuilder();
builder.setSubReqID(i);
builder.setUserName("netty");
builder.setProductName("netty with protobuf");
builder.setAddress("netty address");
return builder.build();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println("Receive server response : [" + msg + "]");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
完结
这篇简单介绍了以下内容:
Google Protobuf是什么- 怎么使用
Google Protobuf生成代码 Netty整合Google Protobuf的代码
其实并不是 Google Protobuf 需要配上 Netty 才能用,事实你也可以看见,Protobuf 完全可以独立适用于任何框架或者应用,而 Netty 在整合这方面当作普通的 POJO 来使用,并无二异。
完结!