高性能 Netty 之编解码神器 Google Protobuf

1,369 阅读6分钟

前言

上篇文章我们讲了关于关于编解码的知识以及使用 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 还有其他的优势,例如

  1. 序列化后体积更小,因为它序列化后是二进制,更适合网络传输
  2. 消息格式升级的兼容性还不错
  3. 序列化和反序列化速度都很快

那么,现在我们来使用 NettyProtobuf 来做一个简单的 demo 吧。

图书查询系统

我们接下来的 demo 是一个简单的图书查询系统,业务流程以下

客户端发送查询的的书名给服务端,服务端返回该书籍查询的基本信息

构造对象

使用 Protobuf 来构造对象主要有以下几步:

  1. 下载用于编译 proto.exe 文件
  2. 将需要构造的对象的信息写入 protobuf 文件
  3. 通过 proto.exe 来生成一个 Java POJO 对象
  4. 使用 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 生成对象

生成这一步其实是通过命令行(Windowcmd),使用 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();
    }
}
完结

这篇简单介绍了以下内容:

  1. Google Protobuf 是什么
  2. 怎么使用 Google Protobuf 生成代码
  3. Netty 整合 Google Protobuf 的代码

其实并不是 Google Protobuf 需要配上 Netty 才能用,事实你也可以看见,Protobuf 完全可以独立适用于任何框架或者应用,而 Netty 在整合这方面当作普通的 POJO 来使用,并无二异。

完结!