Netty学习2 - 基于Netty + Protobuf 实现网络传输对象

89 阅读5分钟

前言

上一篇关于Netty的学习文章,简单实现基于Client-Server的在线聊天室demo。
此篇文章简单带领读者了解下Google Protocol Buffer协议+ Netty,并实现服务端-客户端通过网络传输对象的demo。

WebSocket聊天demo文章

跳转demo代码仓库点这

Protobuf简介

Protobuf 全称 Protocol Buffers(又名protobuf),是谷歌用于序列化(串行化)结构化数据的语言无关、平台无关的可扩展机制。翻译自ProtoBuf Github仓库

它是一种轻量的数据交换格式,相比XML和JSON,有更好的表现(更小体积、速度更快)。使用协议定义各种结构化数据的类型、编码方式,使用Protobuf提供的对应平台编译器生成对应的代码(JAVA、C、Python等)。由于使用二进制编码,因此在网络传输中表现更好。

只看上述文字应该还不明白个中道理,通过下文的实操和代码更容易了解它的作用。

Protobuf-生成Java代码

因为笔者之前捣鼓Nacos安装了Protobuf编译器,此处略过安装过程。
安装后运行命令protoc检查。

image.png

新建Student.proto文件,用于生成一个Student的java类。
Student字段包含:name、age、address

/**
  参考文档:https://protobuf.com.cn/programming-guides/proto2/
  https://protobuf.com.cn/programming-guides/proto3/
  语法:proto2、proto3
  2可在3中使用,3不可在2中使用
 */
syntax = "proto2";

// 生成包名
package org.nott.grpc.model;

// option:选项
// optimize_for(文件选项):可以设置为 SPEED(默认)、CODE_SIZE 或 LITE_RUNTIME
option optimize_for = SPEED;

// 可选项 生成java包名,当与package共存,优先级高于package
option java_package = "org.nott.grpc.model";

// 生成java类名
option java_outer_classname = "DataInfo";

/**
  message: 消息类型,Protobuf每个message对应生成一个java类
  protobuf支持的类型:double、float、int32、int64、bool、string 等等
 */

message Student{
  // optional:字段处于两种可能状态之一,已设置、未设置值
  optional string name = 1; // 1: 字段编号,您必须为消息定义中的每个字段指定 1 到 536,870,911 之间的一个数字,每个字段的编号唯一
  optional int32 age = 2;
  optional string address = 3;
}

使用下列命令,生成Java代码。正确运行的话会出java_outer_classname对应名称的Java

原来的message消息类型生成的Java类属于它的内部类。【不要修改生成的文件】

protoc --java_out=你需要生成的地址(后面会拼接proto文件中的java_package) proto文件所在地址

image.png

public static final class Student extends
    com.google.protobuf.GeneratedMessage implements
    // @@protoc_insertion_point(message_implements:org.nott.grpc.model.Student)
    StudentOrBuilder {
  ...

构建Netty

现在对象已经生成完毕,开始构建Netty的客户端-服务端。

创建Netty客户端的SocketChannel InitializerHandler和具体的客户端

public class ProtoBufClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pip = socketChannel.pipeline();
        // Netty 中处理Protobuf数据流的解码器,用Varint32编码规则
        pip.addLast(new ProtobufVarint32FrameDecoder());
        // 为Protobuf协议的消息头部加上32位长度的整形字段
        pip.addLast(new ProtobufVarint32LengthFieldPrepender());
        // Protobuf解码器,为DataInfo.Student对象解码
        pip.addLast(new ProtobufDecoder(DataInfo.Student.getDefaultInstance()));
        // Protobuf编码器
        pip.addLast(new ProtobufEncoder());
        // 自定义的Handler
        pip.addLast(new ProtobufClientHandler());
    }
}
// 客户端ChannelInboundHandlerAdapter
public class ProtobufClientHandler extends ChannelInboundHandlerAdapter {

    // 连接时发送一个DataInfo.Student信息
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        DataInfo.Student msg = DataInfo.Student.newBuilder().setName("客户端")
                .setAge(1).setAddress("从Localhost来").build();
        ctx.channel().writeAndFlush(msg);
    }
}
// 客户端
public class ProtoBufClient {

    public void run() throws Exception {
        NioEventLoopGroup boss = null;

        try {
            boss = new NioEventLoopGroup();

            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(boss)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ProtoBufClientInitializer())
                    .bind(9999)
                    .sync();
            // 连接服务端
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8888).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            boss.shutdownGracefully();

        }
    }

    public static void main(String[] args) throws Exception{
        new ProtoBufClient().run();
    }
}

创建Netty服务端的SocketChannel InitializerHandler和具体的服务端

//服务端ChannelInitializer
public class ProtobufServiceInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pip = socketChannel.pipeline();
        // 此处添加的编码解码器与客户端一致,除了最后的自定义处理器
        pip.addLast(new ProtobufVarint32FrameDecoder());
        pip.addLast(new ProtobufDecoder(DataInfo.Student.getDefaultInstance()));
        pip.addLast(new ProtobufVarint32LengthFieldPrepender());
        pip.addLast(new ProtobufEncoder());
        pip.addLast(new ProtobufSeverHandler());
    }
}
// 服务端SimpleChannelInboundHandler
// 只处理DataInfo.Student类型的信息
@Slf4j
public class ProtobufSeverHandler extends SimpleChannelInboundHandler<DataInfo.Student> {

    // 读取信息事件接口
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, DataInfo.Student student) throws Exception {
        log.info("接收到Student反序列化对象...");
        log.info(student.getName());
        log.info(student.getAddress());
        log.info(student.getAge() + "");
    }
}
// 服务端具体实现
public class ProtoBufServer {

    public void run() throws Exception{
        NioEventLoopGroup boss = null;
        NioEventLoopGroup worker = null;
        try {
            boss = new NioEventLoopGroup();
            worker = new NioEventLoopGroup();
            ServerBootstrap bootstrap = new ServerBootstrap();
            ChannelFuture future = bootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childOption(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
                    .childHandler(new ProtobufServiceInitializer())
                    // 运行在8888端口
                    .bind(8888)
                    .sync();
            future.channel().closeFuture().sync();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception{
        new ProtoBufServer().run();
    }
}

运行

我们在上文构建好了Netty的客户端-服务端,设置了基于Protobuf协议的编码解码器,按预想情况在服务端运行时,客户端马上运行并且正常链接服务端时,会发送一条DataInfo.Student的信息,并且服务端马上接收且正常解码,日志打印Student对象的各个字段内容。

在服务端运行之后运行客户端

image.png

客户端正常连接,同时服务端Handler读取并打印Student对象信息内容

image.png

小结

上文简单使用了Protobuf定义的数据格式生成了Java代码,可以在Netty框架使用对应协议的处理器,实现基于网络的客户端与服务端对象传输。

由于Protobuf的无关语言,实际上编写.proto文件,即可生成不同语言的代码,且由客户端和服务端不需要使用同一个语言都可正常序列化。

加上其无关平台,基于协议的特性。无需局限于某个特定框架,可用于多处平台。

篇幅有限,加上笔者水平还有提升空间,无法在此深入展开。
此篇文章已收录进入本人的学习专栏,其他内容可此同步浏览,感兴趣的朋友可以点下关注,赏给作者一个赞,谢谢。