前言
上篇 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 方法实现自己的编解码业务。