从零开始学netty——初见protobuf

在看此篇内容时需要浏览下面内容
从零开始学netty——如何面对粘包和拆包
从零开始学netty——自定义协议


netty和protobuf的关系

netty和protobuf其实没啥关系。protobuf主要是做对象序列化的,他是一个高效的对象转字节,字节转对象的协议,并且支持多种语言。查他的好处,网上能查一大堆。以前说过自定义协议,传输的byte不总能是string的吧,传输对象肯定更方便编程啊,protobuf就提供了这样的方便功能。


protobuf简易使用教程

首先是写一个.proto的文件内容如下。

syntax = "proto3";#声明版本,3支持map,2不支持
message CommonMessage{
#message对应的是类
     int64 id =1;
     repeated  string list=2 ;//list
}

message MapMessage{
     map<string, CommonMessage> mapInfo=1;#map

}
option java_package = "com.xp";
option java_multiple_files = true;#生成的类是多个的。

上面的例子基本把所有的情况都列举了,使用list用repeated标识类型,使用map用map关键字。如果引用的也是protobuf生成的类就直接可以写那个类的message的名字。

其他常见基本类型参考https://developers.google.com/protocol-buffers/docs/proto3,里面有每种类型对应的java,c++等类型。

option java_multiple_files 我建议使用这个参数,否则他会把对象生成在一个类里,然后用具体的类都是类名.类名的用法。

使用命令生成对象

protoc –java_out=./src info.proto


使用protobuf开发程序

public void connect() throws InterruptedException{
        EventLoopGroup worker =new NioEventLoopGroup();
        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(worker).channel(NioSocketChannel.class).handler(new ChannelInitializer<Channel>() {

                @Override
                protected void initChannel(Channel channel) throws Exception {
                    channel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
                    channel.pipeline().addLast(new ProtobufEncoder());

                }
            });

            ChannelFuture connect = bootstrap.connect("127.0.0.1", 1900);
            CommonMessage.Builder commonMessageBuilder= CommonMessage.newBuilder();
            commonMessageBuilder.addList("he");
            commonMessageBuilder.addList("he");
            commonMessageBuilder.setId(66);
            MapMessage.Builder mapMessageBuilder = MapMessage.newBuilder();
            mapMessageBuilder.putMapInfo("xx", commonMessageBuilder.build());
            MapMessage build = mapMessageBuilder.build();
            connect.channel().writeAndFlush(build);
            System.out.println("over");
            connect.sync();
        }finally{
            worker.shutdownGracefully();
        }

protobuf都是先建立builder对象,设置完以后,build一下产生message对象。剩下的操作就是普通的java对象。

都使用protobuf生成对象了,序列化和反序列化,都是依靠他来做了,这里就需要ProtobufEncoder。

大家应该还记得粘包和拆包操作吧。光有byte是不够的,服务端不知道读取多少,ProtobufVarint32LengthFieldPrepender会加一个32位的标识(int)表示数据包的长度。

public void bind(int port) throws InterruptedException {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(boss, worker).channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<Channel>() {

                    @Override
                    protected void initChannel(Channel channel) throws Exception {
                        channel.pipeline().addLast(new ProtobufVarint32FrameDecoder());
                        channel.pipeline().addLast(new ProtobufDecoder(MapMessage.getDefaultInstance()));
                        channel.pipeline().addLast(new SimpleChannelInboundHandler<MapMessage>() {

                            @Override
                            protected void channelRead0(ChannelHandlerContext arg0, MapMessage arg1) throws Exception {
                                System.out.println(arg1.getMapInfoMap().get("xx"));

                            }
                            @Override
                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                System.out.println("connection");
                                super.channelActive(ctx);
                            }
                            @Override
                            public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                                System.out.println("close");
                                super.channelInactive(ctx);
                            }

                        });

                    }
                });

        ChannelFuture sync = bootstrap.bind(port).sync();
        sync.channel().closeFuture().sync();

服务器端这里也需要有对应的解码器,ProtobufVarint32FrameDecoder就是对应ProtobufVarint32LengthFieldPrepender的解码器。ProtobufDecoder需要说明解出来的对象是什么,MapMessage.getDefaultInstance()就是说明了这个问题,解码出来的对象是什么类型的。


自定义协议的好处

刚才的例子大家也明白了,已经有很多写好的编码器或者解码器了,并且还处理了粘包和拆包的问题。是不是现在可以用一个对象来表示协议呢。
我的回答是否定的,虽然这样开发比较省事,也不用自己管理粘包和拆包,但是会造成编码逻辑的问题。

  1. 数据内容和协议内容混搭。你的对象里既包含协议,又包含内容。不符合面向对象的单一职责设计,正常的自定义协议基本包含这样的信息,这个操作是干什么的,还有这个操作的数据是什么,起码是两部分的,是要做到操作和数据分开的。数据传输数据,内容传输内容,虽然可以写在一个解码器里,在确定调用模块后,只传递数据走就好。
  2. 魔数校验。有的协议是需要有魔数的,例如java的class文件都是0xCAFEBABE开头,用做校验,如果读取不通过会略过一些字节直到找到魔数。
  3. 可移植性差。protobuf不是唯一的选择,当出现更好的协议想替换的时候就很麻烦,如果单单是使用他的序列化和反序列化的功能替换很简单。只把序列化的地方改改就好。
点赞