何为拆包和粘包
在我们计算机使用网络的过程中,计算机可能同时存在多个网络程序,网路上又有无数的网络应用在占用带宽,这就导致了我们的消息在底层的消息发送中,它很有可能是不完整的,在解释拆包和粘包之前,我们先来跑一段程序,代码的只要逻辑是客户端连续给服务端发送100条消息,看看服务端每次接收的结果,主要代码如下:
客户端:
bootstrap.group(nioEventLoopGroup)
// .option(ChannelOption.SO_BACKLOG, 1024)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("收到服务器回复 [{}]", msg);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i= 0; i < 100; i++) {
String lineStr = "你好欢迎来netty到的世界!";
log.info("发送消息长度: {}", lineStr.getBytes().length);
ctx.channel().writeAndFlush(lineStr);
}
}
});
ch.pipeline().addLast(new StringEncoder());
}
}).connect("127.0.0.1", 8888);
服务端
serverBootstrap.group(bossGroup, workerGroup)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
log.info("收到客户端消息:[{}]", msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
channelFuture.channel().closeFuture().sync();
打印结果:
...
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!你好欢迎来netty到的世]
收到客户端消息:[界!你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!]
收到客户端消息:[你好欢迎来netty到的世界!你好欢迎来netty到的世]
收到客户端消息:[界!]
...
粘包和半包产生的原因
粘包: 多个数据包被放在一起发送,好像被"粘"在了一起
半包: 正常的数据包被拆开了,只有完整包的部分数据
拆包: 正常的数据包被拆成了半包,或者需要把粘包拆成正常数据包的过程
虽然我们在应用中是一个一个包是发送的,但是计算机底层只认tcp协议,在正常情况下我们发送数据包,应该是一个包接一个包的发送,但底层的TCP协议还是按照字节流发送数据包,数据包进入底层缓冲池中,tcp定时发送,假如缓冲池满了,底层会马上发送数据包,一旦出现这种情况下,数据包只能被拆开,或者被拆开的部分跟之前的正常数据包"粘"在一起发送,那么服务端就接收到被"粘"在一起的数据包。如果我们要数据正常使用,服务端就需要把接收到的粘包拆开,得到正常的数据包以及多余的半包,半包需要等待下一次发送过来的半包,根据协议拼接回原来正常的数据包。
处理粘包和半包的方案
- 客户端发送固定长度的包,服务端解析获取到固定长度时再根据协议解析数据,流程如下
发送的数据不是一次性发送约定的长度,可能是一次发送大于约定长度,也可能分为几次发送才发送到约定长度,像下图这样
-
根据特定的分隔符来解析数据,也即读取到某个分隔符时确定该数据包已经读取完整,思路如1
-
把数据包分为消息头和消息体进行读取,消息头固定长度,里面有一个字段记录了接下来的消息体的长度,然后再读取到固定的消息体就为一条完整的消息
netty内置的拆包方案
1 固定长度的拆包器 FixedLengthFrameDecoder
这个拆包器对应我们讲的第一种拆包方案,都是读取到固定长度的数据时才开始解析,如果对应的需求的消息协议非常简单,可以使用这个拆包器实现
客户端
@Slf4j
public class NettyClientStr {
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
try {
bootstrap.group(nioEventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("收到服务器回复 [{}]", msg);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i= 0; i < 1000; i++) {
String fixedLengthStr = "你好,欢迎来到netty的世界!";
ctx.channel().writeAndFlush(fixedLengthStr);
}
}
});
ch.pipeline().addLast(new StringEncoder());
}
}).connect("127.0.0.1", 8888).addListener(
future -> {
if (future.isSuccess()) {
log.info("链接服务器成功");
} else {
log.info("链接服务器失败");
System.exit(0);
}
}
).channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
nioEventLoopGroup.shutdownGracefully();
}
}
}
服务端
@Slf4j
public class NettyServerFixedLengthFrameDecoder {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
serverBootstrap.group(bossGroup, workerGroup)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new FixedLengthFrameDecoder(35));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("收到客户端消息:[{}]", msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 优雅关闭
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
客户端在启动时,向服务端重复发送1000条一样的消息,服务端收到并且解析,可以看到,这1000条消息都能被服务端正确的打印,服务端已经能够正常的解析到数据了,通过读取固定长度的消息包解决了粘包或者半包的问题
2 分隔符拆包器 DelimiterBasedFrameDecoder
对应上面第二种方式解码,netty内置了分隔符拆包器,可以自定义分隔符,当服务端接收到约定的分隔符时,就认为一条消息已经读取完毕了,对应的代码如下
客户端
@Slf4j
public class NettyClientStr {
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
try {
bootstrap.group(nioEventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("收到服务器回复 [{}]", msg);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i= 0; i < 1000; i++) {
String delimiterStr = "你好,欢迎来到[demo]netty的世界![demo]";
ctx.channel().writeAndFlush(delimiterStr);
}
}
});
ch.pipeline().addLast(new StringEncoder());
}
}).connect("127.0.0.1", 8888).addListener(
future -> {
if (future.isSuccess()) {
log.info("链接服务器成功");
} else {
log.info("链接服务器失败");
System.exit(0);
}
}
).channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
nioEventLoopGroup.shutdownGracefully();
}
}
}
服务端
@Slf4j
public class NettyServerDelimiterBasedFrameDecoder {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
serverBootstrap.group(bossGroup, workerGroup)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, Unpooled.copiedBuffer("[demo]".getBytes())));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("收到客户端消息:[{}]", msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 优雅关闭
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
根据分隔符"[demo]"进行分隔消息,服务端也都解析到了这1000次发送的消息
3 行拆包器 LineBasedFrameDecoder
行拆包器也是一种分隔符拆包器,只不过它约定了读取到换行符时就认为这是一条消息的结束
客户端
@Slf4j
public class NettyClientStr {
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
try {
bootstrap.group(nioEventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("收到服务器回复 [{}]", msg);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i= 0; i < 1000; i++) {
String lineStr = "你\n好\n欢\n迎\n来\n到\n的\n世\n界!";
ctx.channel().writeAndFlush(lineStr);
}
}
});
ch.pipeline().addLast(new StringEncoder());
}
}).connect("127.0.0.1", 8888).addListener(
future -> {
if (future.isSuccess()) {
log.info("链接服务器成功");
} else {
log.info("链接服务器失败");
System.exit(0);
}
}
).channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
nioEventLoopGroup.shutdownGracefully();
}
}
}
服务端
@Slf4j
public class NettyServerLineBasedFrameDecoder {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
serverBootstrap.group(bossGroup, workerGroup)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("收到客户端消息:[{}]", msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 优雅关闭
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
4 基于长度域拆包器 LengthFieldBasedFrameDecoder
这个拆包器是使用得最多的一个消息处理器,它能够处理各种通用的消息协议,下面来看看该类的构造函数以及每个构造函数的参数
public LengthFieldBasedFrameDecoder(
ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
...
}
我们自定义消息协议的编解码,基本上都是按照以上几个参数进行的
- maxFrameLength - 解析的最大帧长度,也即接收到客户端消息时,消息头和消息体长度所允许的最大长度;
- lengthFieldOffset - 长度域所在数据包字节数组中的下标,也就是说表示消息体长度的字段起始位置所在下下标;
- lengthFieldLength - 表示长度域的长度,也就是说长度域在消息头中所占的字节数;
- lengthAdjustment - 长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。它们之间的算法为:byteLength(发送的数据包长度) = lengthFieldLengthValue(长度域的值) + lengthFieldOffset(长度字段偏移量) + lengthFieldLength(长度域的长度) + lengthAdjustment
- initialBytesToStrip - 接收到的数据包,去除前initialBytesToStrip位
- failFast - true: 读取到长度域超过maxFrameLength,就抛出一个 TooLongFrameException。false: 只有真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出;
- byteOrder - 数据存储采用大端模式还是小端模式,默认采用大端模式,没有特殊的需求,建议不要修改。
下面举例说明每个参数的用法
客户端发送的内容为 “你好,欢迎来到netty的世界”
场景一
数据包大小: 35Byte = 2Byte(一个short大小的数据长度域) + 33Byte(数据包长度)
由看图可以看出,解码后,数据格式以及长度没有发生变化,那么,LengthFieldBasedFrameDecoder 的参数值为
maxFrameLength: 大于该数据包长度的任意值,这里给了Integer.MAX_VALUE的长度
lengthFieldOffset: 长度域的起始位置就在消息的头部,offset=0
lengthFieldLength: 约定了为一个short类型的长度, lengthFieldLength=2(byte)
lengthAdjustment: byteLength - lengthFieldLengthValue - lengthFieldOffset - lengthFieldLength = 35 - 33 - 0 - 2 = 0
initialBytesToStrip: 由于解码前后没有改变数据包长度,此处为0
场景二
解码后去除长度域,只保留消息体(33Byte)
由看图可以看出,解码后,只保留了具体的消息体数据,那么,LengthFieldBasedFrameDecoder 的参数值为
maxFrameLength: 大于该数据包长度的任意值,这里给了Integer.MAX_VALUE的长度
lengthFieldOffset: 长度域的起始位置就在消息的头部,offset=0
lengthFieldLength: 约定了为一个short类型的长度, lengthFieldLength=2(byte)
lengthAdjustment: byteLength - lengthFieldLengthValue - lengthFieldOffset - lengthFieldLength = 35 - 33 - 0 - 2 = 0
initialBytesToStrip: 由于解码后丢失了2个byte的长度,所以此处为2
场景三
解码前后数据包不变,但是长度域中多存了两个字节
由看图可以看出,解码后,保留了完整数据包,但是长度域中被添加了一个多余的长度,那么,LengthFieldBasedFrameDecoder 的参数值为
maxFrameLength: 大于该数据包长度的任意值,这里给了Integer.MAX_VALUE的长度
lengthFieldOffset: 长度域的起始位置就在消息的头部,offset=0
lengthFieldLength: 约定了为一个short类型的长度, lengthFieldLength=2(byte)
lengthAdjustment: byteLength - lengthFieldLengthValue - lengthFieldOffset - lengthFieldLength = 35 - 35 - 0 - 2 = -2
initialBytesToStrip: 由于解码前后没有改变数据包长度,此处为0
场景四
解码前后数据包大小不变,数据包前面是一个固定大小的消息头
由看图可以看出,增加了一个没有使用到的消息头,解码后,保留了完整数据包,那么,LengthFieldBasedFrameDecoder 的参数值为
maxFrameLength: 大于该数据包长度的任意值,这里给了Integer.MAX_VALUE的长度
lengthFieldOffset: 长度域的起始位置就在消息的头部,offset=2
lengthFieldLength: 约定了为一个int类型的长度, lengthFieldLength=4(byte)
lengthAdjustment: byteLength - lengthFieldLengthValue - lengthFieldOffset - lengthFieldLength = 39 - 33 - 2 - 4 = 0
initialBytesToStrip: 由于解码前后没有改变数据包长度,此处为0
场景五
解码前后数据包大小不变,但是消息头在长度域和消息体中间
由看图可以看出,消息头在长度域和消息体之间,消息头占2个byte的长度,解码后,保留了完整数据包,那么,LengthFieldBasedFrameDecoder 的参数值为
maxFrameLength: 大于该数据包长度的任意值,这里给了Integer.MAX_VALUE的长度
lengthFieldOffset: 长度域的起始位置就在消息的头部,offset=0
lengthFieldLength: 约定了为一个int类型的长度, lengthFieldLength=4(byte)
lengthAdjustment: byteLength - lengthFieldLengthValue - lengthFieldOffset - lengthFieldLength = 39 - 33 - 0 - 4 = 2
initialBytesToStrip: 由于解码前后没有改变数据包长度,此处为0
场景六
数据包前面是消息头1,中间是长度域,再后面是消息头2,接下来才是消息体,解码后只保留消息头2和消息体
由看图可以看出,解码后,去除了消息头1和长度域,那么,LengthFieldBasedFrameDecoder 的参数值为
maxFrameLength: 大于该数据包长度的任意值,这里给了Integer.MAX_VALUE的长度
lengthFieldOffset: 长度域的起始位置就在消息的头部,offset=1
lengthFieldLength: 约定了为一个int类型的长度, lengthFieldLength=2(byte)
lengthAdjustment: byteLength - lengthFieldLengthValue - lengthFieldOffset - lengthFieldLength = 37 - 33 - 1 - 2 = 1
initialBytesToStrip: 由于解码后去除了消息头1(1byte)和长度域(2byte),此处为3
场景七
数据包结构跟场景六是一样的,唯一的区别是此处的长度域的值是整个数据包的长度
由看图可以看出,解码后,去除了消息头1和长度域,那么,LengthFieldBasedFrameDecoder 的参数值为
maxFrameLength: 大于该数据包长度的任意值,这里给了Integer.MAX_VALUE的长度
lengthFieldOffset: 长度域的起始位置就在消息的头部,offset=1
lengthFieldLength: 约定了为一个int类型的长度, lengthFieldLength=2(byte)
lengthAdjustment: byteLength - lengthFieldLengthValue - lengthFieldOffset - lengthFieldLength = 37 - 37 - 1 - 2 = -3
initialBytesToStrip: 由于解码后去除了消息头1(1byte)和长度域(2byte),此处为3