一、粘包与半包现象
1.1 粘包现象
server端
public class StickyBagServer {
static final Logger log = LoggerFactory.getLogger(StickyBagServer.class);
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 添加netty的日志处理器
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("connect {}", ctx.channel());
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.debug("disconnect {}", ctx.channel());
super.channelInactive(ctx);
}
});
}
});
ChannelFuture channelFuture = bootstrap.bind(8080);
channelFuture.sync();
log.debug("{} bound...", channelFuture.channel());
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
log.debug("error");
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
log.debug("shutdown");
}
}
}
client端
public class StickyBagClient {
static final Logger log = LoggerFactory.getLogger(StickyBagClient.class);
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
log.debug("connected...");
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
// 每次发送16个字节的数据,共发送10次
for (int i = 0; i < 10; i++) {
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
ctx.writeAndFlush(buffer);
}
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
控制台
00:29:39.428 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028] REGISTERED
00:29:39.429 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028] ACTIVE
00:29:39.438 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 4096
00:29:39.439 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
00:29:39.439 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
00:29:39.439 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
00:29:39.439 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.delayedQueue.ratio: 8
00:29:39.451 [nioEventLoopGroup-3-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkAccessible: true
00:29:39.451 [nioEventLoopGroup-3-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkBounds: true
00:29:39.451 [nioEventLoopGroup-3-1] DEBUG io.netty.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@144e1053
00:29:39.455 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028] READ: 160B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000010| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000020| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000030| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000040| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000050| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000060| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000070| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000080| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000090| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
+--------+-------------------------------------------------+----------------+
00:29:39.455 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 160, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
00:29:39.456 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded message pipeline : [LoggingHandler#0, StickyBagServer$1$1#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028].
00:29:39.456 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028] READ COMPLETE
可见虽然客户端是分别以16字节为单位,通过channel向服务器发送了10次数据,可是服务器端却只接收了一次,接收数据的大小为160B,即客户端发送的数据总大小,这就是粘包现象。
1.2 半包现象
我们可以调节server端接收缓冲区的大小,设置小一点,当client端一次发送的包size超过设置最大值的时候,就会发生半包现象。
在server端添加一行此代码,client端发送18个字节,分别为0-17,ChannelOption.SO_RCVBUF方法不生效。
bootstrap.channel(NioServerSocketChannel.class)
// .option(ChannelOption.SO_RCVBUF, 10)
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16))
.group(boss, worker)
控制台
1.3 原因分析
TCP协议中,每发送一次数据就需要进行一次ack确认,但是这样就意味着数据的发送将是串行的。于是TCP协议中引入了一个叫做滑动窗口的概念。滑动窗口其实就是一个缓冲区,在滑动窗口内发送的数据,无需接收响应,可以继续发送。当第一个数据ack确认之后,滑动窗口就会向下移动一个单位。当接收方的滑动窗口设置足够大,并且接收方处理不及时的情况下,发送方发过来的数据就会在接收方的滑动窗口中缓冲多个报文,最终导致粘包。当接收方的滑动窗口设置小于实际发送量,就只能先处理一部分数据,等ack确认后再处理后续的,就导致了半包的情况。
除了TCP层之外,Nagle算法也会造成粘包,网卡的MSS限制也会造成半包。
二、粘包半包解决–帧解码器
2.1 分隔符解码器
- LineBasedFrameDecoder行解码器,默认以 \n 或 \r\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常。
- DelimiterBasedFrameDecoder自定义分隔符解码器,支持自定义
- 方法入参:
maxLength – 最大长度
stripDelimiter – 解码的帧是否应该去掉分隔符,默认为true,去掉分隔符
failFast – 超过最大长度,true或者false都会抛出TooLongFrameException异常,没感觉此参数有什么区别
delimiter – 分隔符
LineBasedFrameDecoder
public class LineBaseFrameDecoderTest {
public static void main(String[] args) {
// 行解码器,默认以 \n 或 \r\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常,设置最大长度1024
LineBasedFrameDecoder frameDecoder = new LineBasedFrameDecoder(1024,false,false);
EmbeddedChannel embeddedChannel =
new EmbeddedChannel(frameDecoder, new LoggingHandler(LogLevel.INFO));
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeBytes("hello world\n".getBytes());
buffer.writeBytes("the time is running\n".getBytes());
embeddedChannel.writeInbound(buffer);
}
}
DelimiterBasedFrameDecoder
public class DelimiterBaseFrameDecoderTest {
public static void main(String[] args) {
// 自定义分隔符解码器,设置最大长度1024
DelimiterBasedFrameDecoder frameDecoder = new DelimiterBasedFrameDecoder(256, true, false,
ByteBufAllocator.DEFAULT.buffer().writeBytes("||".getBytes()));
EmbeddedChannel embeddedChannel = new EmbeddedChannel(frameDecoder, new LoggingHandler(LogLevel.INFO));
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeBytes("hello world||".getBytes());
buffer.writeBytes("the time is running||".getBytes());
embeddedChannel.writeInbound(buffer);
}
}
缺点:
- 处理字符数据比较合适,但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误
- 效率低,因为是按照传过来的字节一个一个去找换行符的
2.2 LTC解码器
有参构造
/**
* @param maxFrameLength 最大帧长度
* @param lengthFieldOffset 长度字段的偏移量,即长度字段从第几个字节开始
* @param lengthFieldLength 长度字段所占的字节数
* @param lengthAdjustment 以长度字段为基准,还有几个字节是内容
* @param initialBytesToStrip 从头剥离多少字节,即解码后直接去掉头部我们不想要的字节
*/
LengthFieldBasedFrameDecoder fieldBasedFrameDecoder = new LengthFieldBasedFrameDecoder(1024, 1, 2, 1, 1);
官方示例一:
官方示例二:
官方示例三:
官方示例四:
代码
public class LineBaseFrameDecoderTest {
public static void main(String[] args) {
LengthFieldBasedFrameDecoder fieldBasedFrameDecoder = new LengthFieldBasedFrameDecoder(1024, 2, 2, 1, 5);
EmbeddedChannel embeddedChannel =
new EmbeddedChannel(fieldBasedFrameDecoder, new LoggingHandler(LogLevel.INFO));
byte[] content = "hello, world".getBytes();
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
// 魔数,占两个字节
buffer.writeByte(0XCA);
buffer.writeByte(0XFE);
// 长度字段,占两个字节
buffer.writeShort(content.length);
// 版本号,占一个字节
buffer.writeByte(1);
// 内容占12个字节
buffer.writeBytes(content);
embeddedChannel.writeInbound(buffer);
}
}