Netty-搭建一个简单的Protobuf协议消息接收发送服务

前言

上篇 Netty-搭建一个简单的Json协议消息接收发送服务 文章简单快速的搭建了一个Json协议的Netty服务,并解决了粘包半包问题,这篇我们用Protobuf协议搭建Netty服务。


一、Protobuf是什么?

GitHub地址:https://github.com/google/protobuf

Protobuf是google开源的项目,全称 Google Protocol Buffers,特点如下:

  • 支持跨平台多语言,支持目前绝大多数语言例如C++、C#、Java、pthyon等
  • 高性能,可靠性高,google出品有保障
  • 使用protobuf编译器能自动生成代码,但需要编写proto文件,需要一点学习成本

二、Protobuf使用

1.下载protoc.exe编译器

Protobuf是将类的定义使用.proto文件进行描述,然后通过protoc.exe编译器,根据.proto自动生成.java文件,然后将生成的.java文件拷贝到项目中使用即可。在Github主页我们下载Windows下的编译器,可以在releases页面下载:https://github.com/google/protobuf/releases

下载完成之后放到磁盘上进行解压,可以将protoc.exe配置到环境变量中去,这样就可以直接在cmd命令行中使用protoc命令,也可以不用配置,直接到解压后的protoc\bin目录下进行文件的编译。

2.编译Proto文件

 下面我们基于上篇的JsonMsgDto对象来构建一个Message.proto文件。

syntax = "proto3";
//定义protobuf的包名称空间
package com.yimint.netty.common.protocol;
// [结束声明]

// [开始 java 选项配置]
option java_package = "protocol";
option java_outer_classname = "MessageProto";
// [结束 java 选项配置]

// [开始 消息定义]
message Message {
  string id = 1;
  string content = 2;
}
// [结束 消息定义]

syntax 声明可以选择protobuf的编译器版本(v2和v3)

  • syntax=”proto2”;选择2版本
  • syntax=”proto3”;选择3版本

option java_outer_classname=”MessageProto”用来指定生成的java类的类名。
message相当于Java语言中的class语句,表示定义一个信息,其实也就是类。
message里面的信息就是我们要传输的字段了,子段后面需要有一个数字编号,从1开始递增

.proto文件定好之后就可以用编译器进行编译,输出我们要使用的Java类,我们这边不配置环境变量,直接到解压包的bin目录下进行操作

首先将我们的Message.proto文件复制到bin目录下,然后在这个目录下打开CMD窗口,输入下面的命令进行编译操作:

protoc ./Message.proto --java_out=./

 - -java_out是输出目录,我们就输出到当前目录下,执行完之后可以看到bin目录下多了一个MessageProto.java文件,把这个文件复制到项目中使用即可。

三、Nettty整合Protobuf

首先加入Protobuf的Maven依赖

        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.20.1</version>
        </dependency>

 1、Protobuf服务端

public class ProtoBufServer {
    private final static Logger LOGGER = LoggerFactory.getLogger(ProtoBufServer.class);
    public static final String SERVER_IP = "127.0.0.1";
    public static final int SERVER_PORT = 54123;

    public static void main(String[] args) {
        ServerBootstrap b = new ServerBootstrap();
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            b.group(bossGroup, workGroup);
            b.channel(NioServerSocketChannel.class);
            b.localAddress(SERVER_PORT);
            b.option(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);
            b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    // pipeline管理子通道channel中的Handler
                    // 向子channel流水线添加3个handler处理器

                    // protobufDecoder仅仅负责编码,并不支持读半包,所以在之前,一定要有读半包的处理器。
                    // 有三种方式可以选择:
                    // 使用netty提供ProtobufVarint32FrameDecoder
                    // 继承netty提供的通用半包处理器 LengthFieldBasedFrameDecoder
                    // 继承ByteToMessageDecoder类,自己处理半包

                    // 半包的处理
                    socketChannel.pipeline().addLast(new ProtobufVarint32FrameDecoder());
                    // 需要解码的目标类
                    socketChannel.pipeline().addLast(new ProtobufDecoder(MessageProto.Message.getDefaultInstance()));
                    socketChannel.pipeline().addLast(new ProtoBufHandler());
                }
            });
            ChannelFuture channelFuture = b.bind();
            channelFuture.addListener((future) -> {
                if (future.isSuccess()) {
                    LOGGER.info(" ========》反应器线程 回调 Protobuf服务器启动成功,监听端口: " +
                            channelFuture.channel().localAddress());

                }
            });
            channelFuture.sync();
            LOGGER.info(" 调用线程执行的,Protobuf服务器启动成功,监听端口: " +
                    channelFuture.channel().localAddress());

            ChannelFuture closeFuture = channelFuture.channel().closeFuture();
            closeFuture.sync();
        } catch (Exception ex) {
            ExceptionUtil.stacktraceToString(ex);
        } finally {
            workGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
public class ProtoBufHandler extends ChannelInboundHandlerAdapter {
    private final static Logger LOGGER = LoggerFactory.getLogger(ProtoBufHandler.class);
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        MessageProto.Message protoMsg = (MessageProto.Message) msg;
        
        LOGGER.info("收到一个 MsgProtos.Msg 数据包 =》");
        LOGGER.info("protoMsg.getId():=" + protoMsg.getId());
        LOGGER.info("protoMsg.getContent():=" + protoMsg.getContent());
    }
}

 2、Protobuf客户端

public class ProtoBufClient {
    private final static Logger LOGGER = LoggerFactory.getLogger(ProtoBufClient.class);
    public static final String SERVER_IP = "127.0.0.1";
    public static final int SERVER_PORT = 54123;

    public static void main(String[] args) {
        Bootstrap b = new Bootstrap();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            b.group(workGroup);
            b.channel(NioSocketChannel.class);
            b.remoteAddress(SERVER_IP, SERVER_PORT);
            b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
            b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,10000);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    // 客户端channel流水线添加2个handler处理器
                    /**
                     * ProtobufVarint32LengthFieldPrepender 是 Netty 提供的编码器。它的作用是在消息的头部添加一个表示消息长度的字段,以便接收方可以正确解析出消息的长度。
                     * 通常搭配 ProtobufVarint32FrameDecoder 一起使用,发送方会在消息前添加一个 Varint32 类型的字段,表示消息的长度,接收方会根据该长度字段来拆分消息,保证接收到完整的消息数据
                     */
                    socketChannel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
                    socketChannel.pipeline().addLast(new ProtobufEncoder());
                }
            });
            ChannelFuture channelFuture = b.connect();
            channelFuture.addListener((ChannelFuture futureListener) -> {
                if (futureListener.isSuccess()) {
                    LOGGER.info("ProtoBufClient客户端连接成功!");
                } else {
                    LOGGER.info("ProtoBufClient客户端连接失败!");
                }
            });
            channelFuture.sync();
            Channel channel = channelFuture.channel();
            Scanner scanner = new Scanner(System.in);
            LOGGER.info("请输入发送内容:");
            // 发送回调监听
            GenericFutureListener sendCallBack = future -> {
                if (future.isSuccess()) {
                    LOGGER.info("发送成功!");
                } else {
                    LOGGER.info("发送失败!");
                }
            };

            while (scanner.hasNext()) {
                //获取输入的内容
                String next = scanner.next();
                MessageProto.Message message = MessageProto.Message.newBuilder().setId(RandomUtil.randomNumbers(2)).setContent(next).build();
                ChannelFuture writeAndFlushFuture = channel.writeAndFlush(message);
                writeAndFlushFuture.addListener(sendCallBack);
                LOGGER.info("请输入发送内容:");
            }
        } catch (Exception ex) {
            LOGGER.error(ExceptionUtil.stacktraceToString(ex));
        }finally {
            workGroup.shutdownGracefully();
        }
    }
}

四、测试 

 

 


总结

以上一个简单的Protobuf协议的Netty服务就搭建完成了;次案例中使用了 Netty提供的编码器  ProtobufVarint32FrameDecoder 和 ProtobufVarint32LengthFieldPrepender 解决粘包和半包问题,如果需要自定义Protobuf协议的话,比如:在消息中增加魔术及版本号字段;那么可以继承 ByteToMessageDecoder 和 MessageToByteEncoder 重写 decode 和 encode 方法实现自己的编解码业务。

项目地址:https://github.com/YIminta/Netty-learning

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值