netty系列之:在netty中使用protobuf協議

flydean 發表於 2021-08-25
Netty

簡介

netty中有很多適配不同協議的編碼工具,對於流行的google出品的protobuf也不例外。netty為其提供了ProtobufDecoder和ProtobufEncoder兩個工具還有對應的frame detection,接下來我們會通過一個例子來詳細講解如何在netty中使用protobuf。

定義protobuf

我們舉個最簡單的例子,首先定義一個需要在網路中進行傳輸的message,這裡我們定義一個student物件,他有一個age和一個name屬性,如下所示:

syntax = "proto3";

package com.flydean17.protobuf;

option java_multiple_files = true;
option java_package = "com.flydean17.protobuf";
option java_outer_classname = "StudentWrapper";

message Student {
  optional int32 age = 1;
  optional string name =2;
}

使用下面的命令,對其進行編譯:

 protoc --experimental_allow_proto3_optional  -I=. --java_out=. student.proto

可以看到生成了3個檔案,分別是Student,StudentOrBuilder和StudentWrapper。其中Student和StudentOrBuilder是我們真正需要用到的物件。

定義handler

在handler中,我們主要進行對訊息進行處理,這裡我們在clientHandler中進行訊息的構建和傳送,StudentClientHandler繼承SimpleChannelInboundHandler並重新channelActive方法, 在該方法中我們使用protobuf的語法,構建一個新的Student例項,並給他設定好age和name兩個屬性。

然後使用ctx.write和ctx.flush方法將其傳送到server端:

    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // channel活躍
        //構建一個Student,並將其寫入到channel中
        Student student= Student.newBuilder().setAge(22).setName("flydean").build();
        log.info("client傳送訊息{}",student);
        ctx.write(student);
        ctx.flush();
    }

StudentServerHandler也是繼承SimpleChannelInboundHandler,並重寫channelRead0方法,當server端讀取到student訊息的時候,日誌輸出,並將其回寫到channel中,供clientHandler讀取:

    public void channelRead0(ChannelHandlerContext ctx, Student student) throws Exception {
        log.info("server收到訊息{}",student);
        // 寫入訊息
        ChannelFuture future = ctx.write(student);
    }

當client讀取到訊息之後,直接日誌輸出,不再做進一步處理,到此,一輪client和server端的互動就完成了:

    public void channelRead0(ChannelHandlerContext ctx, Student student) throws Exception {
        log.info("client收到訊息{}",student);
    }

設定ChannelPipeline

在上一節,不管是在StudentClientHandler還是在StudentServerHandler中,我們都假設channel中傳遞的物件就是Student,而不是原始的ByteBuf。這是怎麼做到的呢?

這裡我們需要使用到netty提供的frame detection,netty為protobuf協議專門提供了ProtobufDecoder和ProtobufEncoder,用於對protobuf物件進行編碼和解碼。

但是這兩個編碼和解碼器分別是MessageToMessageEncoder和MessageToMessageDecoder,他們是訊息到訊息的編碼和解碼器,所以還需要和frame detection配合使用。

netty同樣提供了和protobuf配合使用的frame detector,他們是ProtobufVarint32FrameDecoder和ProtobufVarint32LengthFieldPrepender。

Varint32指的是protobuf的編碼格式,第一個位元組使用的是可變的varint。

有了frame detector和編碼解碼器,我們只需要將其順序加入ChannelPipeline即可。

在客戶端,StudentClientInitializer繼承自ChannelInitializer,我們需要重寫其initChannel方法:

    public void initChannel(SocketChannel ch) {
        ChannelPipeline p = ch.pipeline();

        p.addLast(new ProtobufVarint32FrameDecoder());
        p.addLast(new ProtobufDecoder(Student.getDefaultInstance()));

        p.addLast(new ProtobufVarint32LengthFieldPrepender());
        p.addLast(new ProtobufEncoder());

        p.addLast(new StudentClientHandler());
    }

在伺服器端,同樣StudentServerInitializer也繼承自ChannelInitializer,也需要重寫其initChannel方法:

    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline p = ch.pipeline();

        p.addLast(new ProtobufVarint32FrameDecoder());
        p.addLast(new ProtobufDecoder(Student.getDefaultInstance()));

        p.addLast(new ProtobufVarint32LengthFieldPrepender());
        p.addLast(new ProtobufEncoder());

        p.addLast(new StudentServerHandler());
    }

這樣ChannelPipeline也設定完成了。

構建client和server端並執行

最後好做的就是構建client和server端並執行,這和普通的netty客戶端和伺服器端並沒有什麼區別:

構建StudentClient:

   public static void main(String[] args) throws Exception {

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .handler(new StudentClientInitializer());
            // 建立連線
            Channel ch = b.connect(HOST, PORT).sync().channel();
            // 等待關閉
            ch.closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

構建StudentServer:

   public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new StudentServerInitializer());

            b.bind(PORT).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

執行可得:

server端:
[nioEventLoopGroup-3-1] INFO  c.f.protobuf.StudentServerHandler - server收到訊息age: 22
name: "flydean"

[nioEventLoopGroup-2-1] INFO  c.f.protobuf.StudentClientHandler - client傳送訊息age: 22
name: "flydean"

client端:
[nioEventLoopGroup-2-1] INFO  c.f.protobuf.StudentClientHandler - client收到訊息age: 22
name: "flydean"

可見Student訊息已經傳送和接收成功了。

總結

netty提供了很多和協議適配的工具類,這樣我們就可以專注於業務邏輯,不需要考慮具體的編碼轉換的問題,非常好用。

本文的例子可以參考:learn-netty4

本文已收錄於 http://www.flydean.com/17-netty-protobuf/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!