javascript序列化json-JSON 和 ProtoBuf 序列化

2023-08-29 0 4,810 百度已收录

JSON 和 ProtoBuf 序列

当我们开发一些远程过程调用(RPC)程序时,一般都会涉及到对象的序列化/反序列化。 例如,客户端通过TCP方式向服务器发送一个“Person”对象; 由于TCP合约(UDP这些底层合约)只能发送字节流,因此应用层需要将JavaPOJO对象序列化为字节流,数据接收方可以将其反序列化为JavaPOJO对象。 “序列化”必须涉及编码和低格式(Encoding&Format)。 目前我们可以选择的编码方式有:

为了评价一个序列化框架的异同,我们从两个方面入手:

(1) 结果数据的大小。 原则上,序列化数据大小越小,传输效率越高。 (2)结构复杂度,会影响序列化/反序列化的效率,结构越复杂,耗时越多。

理论上来说,对于性能要求不高的服务器程序,可以选择JSON系列的序列化框架; 对于性能要求比较高的服务器程序,应该选择传输效率更高的二补序列化框架。 建议是Protobuf。

Protobuf是一个高性能、易于扩展的序列化框架。 其性能测试相关数据请参考官方文档。 Protobuf本身非常简单,易于开发,并且与Netty框架结合,非常方便地实现通信应用。 反过来,Netty也提供了相应的编解码器,解决了Protobuf在Socket通信中出现的“半包、粘包”的问题。 无论是使用JSON和Protobuf,还是其他反序列化合约,我们都要保证在数据包反序列化之前,接收端的ByteBuf补码包必须是完整的应用层补码包,而不是半包或者粘包。

粘包和拆包解读

半包问题的实际例子

修改上面的NettyEchoClient实例,以循环的形式向NettyEchoServer echo服务器写入大量ByteBuf,然后看实际的服务器响应结果。 注意:服务器类不需要修改,直接使用之前的echo服务器即可。 修改后的客户端类 - 称为 NettyDumpSendClient。 客户端成功建立连接后,使用for循环通过通道不断将ByteBuf写入服务端。 还是讲1000遍,写的Bytebuf的内容是一样的,都是字符串内容:“疯狂创客圈:高性能学习者社区!”。 代码如下所示:

        package com.crazymakercircle.netty.echoServer;
        //...
        public class NettyDumpSendClient {
            private int serverPort;
            private String serverIp;
            Bootstrap b = new Bootstrap();
            public NettyDumpSendClient(String ip, int port) {
                this.serverPort = port;
                this.serverIp = ip;
            }
            public void runClient() {
                //创建反应器线程组
                //...省略,启动客户端Bootstrap启动器配置和启动
                // 阻塞,直到连接完成
                f.sync();
                Channel channel = f.channel();
                //发送大量的文字
                String content= "疯狂创客圈:高性能学习者社群!";
                byte[] bytes =content.getBytes(Charset.forName("utf-8"));
                for (int i = 0; i< 1000; i++) {
                    //发送ByteBuf
                    ByteBuf buffer = channel.alloc().buffer();
                    buffer.writeBytes(bytes);
                    channel.writeAndFlush(buffer);
                }
                //...省略从容关闭客户端
            }
            public static void main(String[] args) throws InterruptedException {
                int port = NettyDemoConfig.SOCKET_SERVER_PORT;
                String ip = NettyDemoConfig.SOCKET_SERVER_IP;
                new NettyDumpSendClient(ip, port).runClient();
            }
        }

在运行程序查看结果之前,首先要启动的是上面介绍的NettyEchoServer回显服务器。 之后,启动新编译的客户端NettyDumpSendClient程序。 客户端程序连接成功后,会向服务器发送1000个ByteBuf内容缓冲区。 服务器NettyEchoServer收到后,会输出到控制台,然后奉献给客户端。 服务器的输出如图 8-1 所示。

仔细观察服务器的控制台输出,我们可以看到有三种类型的输出:(1)读取完整的客户端输入ByteBuf。 (2) 从多个客户端读取 ByteBuf 输入并将它们“粘”在一起。 (3)读取部分ByteBuf内容,但出现乱码。 然后仔细观察客户端的输出。 可以看到,上述三种输出在客户端和服务器端同样存在。

对应第一种情况,接收端接收到的完整的ByteBuf在这里被称为“全包”。 对应第二种情况,多个发送方的输入ByteBuf“粘”在一起,这些读取到的ByteBuf被称为“粘包”。 对应第三种情况,一个输入的ByteBuf被“拆解”读取,读取到一个破包,这些读取到的ByteBuf被称为“半包”。 为了简单起见,“粘包”的情况也可以看作是一种特殊的“半包”。 “棒包”和“半包”可以统称为传输的“半包问题”。

半袋问题

(1)粘包是指接收端(Receiver)收到一个ByteBuf,其中包含发送端(Sender)的多个ByteBuf,多个ByteBuf“粘”在一起。 (2)半包,即接收端“解包”发送端的一个ByteBuf,收到多个破包。 换句话说,接收方收到的 ByteBuf 是发送方的 ByteBuf 的一部分。 粘包和半包是指ByteBuf缓冲区一次接收异常,如图8-2所示。

javascript序列化json-JSON 和 ProtoBuf 序列化

半封装现象的原理

寻根粘包和半包的来源还得从操作系统底层开始。 众所周知,底层网络以二进制补码字节数据包的形式传输数据。 读取数据的过程大致如下:当IO可读时,Netty会从底层网络读取二补码数据到ByteBuf缓冲区中,然后交给Netty程序转换成JavaPOJO对象。 写入数据的过程大致是这样的:中间编码器的工作是将Java类型的数据转换成只能在底层传输的二补ByteBuf缓冲数据。 解码器的作用则相反,就是将从底层传入的二补ByteBuf缓冲区数据转换成只能由Java处理的JavaPOJO对象。 在发送端Netty的应用层进程缓冲区中,程序以ByteBuf为单位发送数据,当到达底层操作系统内核缓冲区时,底层会按照合约规范重新组装数据包,并组装起来转化为传输层TCP层合约消息,然后发送。 接收端收到传输层的二补数据包后,首先保存在内核缓冲区中,然后在Netty读取ByteBuf时复制到进程缓冲区中。 在接收端,当Netty程序将数据从内核缓冲区复制到Netty进程缓冲区的ByteBuf时,问题就出现了:

(1)首先,底层缓冲区每次的数据容量是有限的。 当TCP底层缓冲的数据包比较大时,一个底层数据包会被分成多个ByteBuf进行复制,导致进程缓冲区读取到的数据是半包。 (2)当TCP底层缓冲的数据包比较小时,一次复制多个内核缓冲数据包,导致进程缓冲读取粘包。

怎么解决呢? 基本思想是,在接收端,Netty程序需要根据自定义契约重新组装应用层的读过程缓冲区ByteBuf,重新组装我们应用层的数据包。 接收端的这个过程一般称为发包,或拆包。 在Netty中,有两种发送数据包的方式,从第7章我们可以看到: (1)可以自定义解码器数据包发送方:基于ByteToMessageDecoder或ReplayingDecoder,定义自己的进程缓冲区数据包发送方。 (2)使用Netty的外部解码器。 例如,使用Netty外部的LengthFieldBasedFrameDecoder自定义定界符包解码器,以正确打包进程缓冲区ByteBuf。 本章前面将使用这两种方法。

JSON合约通信

Java有3个流行的开源泛型用于处理JSON数据:阿里的FastJson、谷歌的Gson和开源社区的Jackson。

Jackson 是一个简单的、基于 Java 的 JSON 开源库。 使用Jackson开源库,可以轻松将JavaPOJO对象转换为JSON和XML格式字符串; 您还可以轻松地将 JSON 和 XML 字符串转换为 JavaPOJO 对象。 Jackson开源库的优点是:依赖jar包少、使用方便、性能好。 另外,Jackson社区也比较活跃。 Jackson开源库的缺点是复杂的POJO类型、复杂的集合Map、List的转换结果不是标准的JSON格式,或者可能存在一些问题。

Google的Gson开源库是一个功能齐全的JSON解析库。 它源于Google内部需求,由Google自己开发。 2008年5月第一个版本公开发布后,已被许多公司或用户使用。 Gson可以完成复杂类型的POJO和JSON字符串之间的相互转换,转换能力非常强。

阿里巴巴的FastJson是一个高性能的JSON库。 据传FastJson在转换POJO复杂类型的JSON时,可能会出现一些引用类型导致JSON转换出错,需要自定义引用。 顾名思义,在性能方面javascript序列化json,FastJson 库使用独创的算法,以极高的速率将 JSON 转换为 POJO,超越其他 JSON 开源库。

JSON传输的编码器和解码器原理

本质上,JSON 格式只是一种组织字符串的方式。 因此,用于传输 JSON 的合约与用于传输纯文本的合约没有太大区别。 下面使用常用的Head-Content合约来介绍JSON传输。 Head-Content数据包的解码过程如图8-3所示,具体如下:

首先使用LengthFieldBasedFrameDecoder(Netty的外部自定义长度包解码器)对Head-Content补码数据包进行解码,并对Content数组的补码内容进行解码。 之后,使用StringDecoder字符串解码器(Netty的外部解码器)将二进制补码内容解码为JSON字符串。 最后,使用 JsonMsgDecoder 解码器(自定义解码器)将 JSON 字符串解码为 POJO 对象。

首先使用 StringEncoder 编码器(Netty 外部)将 JSON 字符串编码为二补字节链表。 之后,使用LengthFieldPrepender编码器(Netty外部)将二补字节链表编码为Head-Content二补数据包。 LengthFieldPrepender编码器的作用:添加数据包后面内容的二补字节字段的厚度。 该编码器和 LengthFieldBasedFrameDecoder 解码器是天生的一对,并且经常一起使用。 这组“天匹配”属于Netty提供的一组特别重要的编码器和解码器,常用于Head-Content包的传输。

JSON传输的服务器端实际案例

为了清楚地演示JSON传输,下面设计了一个简单的客户端/服务器传输程序:服务器接收客户端发来的数据包,解码为JSON,然后转换为POJO; 客户端将 POJO 转换为 JSON 字符串,对其进行编码并将其发送到服务终端。

javascript序列化json-JSON 和 ProtoBuf 序列化

为了简化流程,服务器端代码只包含入站处理的过程,不包含出站处理的过程。 也就是说,服务器端程序只读取客户端数据包并完成解码。 服务器端的程序不会向对等方(即客户端)写入任何输出数据包。 服务器端实践案例的程序代码如下:

        package com.crazymakercircle.netty.protocol;
        //...
        public class JsonServer {
            //...省略成员属性,构造器
            public void runServer() {
              //创建反应器线程组
              EventLoopGroupbossLoopGroup = new NioEventLoopGroup(1);
              EventLoopGroupworkerLoopGroup = new NioEventLoopGroup();
              try {
                  //...省略:启动器的反应器线程,设置配置项
                  //5 装配子通道流水线
                  b.childHandler(new ChannelInitializer<SocketChannel>() {
                      //有连接到达时会创建一个通道
                      protected void initChannel(SocketChannel ch) throws Exception {
                          // 流水线管理子通道中的Handler业务处理器
                          // 向子通道流水线添加3个Handler业务处理器
                          ch.pipeline().addLast(
                          new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
                          ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
                          ch.pipeline().addLast(new JsonMsgDecoder());
                      }
                  });
                  //....省略端口绑定,服务监听,从容关闭
            }
            //服务器端业务处理器
            static class JsonMsgDecoderextends ChannelInboundHandlerAdapter {
              @Override
              public void channelRead(ChannelHandlerContext ctx, Object msg) throws
                Exception {
                  String json = (String) msg;
                  JsonMsg jsonMsg = JsonMsg.parseFromJson(json);
                  Logger.info("收到一个Json数据包 =》" + jsonMsg);
              }
          }
          public static void main(String[] args) throws InterruptedException {
              int port = NettyDemoConfig.SOCKET_SERVER_PORT;
              new JsonServer(port).runServer();
          }
        }

客户端JSON传输实战案例

为了简化流程,客户端代码只包含出站处理的流程,不包含入站处理的流程。 也就是说,客户端的程序只是对数据进行编码,然后将数据包发送到服务器端。 客户端的程序不处理从另一端(即服务器端)传入的数据包。 客户流程大致如下:

(1)通过微软的Gson框架将POJO序列化为JSON字符串。

(2) 然后,使用 StringEncoder 编码器(Netty 外部)将 JSON 字符串编码为二补字节链表。

(3)最后使用LengthFieldPrepender编码器(Netty外部)将二补字节链表编码成Head-Content格式的二补数据包。 客户端实践案例的程序代码如下:

客户端实践案例的程序代码如下:

        package com.crazymakercircle.netty.protocol;
        //....
        public class JsonSendClient {
            static String content = "疯狂创客圈:高性能学习社群!";
            //...省略成员属性,构造器
            public void runClient() {
              //创建反应器线程组
              EventLoopGroupworkerLoopGroup = new NioEventLoopGroup();
              try {
                  //1 设置反应器线程组
                  b.group(workerLoopGroup);
                  //2 设置nio类型的通道
                  b.channel(NioSocketChannel.class);
                  //3 设置监听端口
                  b.remoteAddress(serverIp, serverPort);
                  //4 设置通道的参数
                  b.option(ChannelOption.ALLOCATOR,
                            PooledByteBufAllocator.DEFAULT);
                  //5 装配通道流水线
                  b.handler(new ChannelInitializer<SocketChannel>() {
                      //初始化客户端通道
                      protected void initChannel(SocketChannelch) throws Exception {
                          // 客户端通道流水线添加2个Handler业务处理器
                          ch.pipeline().addLast(new LengthFieldPrepender(4));
                          ch.pipeline().addLast(new
                                            StringEncoder(CharsetUtil.UTF_8));
                      }
                  });
                  ChannelFuture f = b.connect();
                  f.addListener((ChannelFuturefutureListener) ->
                  {
                      if (futureListener.isSuccess()) {
                        Logger.info("EchoClient客户端连接成功!");
                      } else {
                        Logger.info("EchoClient客户端连接失败!");
                      }
                  });
                  // 阻塞,直到连接完成
                  f.sync();
                  Channel channel = f.channel();
                  //发送Json字符串对象
                  for (int i = 0; i< 1000; i++) {
                      JsonMsg user = build(i, i + "->" + content);
                      channel.writeAndFlush(user.convertToJson());
                      Logger.info("发送报文:" + user.convertToJson());
                  }
                  channel.flush();
                  // 7 等待通道关闭的异步任务结束
                  // 服务监听通道会一直等待通道关闭的异步任务结束
                  ChannelFuturecloseFuture = channel.closeFuture();
                  closeFuture.sync();
              } catch (Exception e) {
                  e.printStackTrace();
              } finally {
                  // 从容关闭EventLoopGroup,
                  // 释放掉所有资源,包括创建的线程
                  workerLoopGroup.shutdownGracefully();
              }
            }
            //构建Json对象
            public JsonMsgbuild(int id, String content) {
              JsonMsg user = new JsonMsg();
              user.setId(id);
              user.setContent(content);
              return user;
            }
            public static void main(String[] args) throws InterruptedException {
              int port = NettyDemoConfig.SOCKET_SERVER_PORT;
              String ip = NettyDemoConfig.SOCKET_SERVER_IP;
              new JsonSendClient(ip, port).runClient();
            }
        }

执行顺序为:先启动服务端,再启动客户端。 启动后,客户端将向服务器发送 1000 个 POJO 转换后的 JSON 字符串。 如果可以从服务器的控制台看到JSON格式的输出字符串,则说明程序运行正确。

Protobuf合约通信

Protobuf是Google提出的一种数据交换格式。 它是一组类似于JSON或XML的数据传输格式和规范,用于不同应用程序或进程之间的通信。 Protobuf的编码过程是:使用预定义的Message数据结构对实际传输的数据进行打包,然后编码成二补码率进行传输或存储。 Protobuf的解码过程正好与编码过程相反:将二补码帧率解码为Protobuf本身定义的Message结构的POJO实例。

Protobuf 既独立于语言,又独立于平台。 Google 官方提供了多种语言的实现:Java、C#、C++、GO、JavaScript 和 Python。 Protobuf数据包是一种二补码格式,比文本格式(JSON、XML)的数据交换要快得多。 由于Protobuf的优异性能,它越来越适合分布式应用场景中的数据通信或者异构环境中的数据交换。

与JSON和XML相比,Protobuf是后起之秀,它是Google开源的一种数据格式。 只不过Protobuf越来越适合高性能、快速响应的数据传输应用场景。 另外,JSON和XML都是文本格式,数据可读; 而Protobuf是二补码数据格式,数据本身是不可读的,只有反序列化后才能得到真正可读的数据。 由于Protobuf是二补码数据格式,数据序列化后,体积比JSON和XML更小,更适合网络传输。

一般来说,在需要大量数据传输的应用场景中,由于数据量较大,选择Protobuf可以显着减少传输的数据量,提高网络IO的速度。 对于构建高性能通信服务器,Protobuf 传输合约是性能最高的传输合约之一。 Momo的消息传输使用Protobuf合约。

简单proto文件的实际案例

Protobuf 使用 proto 文件来预定义消息格式。 数据包按照proto文件定义的消息格式以二补码率进行编码和解码。 proto文件,简单来说,就是一个消息的合约文件,这个合约文件的后缀文件名为“.proto”。 作为演示,下面介绍一个非常简单的proto文件:只定义了一个消息结构,但是消息结构也很简单,只包含两个数组。 示例如下:

        // [开始头部声明]
        syntax = "proto3";
        packagecom.crazymakercircle.netty.protocol;
        // [结束头部声明]
        // [开始java选项配置]
        option java_package = "com.crazymakercircle.netty.protocol";
        option java_outer_classname = "MsgProtos";
        // [结束java选项配置]
        // [开始消息定义]
        message Msg {
          uint32 id = 1;  //消息ID
          string content = 2; //消息内容
        }
        // [结束消息定义]

在“.proto”文件的腹部声明中,需要声明“.proto”使用的Protobuf合约版本,这里是“proto3”。 也可以使用旧版本“proto2”,两个版本的消息格式略有不同。 默认合约版本是“proto2”。

Protobuf支持多种语言,因此它为不同的语言提供了一些可选的声明选项,选项后面跟着option关键字。 “java_package”选项的作用是在“proto”文件中生成消息的POJO类和Builder(构造函数)的Java代码时,将Java代码加载到指定的包中。 “java_outer_classname”选项的作用是:生成“proto”文件对应的Java代码时,生成的Java外部类的名称。 在“proto”文件中javascript序列化json,使用message关键字来定义消息的结构。 当生成“proto”对应的Java代码时,每个具体的消息结构对应一个最终的JavaPOJO类。 消息结构数组对应于 POJO 类的属性。 换句话说,每定义一个“消息”结构就相当于在Java中声明一个类。 并且消息中可以嵌入消息,就像java的内部类一样。

每个消息结构可以有多个数组。 定义数组的格式,简单来说就是“类型名称=数字”。 如“stringcontent=2;”,表示该数组为字符串类型,命名为content,序号为2。数组的序号表示为:Protobuf数据包时对数组的具体排序被序列化和反序列化。 在每个“.proto”文件中,可以声明多个“消息”。 在大多数情况下,具有依赖项或包含项的消息结构将被写入到 . 原型文件。 将这些不相关的消息结构写到不同的文件中,方便管理。

消息POJO和Builder的使用实战案例

将protobuf的Java运行时包的依赖添加到Maven的pom.xml文件中,代码如下:

        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>${protobuf.version}</version>
        </dependency>

这里的protobuf.version版本号具体值为3.6.1。 也就是说,Java运行时的potobuf依赖包的版本和“.proto”消息结构文件中的语法配置版本,以及用于编译“.proto”消息的编译器“protoc3.6.1.exe”的版本。 proto”文件,这三个版本需要兼容。

1.使用Builder构造函数构造POJO消息对象

        package com.crazymakercircle.netty.protocol;
        //...
        public class ProtobufDemo {
            public static MsgProtos.MsgbuildMsg() {
              MsgProtos.Msg.BuilderpersonBuilder = MsgProtos.Msg.newBuilder();
              personBuilder.setId(1000);
              personBuilder.setContent("疯狂创客圈:高性能学习社群");
              MsgProtos.Msg message = personBuilder.build();
              return message;
            }
         //…..
        }

Protobuf为每个消息结构生成的Java类包含一个POJO类和一个Builder类。 要构造POJO消息,首先需要使用POJO类的newBuilder静态方法来获取一个Builder构造函数。 每个POJO数组的值都需要通过Builder构造函数的setter方法来设置。 请注意,消息 POJO 对象没有 setter 方法。 设置数组值后,使用构造函数的 build() 方法构造 POJO 消息对象。

2.序列化和反序列化的形式1 反序列化

        package com.crazymakercircle.netty.protocol;
        //...
        public class ProtobufDemo {
            //第1种方式:序列化serialization &反序列化Deserialization
            @Test
            public void serAndDesr1() throws IOException {
              MsgProtos.Msg message = buildMsg();
              //将Protobuf对象序列化成二进制字节数组
              byte[] data = message.toByteArray();
              //可以用于网络传输,保存到内存或外存
              ByteArrayOutputStreamoutputStream = new ByteArrayOutputStream();
              outputStream.write(data);
              data = outputStream.toByteArray();
              //二进制字节数组反序列化成Protobuf对象
              MsgProtos.MsginMsg = MsgProtos.Msg.parseFrom(data);
              Logger.info("id:=" + inMsg.getId());
              Logger.info("content:=" + inMsg.getContent());
            }
        //….
        }

这些方法通过调用 POJO 对象的 toByteArray() 方法将 POJO 对象序列化为字节列表。 通过调用parseFrom(byte[]data),Protobuf还可以从字节列表中反序列化,得到一个新的POJO实例。 这些方法与普通Java对象的序列化类似,适用于Protobuf POJO序列化到显存或内层的很多应用场景。

3.序列化&反序列化的形式2 反序列化

        package com.crazymakercircle.netty.protocol;
        //...
        public class ProtobufDemo {
         //…
            //第2种方式:序列化serialization &反序列化Deserialization
            @Test
            public void serAndDesr2() throws IOException {
              MsgProtos.Msg message = buildMsg();
              //序列化到二进制码流
              ByteArrayOutputStreamoutputStream = new ByteArrayOutputStream();
              message.writeTo(outputStream);
              ByteArrayInputStreaminputStream =
              new ByteArrayInputStream(outputStream.toByteArray());
              //从二进码流反序列化成Protobuf对象
              MsgProtos.MsginMsg = MsgProtos.Msg.parseFrom(inputStream);
              Logger.info("id:=" + inMsg.getId());
              Logger.info("content:=" + inMsg.getContent());
            }
        //….
        }

这些方法通过调用 POJO 对象的 writeTo(OutputStream) 方法将 POJO 对象的二进制补码字节写入到输出流。 Protobuf通过调用parseFrom(InputStream)从输入流中读取二补码率并重新反序列化,得到新的POJO实例。 这些序列化和反序列化的形式在阻塞二补码左传应用场景中是没有问题的。 例如,可以将二进制补码码率写入阻塞 JavaOIO 套接字或输出到文件。 而且这些方法在异步操作的NIO应用场景中都存在粘包/半包的问题。

javascript序列化json-JSON 和 ProtoBuf 序列化

4.序列化&反序列化的形式3 反序列化

        package com.crazymakercircle.netty.protocol;
        //...
        public class ProtobufDemo {
         //…
            //第3种方式:序列化serialization &反序列化Deserialization
            //带字节长度:[字节长度][字节数据],解决粘包/半包问题
            @Test
            public void serAndDesr3() throws IOException {
              MsgProtos.Msg message = buildMsg();
              //序列化到二进制码流
              ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                  message.writeDelimitedTo(outputStream);
                  ByteArrayInputStream inputStream
                        = new ByteArrayInputStream(outputStream.toByteArray());
              //从二进码流反序列化成Protobuf对象
              MsgProtos.MsginMsg = MsgProtos.Msg.parseDelimitedFrom(inputStream);
              Logger.info("id:=" + inMsg.getId());
              Logger.info("content:=" + inMsg.getContent());
            }
        }

这些方法通过调用 POJO 对象的 writeDelimitedTo(OutputStream) 方法在序列化字节码之前添加字节列表的宽度。 这和上面介绍的Head-Content合约类似,只不过Protobuf对其进行了优化。 宽度类型不是固定宽度的int类型,而是可变宽度的varint32类型。 反序列化时,调用parseDelimitedFrom(InputStream)方法。 Protobuf首先从输入流中读取varint32类型的宽度值,然后根据宽度值读取此消息的二补码字节,然后反序列化得到一个新的POJO实例。 这些方法可以用于异步操作的NIO应用场景,解决粘包/半包问题。

Protobuf编解码实践

Netty默认支持Protobuf编解码,外部安装了一套基本的Protobuf编解码。

1.ProtobufEncoder编码器

打开Netty源码,我们发现ProtobufEncoder的实现逻辑非常简单。 我们直接使用消息。 在下一站给编码员。

2.ProtobufDecoder解码器

ProtobufDecoder 解码器和 ProtobufEncoder 编码器是一一对应的。 ProtobufDecoder需要指定一个POJO消息的原型原型POJO实例,根据原型实例找到对应的Parser解析器,将两者的补码字节解析成ProtobufPOJO消息对象。

在JavaNIO通信中,仅使用上述一组编码器和解码器就会出现粘包/半包的问题。 Netty还提供了匹配的Head-Content类型的Protobuf编码器和解码器,在二进制补码码率之前添加了二进制补码字节列表的宽度。

3.ProtobufVarint32LengthFieldPrepender宽度编码器

这个编码器的作用是在ProtobufEncoder生成的字节列表之前放置一个varint32数字,表示序列化的二进制补码字节的数量。

4. ProtobufVarint32FrameDecoder宽度解码器

ProtobufVarint32FrameDecoder 和 ProtobufVarint32LengthFieldPrepender 相互对应。 它的作用是根据数据包中varint32中的宽度值解码出一个全字节链表。 然后将字节链表交给下一站解码器ProtobufDecoder。

varint32类型的宽度是多少,为什么不使用int的固定类型宽度?

varint32 是一种表示数字的紧凑方式,它不是一种具体的数据类型。 varint32 它使用一个或多个字节来表示一个数字。 值越小,使用的字节越少,值越大,使用的字节越多。 Varint32根据值的大小手动收缩宽度,这样可以减少用于保存宽度的字节数。 也就是说,varint32与int类型最大的区别在于:varint32使用一个或多个字节来表示一个数字。 varint32不是固定宽度,所以为了更好的减少通信过程中的传输量,报文头中的宽度尽量采用varint格式。

至此,Netty的外部ProtoBuf编码器和解码器已经初步介绍完毕。 通过这两套编码器/解码器即可完成Length+ProtobufData(Head-Content)合约的数据传输。 而且,在日益复杂的传输应用场景下,Netty的外部编码器和解码器已经不够用了。 例如,在Head部分添加幻数字段,用于安全验证; 或者还需要对ProtobufData等内容进行加密和解密。也就是说,在复杂的传输应用场景中,需要定制自己的Protobuf编码器和解码器。

javascript序列化json-JSON 和 ProtoBuf 序列化

Protobuf传输的服务器端实践

为了清楚地演示Protobuf传输,下面设计了一个简单的客户端/服务器传输程序:服务器接收客户端发来的数据包,并解码为Protobuf POJO; 客户端将Protobuf POJO编码成二补数据包,然后发送给服务器端。 在服务器端,Protobuf合约的解码过程如下:首先使用Netty外部的ProtobufVarint32FrameDecoder根据varint32格式的可变宽度值从入站数据包中解码出二补码的Protobuf字节码。 之后,您可以使用 Netty 外部的 ProtobufDecoder 解码器将字节码解码为 ProtobufPOJO 对象。 最后,自定义一个ProtobufBussinessDecoder解码器来处理ProtobufPOJO对象。

服务器端实际案例程序代码如下:

        package com.crazymakercircle.netty.protocol;
        //...
        public class ProtoBufServer {
            //...省略成员属性,构造器
            public void runServer() {
              //创建反应器线程组
              EventLoopGroupbossLoopGroup = new NioEventLoopGroup(1);
              EventLoopGroupworkerLoopGroup = new NioEventLoopGroup();
              try {
                  //...省略:启动器的反应器线程,设置配置项
                  //5 装配子通道流水线
                  b.childHandler(new ChannelInitializer<SocketChannel>() {
                      //有连接到达时会创建一个通道
                      protected void initChannel(SocketChannelch) throws Exception {
                          // 流水线管理子通道中的Handler业务处理器
                          // 向子通道流水线添加3个Handler业务处理器
                          ch.pipeline().addLast(newProtobufVarint32FrameDecoder());
                          ch.pipeline().addLast(
                            newProtobufDecoder(MsgProtos.Msg.getDefaultInstance()));
                          ch.pipeline().addLast(new ProtobufBussinessDecoder());
                      }
                  });
                  //....省略端口绑定,服务监听,从容关闭
            }
            //服务器端的业务处理器
            static class ProtobufBussinessDecoderextends ChannelInboundHandlerAdapter
            {
              @Override
              public void channelRead(ChannelHandlerContextctx, Object msg) throws
    Exception {
                  MsgProtos.MsgprotoMsg = (MsgProtos.Msg) msg;
                  //经过流水线的各个解码器,到此Person类型已经可以断定
                  Logger.info("收到一个MsgProtos.Msg数据包 =》");
                  Logger.info("protoMsg.getId():=" + protoMsg.getId());
                  Logger.info("protoMsg.getContent():=" + protoMsg.getContent());
              }
            }
            public static void main(String[] args) throws InterruptedException {
              int port = NettyDemoConfig.SOCKET_SERVER_PORT;
              new ProtoBufServer(port).runServer();
            }
        }

Protobuf传输的客户端实践

在客户端开始出站之前,需要提前构造Protobuf POJO对象。 然后就可以使用通道的write/writeAndFlush方法来启动出站处理的管道执行。 在客户端出站处理流程中,Protobuf合约的编码如图8-5所示,流程如下:

首先使用Netty外部的ProtobufEncoder将ProtobufPOJO对象编码成二进制补码字节链表; 然后使用Netty外部的ProtobufVarint32LengthFieldPrepender编码器,加上varint32格式的可变宽度。 Netty会将编码后的二补码字节码以Length+Content的格式发送到服务器。

客户端练习案例程序代码如下:

        package com.crazymakercircle.netty.protocol;
        //...
        public class ProtoBufSendClient {
            static String content = "疯狂创客圈:高性能学习社群!";
            //...省略成员属性,构造器
            public void runClient() {
              //创建反应器线程组
              EventLoopGroupworkerLoopGroup = new NioEventLoopGroup();
              try {
                  //1 设置反应器线程组
                  b.group(workerLoopGroup);
                  //2 设置nio类型的通道
                  b.channel(NioSocketChannel.class);
                  //3 设置监听端口
                  b.remoteAddress(serverIp, serverPort);
                  //4 设置通道的参数
                  b.option(ChannelOption.ALLOCATOR,
                          PooledByteBufAllocator.DEFAULT);
                  //5 装配通道流水线
                  b.handler(new ChannelInitializer<SocketChannel>() {
                    //初始化客户端通道
                    protected void initChannel(SocketChannelch) throws Exception {
                        // 客户端太多流水线添加2个Handler业务处理器
                        ch.pipeline().addLast(new
                                        ProtobufVarint32LengthFieldPrepender());
                        ch.pipeline().addLast(new ProtobufEncoder());
                    }
                  });
                  ChannelFuture f = b.connect();
                  //...
                  // 阻塞,直到连接完成
                  f.sync();
                  Channel channel = f.channel();
                  //发送Protobuf对象
                  for (int i = 0; i< 1000; i++) {
                    MsgProtos.Msg user = build(i, i + "->" + content);
                    channel.writeAndFlush(user);
                    Logger.info("发送报文数:" + i);
                  }
                  channel.flush();
                //省略关闭等待,从容关闭
          }
          //构建ProtoBuf对象
          public MsgProtos.Msgbuild(int id, String content) {
              MsgProtos.Msg.Builder builder = MsgProtos.Msg.newBuilder();
              builder.setId(id);
              builder.setContent(content);
              return builder.build();
          }
          public static void main(String[] args) throws InterruptedException {
              int port = NettyDemoConfig.SOCKET_SERVER_PORT;
              String ip = NettyDemoConfig.SOCKET_SERVER_IP;
              new ProtoBufSendClient(ip, port).runClient();
          }
        }

执行顺序为:先启动服务端,再启动客户端。 启动后,客户端将向服务器发送 1000 个构造好的 ProtobufPOJO 实例。 如果可以从服务器的控制台看到输出的POJO实例的属性值,则说明程序运行正确。

MsgProtos.Msg.Builderbuilder=MsgProtos.Msg.newBuilder();

生成器.setId(id);

builder.setContent(内容);

返回建设者。 建造();

      public static void main(String[] args) throws InterruptedException {
          int port = NettyDemoConfig.SOCKET_SERVER_PORT;
          String ip = NettyDemoConfig.SOCKET_SERVER_IP;
          new ProtoBufSendClient(ip, port).runClient();
      }
    }


执行次序是:先启动服务器端,然后启动客户端。启动后,客户端会向服务器发送构造好的1000个Protobuf POJO实例。如果能从服务器的控制台看到输出的POJO实例的属性值,说明程序运行是正确的。
**Protobuf消息字段的格式**为:限定修饰符① | 数据类型② | 字段名称③ | = | 分配标识号④

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 javascript javascript序列化json-JSON 和 ProtoBuf 序列化 https://www.wkzy.net/game/183652.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务