文章目录
Netty编解码开发
编解码技术
Java序列化的问题:
- 无法跨语言
- 序列化后码流太大
- 序列化性能太低
如何评判一个编解码框架的优劣
- 是否支持夸语言,支持的语言种类是否丰富
- 编码后的码流大小
- 编解码的性能
- 类库是否小巧,API使用是否方便
- 使用者需要手工开发的工作量和难度
MessagePack编解码
MessagePack是一个高效的二进制序列化框架,像JSON一样支持不同语言间的数据交换,但是它的性能更高,序列化之后的码流也更小。
特点如下:
- 编解码高效,性能高
- 序列化之后的码流小
- 支持跨语言
MessagePack官网 https://msgpack.org/
MessagePack简单使用
使用maven引入MessagePack
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack</artifactId>
<version>0.6.12</version>
</dependency>
简单使用(来自MessagePack官网)
package com.wy.messagepack;
import org.msgpack.MessagePack;
import org.msgpack.template.Templates;
import org.msgpack.type.Value;
import org.msgpack.unpacker.Converter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName MessagePackMode1
* @Description TODO
* @Author Wang Yue
* @Date 2021/2/16 16:41
*/
public class MessagePackMode1 {
public static void main(String[] args) throws IOException {
List<String> src = new ArrayList<>();
src.add("msgpack");
src.add("kunofs");
src.add("viver");
MessagePack messagePack = new MessagePack();
//序列化
byte[] raw = messagePack.write(src);
//使用模板进行反序列化
List<String> dst1 = messagePack.read(raw, Templates.tList(Templates.TString));
System.out.println(dst1.get(0));
System.out.println(dst1.get(1));
System.out.println(dst1.get(2));
//反序列化为值,然后进行类型转换
Value dynamic = messagePack.read(raw);
List<String> dst2 = new Converter(dynamic).read(Templates.tList(Templates.TString));
System.out.println(dst2.get(0));
System.out.println(dst2.get(1));
System.out.println(dst2.get(2));
}
}
在Netty中使用MessagePack作为编解码器
编码器开发
package com.wy.netty.messagepack;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.msgpack.MessagePack;
/**
* @ClassName MsgPackEncoder
* @Description TODO
* @Author Wang Yue
* @Date 2021/2/16 17:09
*/
public class MsgPackEncoder extends MessageToByteEncoder<Object> {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
MessagePack messagePack = new MessagePack();
byte[] raw = messagePack.write(msg);
out.writeBytes(raw);
}
}
解码器开发
package com.wy.netty.messagepack;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import org.msgpack.MessagePack;
import org.msgpack.type.Value;
import java.util.List;
/**
* @ClassName MsgpackDecoder
* @Description TODO
* @Author Wang Yue
* @Date 2021/2/16 17:13
*/
public class MsgpackDecoder extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
final byte[] array;
final int length = msg.readableBytes();
array = new byte[length];
msg.getBytes(msg.readerIndex(), array, 0, length);
MessagePack messagePack = new MessagePack();
out.add(messagePack.read(array));
}
}
使用MessagePack作为编解码器开发Netty服务器与客户端
服务器
package com.wy.netty.messagepack;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
/**
* @ClassName NettyServer
* @Description TODO
* @Author Wang Yue
* @Date 2021/2/15 18:02
*/
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
//两个group的
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
//使用Messagepack进行编解码
ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));
ch.pipeline().addLast("msgpack decoder", new MsgpackDecoder());
ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));
ch.pipeline().addLast("msgpack encoder", new MsgPackEncoder());
ch.pipeline().addLast(new NettyServerHandler());
}
});
System.out.println("...服务器 is ready...");
//启动服务器
ChannelFuture channelFuture = serverBootstrap.bind(6668).sync();
//对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.wy.netty.messagepack;
import com.wy.netty.pojo.User;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @ClassName NettyServerHandler
* @Description TODO
* @Author Wang Yue
* @Date 2021/2/15 18:19
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
private int count = 1;
private User user = new User("李四", 20);
//读取数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("客户端发送消息是: " + msg + " 本次是第:" + count++ + "次");
ctx.writeAndFlush(user);
}
//处理异常,一般需要关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
客户端
package com.wy.netty.messagepack;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
/**
* @ClassName NettyClient
* @Description TODO
* @Author Wang Yue
* @Date 2021/2/15 18:41
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//客户端需要一个事件循环即可
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));
ch.pipeline().addLast("msgpack decoder", new MsgpackDecoder());
ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));
ch.pipeline().addLast("msgpack encoder", new MsgPackEncoder());
//加入自己的处理器
ch.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("...客户端 is ok...");
//启动客户端连接服务器
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
package com.wy.netty.messagepack;
import com.wy.netty.pojo.User;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @ClassName NettyClientHandler
* @Description TODO
* @Author Wang Yue
* @Date 2021/2/15 18:50
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
private int count = 1;
private User user = new User("张三", 19);
//当通道就绪时,就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 1000; i++) {
ctx.writeAndFlush(user);
}
}
//当通道有读取事件时,会触发此方法
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器回复消息: " + msg + " 本次是第:" + count++ + "次");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
粘包/半包支持
上述代码示例中使用 LengthFieldBasedFrameDecoder
和 LengthFieldPrepender
实现对粘包和半包的支持。
这种方式就是解决策略中的第三种,即,在消息头中新增报文长度字段,利用该字段进行半包的编解码。
关于LengthFieldBasedFrameDecoder
的解析如下,参考https://www.cnblogs.com/crazymakercircle/p/10294745.html
自定义长度解码器LengthFieldBasedFrameDecoder构造器,涉及5个参数,都与长度域(数据包中的长度字段)相关,具体介绍如下:
(1) maxFrameLength - 发送的数据包最大长度;
(2) lengthFieldOffset - 长度域偏移量,指的是长度域位于整个数据包字节数组中的下标;
(3) lengthFieldLength - 长度域的自己的字节数长度。
(4) lengthAdjustment – 长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。
(5) initialBytesToStrip – 丢弃的起始字节数。丢弃处于有效数据前面的字节数量。比如前面有4个节点的长度域,则它的值为4。
LengthFieldBasedFrameDecoder spliter=new LengthFieldBasedFrameDecoder(1024,0,4,0,4);
第一个参数为1024,表示数据包的最大长度为1024;第二个参数0,表示长度域的偏移量为0,也就是长度域放在了最前面,处于包的起始位置;第三个参数为4,表示长度域占用4个字节;第四个参数为0,表示长度域保存的值,仅仅为有效数据长度,不包含其他域(如长度域)的长度;第五个参数为4,表示最终的取到的目标数据包,抛弃最前面的4个字节数据,长度域的值被抛弃。
关于LengthFieldPrepender
的解析,参考https://www.cnblogs.com/pc-boke/articles/9293226.html
如果协议中的第一个字段为长度字段,netty提供了LengthFieldPrepender编码器,它可以计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中,如图所示:
通过LengthFieldPrepender可以将待发送消息的长度写入到ByteBuf的前2个字节,编码后的消息组成为长度字段+原消息的方式。
通过设置LengthFieldPrepender为true,消息长度将包含长度字段占用的字节数,打开LengthFieldPrepender后,图3-3示例中的编码结果如下图所示: