在看此篇内容时需要浏览下面内容
从零开始学netty——如何面对粘包和拆包
从零开始学netty——自定义协议
netty和protobuf其实没啥关系。protobuf主要是做对象序列化的,他是一个高效的对象转字节,字节转对象的协议,并且支持多种语言。查他的好处,网上能查一大堆。以前说过自定义协议,传输的byte不总能是string的吧,传输对象肯定更方便编程啊,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
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()就是说明了这个问题,解码出来的对象是什么类型的。
刚才的例子大家也明白了,已经有很多写好的编码器或者解码器了,并且还处理了粘包和拆包的问题。是不是现在可以用一个对象来表示协议呢。
我的回答是否定的,虽然这样开发比较省事,也不用自己管理粘包和拆包,但是会造成编码逻辑的问题。
- 数据内容和协议内容混搭。你的对象里既包含协议,又包含内容。不符合面向对象的单一职责设计,正常的自定义协议基本包含这样的信息,这个操作是干什么的,还有这个操作的数据是什么,起码是两部分的,是要做到操作和数据分开的。数据传输数据,内容传输内容,虽然可以写在一个解码器里,在确定调用模块后,只传递数据走就好。
- 魔数校验。有的协议是需要有魔数的,例如java的class文件都是0xCAFEBABE开头,用做校验,如果读取不通过会略过一些字节直到找到魔数。
- 可移植性差。protobuf不是唯一的选择,当出现更好的协议想替换的时候就很麻烦,如果单单是使用他的序列化和反序列化的功能替换很简单。只把序列化的地方改改就好。