Netty学习——Netty解决TCP粘包与拆包问题

TCP 粘包与拆包

TCP是一个“流”协议,TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小包封装成一个大的数据包进行发送,这就是TCP的粘包和拆包问题。

TCP 粘包与拆包问题说明

Netty学习——Netty解决TCP粘包与拆包问题_第1张图片
假设客户端分别发送了俩个数据包D1和D2到服务端。但服务端每次读到的字节数是不确定的,所以可能存在下面四种情况:

  1. 服务端分两次读取到了D1和D2数据包,也就是理想的正常情况,没有发生粘包和拆包。
  2. 服务端一次接收收到了两个数据包,此时,D1和D2粘到了一起,这种情况被称为TCP粘包。
  3. 服务端分两次读取到了两个数据包,第一次读取到了D1完整的数据包和D2部分的数据,第二次读取到了D2剩余的数据,这种情况被称为TCP拆包。
  4. 服务端分两次读取到了两个数据包,第一次读取到了D2部分数据,第二次读取到了D1剩余的数据和D2完整的数据包,这种情况和第三种情况一样是TCP拆包。
  5. 第五种情况要假设TCP接收滑窗非常小,而数据包D1和D2比较大,这样就有可能发生服务端多次接收才能完成接收,期间会发生多次拆包。
TCP 粘包与拆包发生的原因

问题产生的原因有三点:

  • 程序写入的字节大小大于套接口发送缓冲去大小。
  • 进行MSS大小的TCP分段。
  • 以太网帧的payload大于MTU进行IP分片。
    Netty学习——Netty解决TCP粘包与拆包问题_第2张图片

Netty中解决TCP粘包与拆包问题

TCP粘包与拆包异常案例

我们通过一个简单的Netty服务端和客户端例子来复现一下粘包和拆包的异常情况。这个例子中客户端会向服务端发送一个指定的字符串来查询当前时间,服务端接收客户端发送的字符串,如果是合法的查询字符串则返回当前的系统实现,否则返回错误提示。

服务端TimeServer.java
package problem;

import java.util.logging.Logger;

import io.netty.bootstrap.ServerBootstrap;
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 problem.handler.TimeServerHandler;

public class TimeServer {

	private static final Logger logger = Logger.getLogger(TimeServer.class.getName());

	public void run(int port) {
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
					.childOption(ChannelOption.SO_KEEPALIVE, true)
					.childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						protected void initChannel(SocketChannel ch) throws Exception {
							// TODO Auto-generated method stub
							ch.pipeline().addLast(new TimeServerHandler());
						}
					});
			ChannelFuture f = b.bind(port).sync();
			f.channel().closeFuture().sync();
		} catch (InterruptedException e) {
			logger.warning(e.getLocalizedMessage());
		} finally {
			bossGroup.shutdownGracefully();
			workGroup.shutdownGracefully();
		}
	}

	public static void main(String[] args) {
		new TimeServer().run(8080);
	}
}
服务端处理类TimeServerHandler.java
package problem.handler;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.logging.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

	private static final Logger logger = Logger.getLogger(TimeServerHandler.class.getName());

	private int counter;

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// TODO Auto-generated method stub
		ByteBuf buf = (ByteBuf) msg;
		byte[] req = new byte[buf.readableBytes()];
		buf.readBytes(req);
		String body = new String(req, StandardCharsets.UTF_8).substring(0,
				req.length - System.getProperty("line.separator").length());
		System.out.println("The time server receive order : " + body + "; This counter is : " + (++counter));
		String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)
				? new Date(System.currentTimeMillis()).toString() + System.getProperty("line.separator")
				: "BAD ORDERE";
		ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
		ctx.writeAndFlush(resp);
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		logger.warning(cause.getLocalizedMessage());
		ctx.close();
	}

}
客户端TimeClient.java
package problem;

import java.util.logging.Logger;

import io.netty.bootstrap.Bootstrap;
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 problem.handler.TimeClientHandler;

public class TimeClient {

	private static final Logger logger = Logger.getLogger(TimeClient.class.getName());

	public void run(String host, int port) {
		EventLoopGroup group = new NioEventLoopGroup();
		try {
			Bootstrap b = new Bootstrap();
			b.group(group).channel(NioSocketChannel.class).option(ChannelOption.SO_KEEPALIVE, true)
					.handler(new ChannelInitializer<SocketChannel>() {
						@Override
						protected void initChannel(SocketChannel ch) throws Exception {
							// TODO Auto-generated method stub
							ch.pipeline().addLast(new TimeClientHandler());
						}
					});
			ChannelFuture future = b.connect(host, port).sync();
			future.channel().closeFuture().await(5000L);
		} catch (InterruptedException e) {
			logger.warning(e.getLocalizedMessage());
		} finally {
			group.shutdownGracefully();
		}
	}

	public static void main(String[] args) {
		new TimeClient().run("127.0.0.1", 8080);
	}
}
客户端处理类TimeClientHandler.java
package problem.handler;

import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {

	private static final Logger logger = Logger.getLogger(TimeClientHandler.class.getName());

	private byte[] req;

	private int counter;

	public TimeClientHandler() {
		req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
	}

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		System.out.println("client connect server successful!");
		ByteBuf message = null;
		for (int i = 0; i < 100; i++) {
			message = Unpooled.buffer(req.length);
			message.writeBytes(req);
			ctx.writeAndFlush(message);
		}
	}

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		System.out.println("读取");
		ByteBuf buf = (ByteBuf) msg;
		byte[] receiveByte = new byte[buf.readableBytes()];
		buf.readBytes(receiveByte);
		String body = new String(receiveByte, StandardCharsets.UTF_8);
		System.out.println("Now is " + body + "; counter:" + (++counter));
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		System.err.println(cause.getLocalizedMessage());
		ctx.close();
	}
}
运行效果
服务端

Netty学习——Netty解决TCP粘包与拆包问题_第3张图片
Netty学习——Netty解决TCP粘包与拆包问题_第4张图片
可以看到服务端只接收了两次,这里发生了粘包

客户端

Netty学习——Netty解决TCP粘包与拆包问题_第5张图片
同样客户端应该收到两条回复才对,但是这里只有一条,也发生了粘包。由于我们程序中没有考虑粘包和拆包所以发生了上面的情况。

使用Netty中的编码器解决TCP粘包问题

为了解决TCP粘包和拆包导致的半包读写问题,Netty提供了一些自带的编码器用来处理半包。下面的代码是对上面发生异常的代码做的修改。

TimeServer.java下在childHandler方法下做如下修改
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeServerHandler());

也就是在原有的基础上增加了LineBasedFrameDecoder这个解码器和StringDecoder解码器。

TimeServerHandler.java下将channelRead这个方法做如下修改
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// TODO Auto-generated method stub
//		ByteBuf buf = (ByteBuf) msg;
//		byte[] req = new byte[buf.readableBytes()];
//		buf.readBytes(req);
//		String body = new String(req, StandardCharsets.UTF_8).substring(0,
//				req.length - System.getProperty("line.separator").length());
		String body = (String) msg;
		System.out.println("The time server receive order : " + body + "; This counter is : " + (++counter));
		String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)
				? new Date(System.currentTimeMillis()).toString() + System.getProperty("line.separator")
				: "BAD ORDERE";
		ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
		ctx.writeAndFlush(resp);
	}

可以看到,将上面转化接收数据的过程注释掉,然后把msg强转为String即可(相当简洁)。

TimeClient.java在handler方法中做如下修改
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeClientHandler());

修改方式和服务端一样。

TimeClientHandler在channelRead方法中做如下修改
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//		System.out.println("读取");
//		ByteBuf buf = (ByteBuf) msg;
//		byte[] receiveByte = new byte[buf.readableBytes()];
//		buf.readBytes(receiveByte);
//		String body = new String(receiveByte, StandardCharsets.UTF_8);
		String body = (String) msg;
		System.out.println("Now is " + body + "; counter:" + (++counter));
	}

同样和服务端一样。

运行效果
服务端

Netty学习——Netty解决TCP粘包与拆包问题_第6张图片
Netty学习——Netty解决TCP粘包与拆包问题_第7张图片

客户端

Netty学习——Netty解决TCP粘包与拆包问题_第8张图片
Netty学习——Netty解决TCP粘包与拆包问题_第9张图片
此时就会发现粘包的问题已经解决了,使用Netty来解决对使用者来说是很方便的,只需要将支持半包的解码的handler添加到ChannelPipeline中即可,不需要额外的代码。

原理分析

LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断是否有"\n"或者"\r\n",如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,同时支持配置单行最大长度,如果连续读取到最大长度后没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。
StringDecoder的作用就是将接收到的对象转为字符串,然后再继续调用后面的handler。LineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器。初次之外,Netty还有其他多种解码器,用来满足不同的需求。

你可能感兴趣的:(Netty,tcp/ip,学习,网络)