Netty的TCP粘包/拆包

目录

 

1 TCP粘包/拆包

TCP粘包/拆包问题说明

TCP粘包/拆包发生的原因

粘包问题的解决策略

2 Netty遇到粘包和拆包

3 Netty解决粘包和拆包


1 TCP粘包/拆包

  TCP是一个“流”协议,所谓流就是没有边界的一串字符串,其间没有分界线。下面就用一个列子来说明

TCP粘包/拆包问题说明

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。

(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;

(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;

(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;

(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。

如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

TCP粘包/拆包发生的原因

问题产生的原因有三个,分别如下。

(1)应用程序write写入的字节大小大于套接口发送缓冲区大小;

(2)进行MSS大小的TCP分段;

(3)以太网帧的payload大于MTU进行IP分片。

 Netty的TCP粘包/拆包_第1张图片

粘包问题的解决策略

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。

(1)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;

(2)在包尾增加回车换行符进行分割,例如FTP协议;

(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度;

(4)更复杂的应用层协议。

2 Netty遇到粘包和拆包

   在前面一章(Netty的HelloWord)中客户端只发送了一次请求给服务端,如果发送多次请求而且没有进行TCP粘包和拆包的处理,会发生什么样的情况。

 我们把上一章的例子的TimeClientHandler和TimeServerHandler进行修改

TimeClientHandler类 把发送消息加上一个换行符 然后发送一百次

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

System.out.println("从服务端得到的时间:"+body+"   counter:"+ ++counter);

public class TimeClientHandler extends ChannelHandlerAdapter {
	
	private static final Logger logger = Logger.getLogger(TimeClientHandler.class.getName());
	
	private byte[] req;
	private int counter = 0;

	public TimeClientHandler() {
		req = ("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes();
	}
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		ByteBuf message;
		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 {
		ByteBuf buf = (ByteBuf) msg;
		byte[] time = new byte[buf.readableBytes()];
		buf.readBytes(time);
		String body = new String(time,"utf-8");
		System.out.println("从服务端得到的时间:"+body+"   counter:"+ ++counter);
		
	}
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		logger.warning(cause.getMessage());
		ctx.close();
	}
	
}

TimeServerHandler类 服务端获取信息

String body = new String(req,"utf-8").substring(0,req.length-(System.getProperty("line.separator").length()));

System.out.println("从客戶端发送的消息:"+body+"  counter:"+ ++counter);

public class TimeServerHandler extends ChannelHandlerAdapter {
	private int counter = 0;
	
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		ByteBuf buf = (ByteBuf) msg;
		byte[] req = new byte[buf.readableBytes()];
		buf.readBytes(req);
		//System.getProperty("line.separator") 获取回车换行符
		String body = new String(req,"utf-8").substring(0,req.length-(System.getProperty("line.separator").length()));
		
		System.out.println("从客戶端发送的消息:"+body+"  counter:"+ ++counter);
		
		String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
				System.currentTimeMillis()).toString() : "BAD ORDER";
		
		
		ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
		ctx.write(resp);
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
		ctx.flush();
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		ctx.close();
	}
}

  测试 先运行服务端,在运行客户端

结果会发现并没有我们想象的那样,客户端发送一百条请求,服务端会接受一百条请求并且打印这些请求,然后在响应客户端一百条响应,客户端打印一百条来自服务端响应的消息

出现这种的原因在于消息在TCP中发生了粘包和拆包的问题

3 Netty解决粘包和拆包

   在Netty中解决粘包和拆包问题需要使用解码器(Decoder),在Netty中提供了许多的解码器,比如LineBasedFrameDecoder,回车符解码器,FixedLengthFrameDecoder固定长度解码器。这些解码器的父类是ByteToMessageDecoder

Netty的TCP粘包/拆包_第2张图片

在ByteToMessageDecoder中定义了一个decode的方法,该方法定义了解码规则,不同的解码器实现ByteToMessageDecoder类重写decode方法编写解码规则,我们也可以自己定义自己的解码器,只要重写decode方法就可以。

我们以LineBasedFrameDecoder解码器来说明其流程

还是以前面的例子来介绍其流程(我们前面的例子在消息的结尾都加上了回车换行符)

首先我们在TimeServer类中ChlidChannelHandler 方法中在初始化Channel的时候加上LineBasedFrameDecoder(1024)其中1024表示这个数据包的大小,也就是编码规则(我们这里表示的是换行符)之间的消息大小,如果数据包大小超过就会报异常

我们还加上了StringDecoder()解码器,将消息变成String类型。这样我们就可以在TimeServerHandler()中不需要讲消息转变成String类型可以直接使用。

private class ChlidChannelHandler extends ChannelInitializer{

		@Override
		protected void initChannel(SocketChannel ch) throws Exception {
			/**
			 * LineBasedFrameDecoder() 换行符解码器
			 * StringDecoder()将接收到的对象转换成String对象
			 */
			ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
			ch.pipeline().addLast(new StringDecoder());
			ch.pipeline().addLast(new TimeServerHandler());
			
		}
	}

我们点开LineBasedFrameDecoder类或者StringDecoder类就可以看到decode方法中编写的解码规则。并且将解码后的消息放在一个list集合中,该消息会传到给下一个解码器或者处理器(下面代码为StringDecoder解码器中的decode方法)

 @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception {
        out.add(msg.toString(charset));
    } 
  

然后我们在TimeCilent类中加入LineBasedFrameDecoder类和StringDecoder类解码器。

	private class ChlidChannelHandler extends ChannelInitializer{

		@Override
		protected void initChannel(SocketChannel ch) throws Exception {
			/**
			 * LineBasedFrameDecoder() 换行符解码器
			 * StringDecoder()将接收到的对象转换成String对象
			 */
			ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
			ch.pipeline().addLast(new StringDecoder());
			ch.pipeline().addLast(new TimeServerHandler());
			
		}
	}

最后 运行服务端,接着客户端

Netty的TCP粘包/拆包_第3张图片

Netty的TCP粘包/拆包_第4张图片

你可能感兴趣的:(Netty)