目录
一、什么是粘包/半包问题
二、TCP粘包/半包发生的原因
三、粘包/半包解决办法
四、Netty中粘包/半包解决示例
1. 采用固定长度数据包编解码方式
2. 采用特殊字符作为边界字符编解码方式
3. 基于长度解码器
五、Netty常用编解码器
在客户端发送数据时,实际是把数据写入到了TCP发送缓存里面的; 如图:
1. 如果发送的包的大小比TCP发送缓存的容量大,那么这个数据包就会被分成多个包,通过socket多次发送到服务端,服务端第一次从接受缓存里面获取的数据,实际是整个包的一部分,这时候就产生了半包现象,半包不是说只收到了全包的一半,是说收到了全包的一部分。
2. 如果发送的包的大小比TCP发送缓存容量小,并且TCP缓存可以存放多个包,那么客户端和服务端的一次通信就可能传递了多个包,这时候服务端从接受缓存就可能一下读取了多个包,这时候就出现了粘包现象。
服务端从接受缓存读取数据后一般都是进行解码操作,也就是会把byte流转换了pojo对象,如果出现了粘包或者半包现象,则进行转换时候就会出现异常。出现粘包和半包的原因是TCP层不知道上层业务的包的概念,它只是简单的传递流,所以需要上层应用层协议来识别读取的数据是不是一个完整的包
示例:
假设客户端分别发送了两个数据包Data1和Data2给服务端, 由于服务端一次读取到的字节数是不确定的, 故可能存在以下4种情况:
(1) 服务端分两次读取到了两个独立的数据包,分别是Data1和Data2,没有粘包和拆包
(2) 服务端一次接收到了两个数据包,Data1和Data2粘合在一起,被称为TCP粘包
(3) 服务端分两次读取到了两个数据包,第一次读取到了完整的Data1包和Data2包的前一部分内容Data2_1,第二次读取到了Data2包的剩余内容Data2_2,这被称为TCP拆包
(4) 服务端分两次读取到了两个数据包,第一次读取到了Data1包的部分内容Data1_1,第二次读取到了Data1包的剩余内容Data1_2和Data2包的整包
如果此时服务端TCP接收缓冲区非常小,而数据包Data1和Data2比较大,很有可能会发生第五种可能,即服务端分多次才能将Data1和Data2包接收完全,期间发生多次拆包
1. 客户端发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包
2. 客户端待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包
3. 客户端发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包
4. 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包
1. 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开
2. 发送端将每个数据包封装为固定长度(不够的可以通过补0填充), 例如每个报文的大小为固定长度200字节,这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来
3. 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了
服务器端定义:
package com.netty2.sticky;
public class StickyDemoServer {
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new StickyDemoServer().bind(port);
}
public void bind(int port) throws Exception {
// 第一步:
// 配置服务端的NIO线程组
// 主线程组, 用于接受客户端的连接,但是不做任何具体业务处理,像老板一样,负责接待客户,不具体服务客户
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 工作线程组, 老板线程组会把任务丢给他,让手下线程组去做任务,服务客户
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 类ServerBootstrap用于配置Server相关参数,并启动Server
ServerBootstrap b = new ServerBootstrap();
// 链式调用
// 配置parentGroup和childGroup
b.group(bossGroup, workerGroup)
// 配置Server通道
.channel(NioServerSocketChannel.class)
// 配置通道的ChannelPipeline
.childHandler(new ChildChannelHandler());
// 绑定端口,并启动server,同时设置启动方式为同步
ChannelFuture f = b.bind(port).sync();
System.out.println(StickyDemoServer.class.getName() + " 启动成功,在地址[" + f.channel().localAddress() + "]上等待客户请求......");
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StickyDemoServerHandler());
}
}
}
服务器端处理器定义:
package com.netty2.sticky;
public class StickyDemoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("服务器接收到消息:" + in.toString(CharsetUtil.UTF_8));
//服务器将收到的信息返回到浏览器端
ctx.write(in);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
客户端定义:
package com.netty2.sticky;
public class StickyDemoClient {
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
}
}
new StickyDemoClient().connect(port, "127.0.0.1");
}
public void connect(int port, String host) throws Exception {
// 工作线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StickyDemoClientHandler());
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 等待客户端链路关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
group.shutdownGracefully();
}
}
}
客户端处理器定义:
package com.netty2.sticky;
public class StickyDemoClientHandler extends SimpleChannelInboundHandler {
private static String[] alphabets = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P"};
@Override
public void channelActive(ChannelHandlerContext ctx) {
for(int i=0; i<10; i++) {
StringBuilder builder = new StringBuilder();
builder.append("这是第");
builder.append(i);
builder.append("条消息, 内容是:");
for(int j=0; j<100; j++) {
builder.append(alphabets[i]);
}
builder.append("......");
System.out.println("客户端发送消息["+i+"]长度:"+builder.toString().getBytes().length);
ctx.writeAndFlush(Unpooled.copiedBuffer(builder.toString(), CharsetUtil.UTF_8));
}
}
@Override
public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
System.out.println("客户端接收到消息: " + in.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
启动后控制台输出:
服务器端:
客户端:
由控制台输出的信息可以看出, 由于没有对粘包/半包进行处理, 客户端在发送第一个数据包时把0-6和第7个消息的一部分作为一个数据包发送到客户端, 客户端没有进行拆包而是直接输出以及发送到客户端; 所以就造成了数据获取不完整;
(1) 固定长度数据包编解码处理器定义:
package com.netty2.sticky;
public class StickyDemoDecodeHandler extends ChannelInboundHandlerAdapter {
//存放待拆包数据的缓冲区
private ByteBuf cache;
private int frameLength;
public StickyDemoDecodeHandler(int length) {
this.frameLength = length;
}
static ByteBuf expandCache(ByteBufAllocator alloc, ByteBuf cache, int readable) {
ByteBuf oldCache = cache;
cache = alloc.buffer(oldCache.readableBytes() + readable);
cache.writeBytes(oldCache);
oldCache.release();
return cache;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf data = (ByteBuf) msg;
try {
//读取每一个消息,创建缓冲区
if (cache == null) {
cache = ctx.alloc().buffer(1024);
} else {
//如果现有的缓冲区容量太小,无法容纳原有数据+新读入的数据,就扩容(重新创建一个大的,并把数据拷贝过去)
if (data.readableBytes() > cache.maxCapacity() - cache.writerIndex() ) {
cache = expandCache(ctx.alloc(), cache, data.readableBytes());
}
}
//把新的数据读入缓冲区
cache.writeBytes(data);
/**
* 每次读取frameLength(定长)的数据,做为一个包,存储到集合中, 后面将遍历每个包进行发送
*/
List output = new ArrayList<>();
while (cache.readableBytes() >= frameLength) {
output.add(cache.readBytes(frameLength));
}
/**
* 如果缓冲区还存在部分数据(不是一个完整的数据包), 则调用discardReadBytes()清理缓冲区,将未读数据前移
*/
if (cache.isReadable()) {
cache.discardReadBytes();
}
for (int i = 0; i < output.size(); i++) {
/**
* 遍历数据包集合,发送所有数据包
*/
ctx.fireChannelRead(output.get(i));
}
} finally {
data.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
(2) 重写服务器端的ChildChannelHandler类, 将该处理器加入到pipeline中
private class ChildChannelHandler extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
/**
* 方案1: 将数据包按照固定长度进行解析
*/
ch.pipeline().addLast("framer", new StickyDemoDecodeHandler(139)); //自定义
//ch.pipeline().addLast("framer", new FixedLengthFrameDecoder(139)); //netty封装
ch.pipeline().addLast(new StickyDemoServerHandler());
}
}
(3) 重写客户端的handler()方法传入的ChannelInitializer匿名内部类, 将该处理器加入到pipeline中
new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
/**
* 方案1: 将数据包按照固定长度进行解析
*/
ch.pipeline().addLast("framer", new StickyDemoDecodeHandler(139)); //自定义
//ch.pipeline().addLast("framer", new FixedLengthFrameDecoder(139)); //netty封装
ch.pipeline().addLast(new StickyDemoClientHandler());
}
}
启动后控制台输出:
服务器端:
客户端:
(1) 特殊字符编解码处理器定义
package com.netty2.sticky;
public class StickyDemoDecodeHandlerV2 extends ChannelInboundHandlerAdapter {
private ByteBuf cache;
private byte delimiter; //包分隔符
public StickyDemoDecodeHandlerV2(ByteBuf delimiter) {
if (delimiter == null) {
throw new NullPointerException("delimiter");
}
if (!delimiter.isReadable()) {
throw new IllegalArgumentException("empty delimiter");
}
this.delimiter = delimiter.readByte();
}
static ByteBuf expandCache(ByteBufAllocator alloc, ByteBuf cache, int readable) {
ByteBuf oldCache = cache;
cache = alloc.buffer(oldCache.readableBytes() + readable);
cache.writeBytes(oldCache);
oldCache.release();
return cache;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf data = (ByteBuf) msg;
try {
if (cache == null) {
//读取每一个消息,创建缓冲区
cache = ctx.alloc().buffer(1024);
} else {
//如果现有的缓冲区容量太小,无法容纳原有数据+新读入的数据,就扩容(重新创建一个大的,并把数据拷贝过去)
if (data.readableBytes() > cache.maxCapacity() - cache.writerIndex()) {
cache = expandCache(ctx.alloc(), cache, data.readableBytes());
}
}
//把新的数据读入缓冲区
cache.writeBytes(data);
List output = new ArrayList<>();
int frameIndex = 0;
int frameEndIndex = 0;
int length = cache.readableBytes();
while (frameIndex <= length) {
//获取缓冲区中边界字符的下标位置; frameIndex:开始读取的下标位置
frameEndIndex = cache.indexOf(frameIndex + 1, length, delimiter);
if (frameEndIndex == -1) {
//如果缓冲区没有边界字符, 说明缓冲区中未读范围中是一个不完整的数据包, 则清理缓冲区
cache.discardReadBytes();
break;
}
//如果存在边界字符, 则读取到边界字符的位置
output.add(cache.readBytes(frameEndIndex - frameIndex));
//将指针跳过边界字符所占的下标位置
cache.skipBytes(1);
//更新读取位置
frameIndex = frameEndIndex + 1;
}
if (cache.isReadable()) {
cache.discardReadBytes();
}
for (int i = 0; i < output.size(); i++) {
//遍历数据包集合,发送所有数据包
ctx.fireChannelRead(output.get(i));
}
} finally {
data.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
(2) 重写服务器端的ChildChannelHandler类, 将该处理器加入到pipeline中
private class ChildChannelHandler extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
/**
* 方案2: 使用特殊字符作为边界()自定义
*/
ch.pipeline().addLast("framer", new StickyDemoDecodeHandlerV2(Unpooled.wrappedBuffer(new byte[] { '#' }))); //自定义
//ch.pipeline().addLast("framer", new DelimiterBasedFrameDecoder(8192, Unpooled.wrappedBuffer(new byte[] { '#' }))); //netty封装
ch.pipeline().addLast(new StickyDemoServerHandler());
}
}
(3) 修改StickyDemoServerHandler的channelRead()方法, 在服务器端回传数据时需要加上边界字符
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("服务器接收到消息:" + in.toString(CharsetUtil.UTF_8));
//服务器将收到的信息返回到浏览器端
ctx.write(in);
/**
* 服务器端发送数据时需要给每个包最后添加一个边界字符
*/
ctx.write(Unpooled.copiedBuffer("#", CharsetUtil.UTF_8));
}
(4) 重写客户端的handler()方法传入的ChannelInitializer匿名内部类, 将该处理器加入到pipeline中
new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
/**
* 方案2: 使用特殊字符作为边界(netty封装)
*/
ch.pipeline().addLast("framer", new StickyDemoDecodeHandlerV2(Unpooled.wrappedBuffer(new byte[] { '#' }))); //自定义
//ch.pipeline().addLast("framer", new DelimiterBasedFrameDecoder(8192, Unpooled.wrappedBuffer(new byte[] { '#' }))); //netty封装
ch.pipeline().addLast(new StickyDemoClientHandler());
}
}
(5) 修改StickyDemoClientHandler的channelActive()方法, 在发送的信息后添加边界字符
@Override
public void channelActive(ChannelHandlerContext ctx) {
for(int i=0; i<10; i++) {
StringBuilder builder = new StringBuilder();
builder.append("这是第");
builder.append(i);
builder.append("条消息, 内容是:");
for(int j=0; j<100; j++) {
builder.append(alphabets[i]);
}
builder.append("......");
/**
* 添加边界字符, 可以在读取时按照边界字符进行编解码
*/
builder.append("#");
System.out.println("客户端发送消息["+i+"]长度:"+builder.toString().getBytes().length);
ctx.writeAndFlush(Unpooled.copiedBuffer(builder.toString(), CharsetUtil.UTF_8));
}
}
启动后控制台输出:
服务器端:
客户端:
a. 基于数据头不固定长度的解码器:LengthFieldBasedFrameDecoder
参数说明:
b. LengthFieldPrepender 编码器
参数说明:
(1)服务器端定义:
package com.netty2.lf;
public class LengthFieldDecodeDemoServer {
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new LengthFieldDecodeDemoServer().bind(port);
}
public void bind(int port) throws Exception {
// 第一步:
// 配置服务端的NIO线程组
// 主线程组, 用于接受客户端的连接,但是不做任何具体业务处理,像老板一样,负责接待客户,不具体服务客户
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 工作线程组, 老板线程组会把任务丢给他,让手下线程组去做任务,服务客户
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 类ServerBootstrap用于配置Server相关参数,并启动Server
ServerBootstrap b = new ServerBootstrap();
// 链式调用
// 配置parentGroup和childGroup
b.group(bossGroup, workerGroup)
// 配置Server通道
.channel(NioServerSocketChannel.class)
// 配置通道的ChannelPipeline
.childHandler(new ChildChannelHandler());
// 绑定端口,并启动server,同时设置启动方式为同步
ChannelFuture f = b.bind(port).sync();
System.out.println(LengthFieldDecodeDemoServer.class.getName() + " 启动成功,在地址[" + f.channel().localAddress() + "]上等待客户请求......");
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("addLength", new LengthFieldPrepender(4, false));
ch.pipeline().addLast("framer", new LengthFieldBasedFrameDecoder(1024, 0, 4, 0,4));
ch.pipeline().addLast(new LengthFieldDecodeDemoServerHandler());
}
}
}
(2) 服务器端处理器定义:
package com.netty2.lf;
public class LengthFieldDecodeDemoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
System.out.println("服务器接收到消息:" + in.toString(CharsetUtil.UTF_8));
ctx.write(in);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
(3) 客户端定义:
package com.netty2.lf;
public class LengthFieldDecodeDemoClient {
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
}
}
new LengthFieldDecodeDemoClient().connect(port, "127.0.0.1");
}
public void connect(int port, String host) throws Exception {
// 工作线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("framer", new LengthFieldBasedFrameDecoder(1024, 0, 4, 0,4));
ch.pipeline().addLast("addLength", new LengthFieldPrepender(4, false));
ch.pipeline().addLast(new LengthFieldDecodeDemoClientHandler());
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 等待客户端链路关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
group.shutdownGracefully();
}
}
}
(4) 客户端处理器定义:
package com.netty2.lf;
public class LengthFieldDecodeDemoClientHandler extends SimpleChannelInboundHandler {
private Random rand = new Random();
private static String[] alphabets = {"A", "B", "C", "D", "E", "F", "G", "H", "I","J", "K", "L", "M", "N", "O", "P"};
@Override
public void channelActive(ChannelHandlerContext ctx) {
for(int i=0; i<10; i++) {
StringBuilder builder = new StringBuilder();
builder.append("这是第");
builder.append(i);
builder.append("条消息, 内容是:");
for(int j=0; j
启动后控制台输出:
服务器端:
客户端:
客户端处理器V2定义(加入头部信息)
package com.netty2.lf;
public class LengthFieldDecodeDemoClientHandlerV2 extends SimpleChannelInboundHandler {
private Random rand = new Random();
private static String[] alphabets = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P" };
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i = 0; i < 10; i++) {
StringBuilder builder = new StringBuilder();
builder.append("这是第");
builder.append(i);
builder.append("条消息, 内容是:");
for (int j = 0; j < rand.nextInt(20); j++) {
builder.append(alphabets[i]);
}
builder.append("......");
ByteBuf header = Unpooled.copiedBuffer("Header", CharsetUtil.UTF_8);
ByteBuf body = Unpooled.copiedBuffer(builder.toString(), CharsetUtil.UTF_8);
CompositeByteBuf cbf = ctx.alloc().compositeBuffer();
cbf.addComponents(true, header, body);
ctx.writeAndFlush(cbf);
}
}
@Override
public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
System.out.println("客户端接收到消息: " + in.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
启动后控制台输出:
服务器端:
客户端:
LineBasedFrameDecoder
DelimiterBasedFrameDecoder
FixedLengthFrameDecoder
LengthFieldBasedFrameDecoder