Netty之Tcp拆包粘包

1、TCP粘包、拆包问题

    1.1TCP粘包/拆包问题

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

    1.2TCP粘包/拆包发生的原因

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

        2.进行MSS(TCP协议定义的最大报文段长度)大小的TCP分段。

        3.以太网帧的payload大于MTU最大传输单元)进行IP分片。

2、TCP粘包问题的解决策略  

    2.1TCP粘包问题解决策略            

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

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

           2. 包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分;

           3. 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段;

           4. 更复杂的自定义应用层协议。

    2.2netty粘包问题解决策略  

        Netty提供了多个解码器,可以进行分包的操作,分别是: 
            * LineBasedFrameDecoder (回车换行解码器
            * DelimiterBasedFrameDecoder
(添加特殊分隔符报文来分包) 
            * FixedLengthFrameDecoder
(使用定长的报文来分包) 
            * LengthFieldBasedFrameDecoder(
通过在消息头定义长度字段来标识消息总长度)

    2.2.1LineBasedFrameDecoder     

        LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。StringDecoder的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的HandleroLineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包。 

        代码实现

        服务器端

package com.xyz.netty.decoder1;

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 io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class Server {

	public static void main(String[] args) throws Exception{
		//1、定义两个线程组
		EventLoopGroup pGroup = new NioEventLoopGroup();//一个是用于处理服务器端接收客户端连接的  
		EventLoopGroup cGroup = new NioEventLoopGroup();//一个是进行网络通信的(网络读写的)  
		try {
			//2、创建辅助工具类,用于服务器通道的一系列配置  
			ServerBootstrap bootstrap = new ServerBootstrap();
			bootstrap.group(pGroup, cGroup)//绑定俩个线程组
			.channel(NioServerSocketChannel.class)//指定NIO的模式
			.option(ChannelOption.SO_BACKLOG, 1024)
			.option(ChannelOption.SO_SNDBUF, 32*1024)
			.option(ChannelOption.SO_RCVBUF, 32*1024)
			.childHandler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					
					//3、处理业务
					ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
					//设置字符串形式的解码
					ch.pipeline().addLast(new StringEncoder());
					ch.pipeline().addLast(new StringDecoder());
					//处理业务
					ch.pipeline().addLast(new ServerHandler());
				}
			});
			
			//4、进行绑定
			ChannelFuture cf = bootstrap.bind(8765).sync();
			System.out.println("server start....");
			//5、等待关闭
			cf.channel().closeFuture().sync();
		} finally{
			pGroup.shutdownGracefully();
			cGroup.shutdownGracefully();
		}
		
	}

}

        服务器端管理类

package com.xyz.netty.decoder1;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;

public class ServerHandler extends ChannelHandlerAdapter{

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg)throws Exception {
		
		//读取客户端发过来的信息  
		String body = (String) msg;
		System.out.println("服务器接收到客户端的信息为 " + body);
		
		//给客户端回应信息
		String response = "server 已经接收到信息, 信息为 " + body + "\n";
		ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
	}

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

        客户端

package com.xyz.netty.decoder1;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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 io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class Client {
	
	public static void main(String[] args) throws Exception{
		
		//1、创建线程组
		EventLoopGroup group = new NioEventLoopGroup();
		try {
			
			//2、创建辅助工具类,用于服务器通道的一系列配置  
			Bootstrap bootstrap = new Bootstrap();
			bootstrap.group(group)
			.channel(NioSocketChannel.class)
			.handler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					
					//3 在这里配置具体数据接收方法的处理  
					ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
					
					//设置字符串解码
					ch.pipeline().addLast(new StringEncoder());
					ch.pipeline().addLast(new StringDecoder());
					
					ch.pipeline().addLast(new ClientHandler());
				}
			});
			
			//4、建立连接
			ChannelFuture cf =  bootstrap.connect("127.0.0.1", 8765).sync();
			System.out.println("Client connet.....");
			
			//5、发送信息
			cf.channel().writeAndFlush(Unpooled.wrappedBuffer("Hello\nworld\n!\n".getBytes()));
			cf.channel().writeAndFlush(Unpooled.wrappedBuffer("你好\n世界\n!\n".getBytes()));
			
			//6、等待关闭
			cf.channel().closeFuture().sync();
		} finally{
			group.shutdownGracefully();
		}
	}
		
}

        客户端管理类

package com.xyz.netty.decoder1;

import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;

public class ClientHandler extends ChannelHandlerAdapter{

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg)throws Exception {
		
		try {
			//读取服务器端发过来的信息
			String body = (String) msg;
			System.out.println("客户端接收到服务端的响应消息 " + body);
		} finally{
			ReferenceCountUtil.release(msg);
		}
	}

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

    2.2.2DelimiterBasedFrameDecoder

        首先创建分隔符缓冲对象ByteBuf,我使用“$一”作为分隔符。创建DelimiterBasedFrameDecoder对象,将其加入到ChannelPipeline中。DelimiterBasedFrameDecoder有多个构造方法,这里我选择了传递两个参数的:第一个1024表示单条消息的最大长度,当达到该长度后仍然没有查找到分隔符,就抛出TooLongFrameException异常,防止由于异常码流缺失分隔符导致的内存溢出;第二个参数就是分隔符缓冲对象。

        代码实现(和LineBasedFrameDecoder有区别的地方客户端处理类保持不变

        服务器端

bootstrap.group(pGroup, cGroup)//绑定俩个线程组
			.channel(NioServerSocketChannel.class)//指定NIO的模式
			.option(ChannelOption.SO_BACKLOG, 1024)
			.option(ChannelOption.SO_SNDBUF, 32*1024)
			.option(ChannelOption.SO_RCVBUF, 32*1024)
			.childHandler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					
					//3、处理业务
					//创建分隔符缓冲对象ByteBuf
					ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
					ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
					//设置字符串形式的解码
					ch.pipeline().addLast(new StringEncoder());
					ch.pipeline().addLast(new StringDecoder());
					//处理业务
					ch.pipeline().addLast(new ServerHandler());
				}
			});

        服务器管理类 

//给客户端回应信息
		String response = "server 已经接收到信息, 信息为 " + body + "$_";
		ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));

        客户端

package com.xyz.netty.decoder2;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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 io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class Client {
	
	public static void main(String[] args) throws Exception{
		
		//1、创建线程组
		EventLoopGroup group = new NioEventLoopGroup();
		try {
			
			//2、创建辅助工具类,用于服务器通道的一系列配置  
			Bootstrap bootstrap = new Bootstrap();
			bootstrap.group(group)
			.channel(NioSocketChannel.class)
			.handler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					
					//3 在这里配置具体数据接收方法的处理  
					//创建分隔符缓冲对象ByteBuf
					ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
					ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
					
					//设置字符串解码
					ch.pipeline().addLast(new StringEncoder());
					ch.pipeline().addLast(new StringDecoder());
					
					ch.pipeline().addLast(new ClientHandler());
				}
			});
			
			//4、建立连接
			ChannelFuture cf =  bootstrap.connect("127.0.0.1", 8765).sync();
			System.out.println("Client connet.....");
			
			//5、发送信息
			cf.channel().writeAndFlush(Unpooled.wrappedBuffer("Hello$_world$_!$_".getBytes()));
			cf.channel().writeAndFlush(Unpooled.wrappedBuffer("你好$_世界$_!$_".getBytes()));
			
			//6、等待关闭
			cf.channel().closeFuture().sync();
		} finally{
			group.shutdownGracefully();
		}
	}
		
}

    2.2.3FixedLengthFrameDecoder        

        是固定长度解码器,它能按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包等问题。利用FixedLengthFrameDecoder解码,无论一次性接收到多少的数据,他都会按照构造函数中设置的长度进行解码;如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下一个包,到达后进行拼包,直到读取完整的包。

    不同编码中英文字母和中文所占的字节长度

        英文字母

            字节数 : 1;编码:GBK

          字节数 : 1;编码:UTF-8

        中文

            字节数 : 2;编码:GBK

            字节数 : 3;编码:UTF-8

        代码实现

        服务器端

.childHandler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					
					//3、处理业务
					ch.pipeline().addLast(new FixedLengthFrameDecoder(6));
					//设置字符串形式的解码
					ch.pipeline().addLast(new StringEncoder());
					ch.pipeline().addLast(new StringDecoder());
					//处理业务
					ch.pipeline().addLast(new ServerHandler());
				}
			});

        服务器管理类

//给客户端回应信息
		String response = "server" + body;
		ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));

        客户端

package com.xyz.netty.decoder3;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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 io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class Client {
	
	public static void main(String[] args) throws Exception{
		
		//1、创建线程组
		EventLoopGroup group = new NioEventLoopGroup();
		try {
			
			//2、创建辅助工具类,用于服务器通道的一系列配置  
			Bootstrap bootstrap = new Bootstrap();
			bootstrap.group(group)
			.channel(NioSocketChannel.class)
			.handler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					
					//3 在这里配置具体数据接收方法的处理  
					ch.pipeline().addLast(new FixedLengthFrameDecoder(6));
					
					//设置字符串解码
					ch.pipeline().addLast(new StringEncoder());
					ch.pipeline().addLast(new StringDecoder());
					
					ch.pipeline().addLast(new ClientHandler());
				}
			});
			
			//4、建立连接
			ChannelFuture cf =  bootstrap.connect("127.0.0.1", 8765).sync();
			System.out.println("Client connet.....");
			
			//5、发送信息
			cf.channel().writeAndFlush(Unpooled.wrappedBuffer("Hello world !     ".getBytes()));
			cf.channel().writeAndFlush(Unpooled.wrappedBuffer("你好世界!     ".getBytes()));
			
			//6、等待关闭
			cf.channel().closeFuture().sync();
		} finally{
			group.shutdownGracefully();
		}
	}
		
} 

猜你喜欢

转载自blog.csdn.net/strong997/article/details/79999804