初期Nettyの原理(四)-シーケンス化

56016 ワード

リモート・プロシージャ・コール(RPC)のプログラムを開発する場合、オブジェクトのシーケンス化と逆シーケンス化の問題が一般的に発生します(TCPやUDPのような低層プロトコルではバイト・ストリームしか転送できないため、アプリケーション・レイヤはJava POJOオブジェクトをバイト・ストリームにシーケンス化して転送する必要があります).
オブジェクトのシーケンス化には、次の方法があります.
  • JSON:Java POJOオブジェクトをJSON構造化文字列に変換します.一般的にWebアプリケーションやモバイル開発に用いられ、可読性が高く、性能が悪い.
  • XML:JSONと同様に、文字列にシーケンス化されているが、フォーマットが異なり、可読性が高く、異性系に一般的に用いられている.
  • JDK内蔵シーケンス化:Java POJOオブジェクトをバイナリバイト配列に変換し、移植性が強く、性能が悪く、可読性が悪い.
  • Protobuf:JDK内蔵シーケンス化のように、Googleオープンソースの高性能、拡張性の高いフレームワークで、一般的に高性能通信に使用されています.

  • 一般的に用いられるシーケンス化方式はJSON(性能要求があまり高くないWeb開発など)とprotobuf(Nettyとともに高性能通信を実現するなどの高性能アプリケーション)である.
    JSON
    JSONシーケンス化フレームワーク
    使用する比較的多い2つのオープンソースの処理JSONのクラスライブラリ:
  • FastJson:アリオープンソースの高性能JSONライブラリです.独自のアルゴリズムを採用してJSONをJava POJOオブジェクトに変換する速度は非常に速いですが、複雑なPOJOをJSONに変換するとエラーが発生する可能性があります.
  • Gson:Googleオープンソースの非常に完璧なJSON解析ライブラリであり、複雑なタイプのPOJOとJSON間の相互変換を完了することができます.

  • 両者の強みとして,POJOからJSONへの移行にはGson(シーケンス化),JSONからPOJOへの移行にはFastJson(逆シーケンス化)を用いるのが一般的である.次はFastJsonとGsonを組み合わせて使用するJSONツールクラスです.
    public class JsonUtil {
    
        //     Gson    
        private static GsonBuilder builder = new GsonBuilder();
        static {
            //   Html    
            builder.disableHtmlEscaping();
        }
    
        /**
         * POJO    
         * @return   Google Gson  
         */
        public static String convertJson(){
            //            Gson  
            Gson gson = builder.create();
            MsgProto.Person person = ProtobufDemo.buildPerson();
            //   Gson POJO   JSON   
            return gson.toJson(person);
        }
    
        /**
         *  json     POJO
         *      FastJson
         * @param json       json
         * @param clazz         
         * @param    
         * @return       POJO
         */
        public static <T>T parseFromJson(String json, Class<T> clazz){
            //   FastJson JSON      POJO
            return JSONObject.parseObject(json, clazz);
        }
    }
    

    JSONシーケンス化と逆シーケンス化の実践
    まず、JsonUtilツールクラスのメソッドを呼び出してシーケンス化と逆シーケンス化を実現するPOJOクラスPersonを定義します.
    public class Person {
    
        private int id;
        private String name;
        private String phone;
        private String address;
    
        public Person(int id, String name, String phone, String address) {
            this.id = id;
            this.name = name;
            this.phone = phone;
            this.address = address;
        }
    
        /**
         *     JSON
         * @return JSON   
         */
        public String convertToJson(){
            return JsonUtil.convertJson(this);
        }
    
        /**
         *        ,       
         * @param json JSON   
         * @return   
         */
        public static Person parseFromJson(String json){
            return JsonUtil.parseFromJson(json, Person.class);
        }
    
        @Override
        public String toString() {
            return super.toString() + "name = " + name + " phone = " + phone + " address = " + address;
        }
    }
    

    もう1つのテスト方法(Junitテストフレームワークを使用)を書き、pojoのシーケンス化と逆シーケンス化をテストします.
        @Test
        public void testJson(){
            Person person = new Person(1, "monkJay", "13330114338", "    ");
            //        JSON   
            String json = person.convertToJson();
            LogUtil.info("    JSON    : [{}]",json);
            //  JSON          
            Person person1 = Person.parseFromJson(json);
            LogUtil.info("        :[{}]", person1);
        }
    

    テスト結果:
        JSON    : [{"id":1,"name":"monkJay","phone":"13330114338","address":"    "}]
            :[pojo.Person@553f17c name = monkJay phone = 13330114338 address =     ]
    

    JSONは文字列であるため、転送JSONと転送文字列はHead-Contentプロトコルを使用している.したがって,NettyでのJSONの転送は前の転送文字列と同様であり,唯一異なるのは復号して文字列を得た後の処理が異なることである(JSON文字列はPOJOオブジェクトに逆シーケンス化される).
    Protobuf
    概要
    protobufはGoogleが提案したデータ交換のフォーマットであり、その符号化過程は、予め定義されたMessageデータ構造を用いて実際に伝送されたデータをパッケージ化し、バイナリに符号化された符号ストリームを伝送または記憶し、復号過程が逆である.Protobufは言語とプラットフォームとは独立しており、公式には多くの言語の実現を提供しています.
    protobufパケットはバイナリのデータであり、それ自体は可読性を備えていないが、バイナリデータであるため占有空間が小さく、相対的に伝送速度が速いため、性能が高く、高性能で、迅速に使いたいデータ伝送シーン(微信はprotobufでメッセージ伝送)に適している.
    サンプルファイル
    Protobufの構文は、公式リファレンスドキュメントの下に簡単な例を参照できます(msg.proto、ファイルは.protoで終わります).
    // [    ]
    syntax = "proto3"; //      protobuf     ,     ,   proto2
    
    // [Java    ]
    option java_package = "protobuf"; //    Java       
    option java_outer_classname = "MsgProto"; //    Java     
    
    // [    ,                Java POJO ]
    // [   POJO        ,         ,             ]
    message Msg {
        uint32 id = 1; //   ID
        string content = 2; //     
    }
    
    message Person {
        uint32 id = 1; // ID
        string name = 2; //   
        string phone = 3; //   
        string address = 4; //   
    }
    

    MavenプラグインによるJavaコードの生成
    まずGithubでprotobufのリリース版のインストールパッケージをダウンロードします.ダウンロードリンク私のここのダウンロードしたのは最新版win 64ので、解凍した後にbinディレクトリの下で1つのprotocを見ることができます.exeプログラム、このプログラムはprotoファイルをコンパイルして対応するPOJOとbuilderコードを生成するために使用されます.コンソールでコマンドを実行するだけで生成できますが、パスの問題で少し面倒なので、公式にはMavenプラグインを使って解決することをお勧めします.
    サードパーティ製のプラグインprotobuf-maven-pluginがあり、Mavenを使用してprotoファイルをコンパイルすることができます.Mavenのpomファイルに以下のプラグイン構成項目を追加すればOKです.
    <plugin>
    <groupId>org.xolstice.maven.pluginsgroupId>
    <artifactId>protobuf-maven-pluginartifactId>
    <version>0.6.1version>
    <configuration>
        
        <protocExecutable>${project.basedir}/src/main/resources/protobuf/protoc.exeprotocExecutable>
        
        <clearOutputDirectory>falseclearOutputDirectory>
        
        <protoSourceRoot>${project.basedir}/src/main/resources/protobufprotoSourceRoot>
        
        <outputDirectory>${project.basedir}/src/main/java/protooutputDirectory>
    configuration>
    <executions>
        <execution>
            <goals>
                <goal>compilegoal>
                <goal>test-compilegoal>
            goals>
        execution>
    executions>
    plugin>
    

    構成が完了すると、プラグインのcompileコマンドを直接実行してJavaコードを生成できますが、このとき生成されたコードはまだ使用できません.protobufの依存を導入していないため、protobuf関連のJavaメソッドは使用できません.では、対応する依存を参照します.この依存バージョンは、ダウンロードしたばかりの実行可能プログラムのバージョンと一致する必要があります.
    <dependency>
    <groupId>com.google.protobufgroupId>
    <artifactId>protobuf-javaartifactId>
    <version>3.11.1version>
    dependency>
    

    protobufシーケンス化の実践
    protoファイルで生成されるPOJOクラスはすべて内部クラスであり,いずれもコンストラクション関数やSetterメソッドがないため,生成されたBuilderコンストラクタを用いてPOJOオブジェクトを構築する必要がある.次に、protoを構築するためのクラスPOJOオブジェクトを新規作成します.
    public class ProtobufDemo {
    
        //             
        public static MsgProto.Msg buildMsg(int id, String content){
            //           Msg      
            MsgProto.Msg.Builder builder = MsgProto.Msg.newBuilder();
            return builder.setId(id).setContent(content).build();
        }
      
        public static MsgProto.Person buildPerson(int id, String name, String phone, String address){
            //           Person      
            MsgProto.Person.Builder builder = MsgProto.Person.newBuilder();
            return builder.setId(id).setName(name).setPhone(phone).setAddress(address).build();
        }
    }
    

    次はprotobufシーケンス化と逆シーケンス化の3つの方法(または使用するJunitテストフレームワーク)です.
        /**
         *              
         *   JDK      ,    POJO     
         */
        @Test
        public void serAndDesr1() throws IOException {
            MsgProto.Msg msg = ProtobufDemo.buildMsg(1, "     proto  ");
            //  protobuf             
            byte[] data = msg.toByteArray();
            //             
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            //             
            outputStream.write(data);
            //            
            data = outputStream.toByteArray();
            //   Protobuf    
            MsgProto.Msg inputMsg = MsgProto.Msg.parseFrom(data);
            LogUtil.info("             : [{}]", inputMsg.getContent());
        }
    
        /**
         *              
         *    POJO                   
         *                     POJO  
         *                , NIO      、    
         */
        @Test
        public void serAndDesr2() throws IOException {
            MsgProto.Msg msg = ProtobufDemo.buildMsg(2, "     proto  ");
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            //          
            msg.writeTo(outputStream);
            ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
            //             POJO  
            MsgProto.Msg inputMsg = MsgProto.Msg.parseFrom(inputStream);
            LogUtil.info("             : [{}]", inputMsg.getContent());
        }
    
        /**
         *               
         *      ,    /    
         */
        @Test
        public void serAndDesr3() throws IOException {
            MsgProto.Msg msg = ProtobufDemo.buildMsg(3, "     proto  ");
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            //                   ,  Head-Content  
            // protobuf       ,      varint32,      
            msg.writeDelimitedTo(outputStream);
            ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
            MsgProto.Msg inputMsg = MsgProto.Msg.parseDelimitedFrom(inputStream);
            LogUtil.info("             : [{}]", inputMsg.getContent());
        }
    

    実行結果:
    20:38:26.804 [main] INFO TestProtobufDemo -              : [     proto  ]
    20:38:26.818 [main] INFO TestProtobufDemo -              : [     proto  ]
    20:38:26.818 [main] INFO TestProtobufDemo -              : [     proto  ]
    

    protobufのNettyでの復号符号化の実践
    NettyにはProtobuf専用のベースデコードエンコーダが内蔵されています.
  • ProtobufEncoderエンコーダ:msgを直接使用する.toByteArray()POJOオブジェクトをバイナリバイト配列に符号化し、ByteBufパケットに入れて次局処理に渡す.
  • ProtobufVarint 32 LengthFieldPrepender長さエンコーダ:パケット内のバイト数を読み出し、バイト数の長さの桁数(varint 32の桁数)を計算し、出力パケットに長さを書き、元のデータを書き込む(Head-Contentで議論されている文字列の書き込みのように).
  • ProtobufVarint 32 FrameDecoder長さデコーダ:パケット内のvarint 32の長さ値に基づいて、十分なバイト配列を復号し、長さに基づいてバッファの保持スライス(実際には長さフィールドを除去し、データ部分をスライス)
  • を返す.
  • ProtobufDecoderデコーダ:デコーダは構築時にプロトタイプPOJOのインスタンスを指定する必要があり、プロトタイプに基づいて対応する解析器を見つけ、バイナリのバイトデータをprotoで定義されたPOJOオブジェクトに解析する.

  • 次に、クライアントとサーバ側の実践を見てみましょう.サーバ側:
    public class EchoServer {
    
        private ServerBootstrap sb = new ServerBootstrap();
        private int port;
    
        public EchoServer(int port){
            this.port = port;
        }
    
        public void runServer(){
            NioEventLoopGroup boss = new NioEventLoopGroup(1);
            NioEventLoopGroup worker = new NioEventLoopGroup();
            try {
                sb.group(boss, worker)
                        .channel(NioServerSocketChannel.class)
                        .localAddress("127.0.0.1", port)
                        .option(ChannelOption.SO_KEEPALIVE, true)
                        .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) {
                                ch.pipeline().addLast(new ProtobufVarint32FrameDecoder())
                                        .addLast(new ProtobufDecoder(MsgProto.Person.getDefaultInstance()))
                                        .addLast(new MyDecoder());
                            }
                        });
                //            
                ChannelFuture future = sb.bind().sync();
                //          
                future.channel().closeFuture().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                boss.shutdownGracefully();
                worker.shutdownGracefully();
            }
        }
    
        private static class MyDecoder extends ChannelInboundHandlerAdapter {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) {
                //   Netty          ,       Person  
                MsgProto.Person person = (MsgProto.Person)msg;
                LogUtil.info("        : [{}]", person);
            }
        }
    
        public static void main(String[] args){
            new EchoServer(88).runServer();
        }
    }
    

    クライアント:
    public class EchoClient {
    
        private Bootstrap bootstrap = new Bootstrap();
        private String ip;
        private int port;
    
        public EchoClient(String ip,int port){
            this.ip = ip;
            this.port = port;
        }
    
        public void runClient(){
            NioEventLoopGroup group = new NioEventLoopGroup();
            try {
                bootstrap.group(group)
                        .channel(NioSocketChannel.class)
                        .remoteAddress(ip, port)
                        .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) {
                                ch.pipeline()
                                        .addLast(new ProtobufVarint32LengthFieldPrepender())
                                        .addLast(new ProtobufEncoder());
                            }
                        });
                //            
                ChannelFuture future = bootstrap.connect().sync();
                Channel channel = future.channel();
                //     Person  
                MsgProto.Person person = ProtobufDemo.buildPerson(1, "monkJay", "13330114338", "    ");
                //        
                channel.writeAndFlush(person);
                //          
                channel.closeFuture().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                group.shutdownGracefully();
            }
        }
        public static void main(String[] args){
            new EchoClient("127.0.0.1", 88).runClient();
        }
    }