1、TCP粘包/分包现象

在TCP通信过程中,客户端与服务端是通过Socket进行通信的,数据的发送为了提高效率,采用了高效的Nagle算法,发送方将数据发送给Socket缓冲区,当缓存区满了
或者时间超时,发送方Socket会将数据发送互接收方。这里就会引起一个问题,如果发送方一次性发送的数据太大了,缓冲区无法一次性完成缓冲与发送,就会将数据进行分包
分多次写入缓冲区,多次进行发送,这就是分包现象。而粘包是指数据接收方,在接收方缓冲区中,接收到了多个数据包,多个数据包前后首尾相连,接收方缓冲区越小,发生
粘包的概念越大,粘包即有可能发生在发送端,也有可能发生在接收端。

 TCP为了保证可靠传输并减少额外的开销(每次发包都要验证),采用了基于流的传输,基于流的传输不认为消息是一条一条的,是无保护消息边界的
(保护消息边界:指传输协议把数据当做一条独立的消息在网上传输,接收端一次只能接受一条独立的消息)。UDP则是面向消息传输的,是有保护消息边界的,接收方一次只接受一条独立的信息,
所以不存在粘包问题。
       

2、粘包原因

2.1 发送方原因

TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),Nagle算法造成了发送方可能会出现粘包问题,而Nagle算法主要做两件事:

  1. 只有上一个分组得到确认,才会发送下一个分组
  2. 收集多个小分组,在一个确认到来时一起发送

对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。

2.2 接收方原因

       TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,
如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。

     接收方没有办法来处理粘包现象,只能将问题交给应用层来处理。应用层的解决办法简单可行,不仅能解决接收方的粘包问题,还可以解决发送方的粘包问题。
 

3、粘包分包主流解决方案

3.1 消息定长

消息的长度是固定的,例如每条消息长度是150Byte,超过150Byte则需要将消息拆分成多条消息发送;如果不足150Byte,则需要在消息后面填充特殊字符直到固定长度。

Netty提供了消息定长的解决方案:FixedLengthFrameDecoder

服务端代码:

public class Server {

	public static void main(String[] args) throws Exception {
		// 1 创建2个线程池,一个是负责接收客户端的连接。一个是负责进行数据传输的
		final EventLoopGroup pGroup = new NioEventLoopGroup();
		final EventLoopGroup cGroup = new NioEventLoopGroup();
		// 2 创建服务器辅助类
		final ServerBootstrap b = new ServerBootstrap();
		b.group(pGroup, cGroup).channel(NioServerSocketChannel.class)
				.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 sc) throws Exception {
						// 设置定长字符串接收
						sc.pipeline().addLast(new FixedLengthFrameDecoder(5));//定长消息
						// 设置字符串形式的解码
						sc.pipeline().addLast(new StringDecoder());
						sc.pipeline().addLast(new ServerHandler());
					}
				});

		// 4 绑定连接
		final ChannelFuture cf = b.bind(8765).sync();
		// 等待服务器监听端口关闭
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
		cGroup.shutdownGracefully();
	}
}

客户端代码:

public class Client {
       public static void main(String[] args) throws Exception {
		 EventLoopGroup group = new NioEventLoopGroup();
		 Bootstrap b = new Bootstrap();
		  b.group(group)
		 .channel(NioSocketChannel.class)
		 .handler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel sc) throws Exception {
				sc.pipeline().addLast(new FixedLengthFrameDecoder(5));//定长消息
				sc.pipeline().addLast(new StringDecoder());
				sc.pipeline().addLast(new ClientHandler());
			}
		});
		ChannelFuture cf = b.connect("127.0.0.1", 8765).sync();
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("aaaaabbbbb".getBytes()));
		Thread.sleep(2000);
		cf.channel().writeAndFlush(Unpooled.copiedBuffer("ccccccc".getBytes()));
		//等待客户端端口关闭
		cf.channel().closeFuture().sync();
		group.shutdownGracefully();
		
	}
}

3.2 加消息结束符

    在每条消息结束之后补上特殊字符以表示消息结束,例如CRLF(回车换行)来表示消息结束。

Netty提供了追加消息结束符的解决方案:DelimiterBasedFrameDecoder

服务端代码:

public class Server {
	public static void main(String[] args) throws Exception{
		//1 创建2个线程,一个是负责接收客户端的连接。一个是负责进行数据传输的
		EventLoopGroup pGroup = new NioEventLoopGroup();
		EventLoopGroup cGroup = new NioEventLoopGroup();
		//2 创建服务器辅助类
		ServerBootstrap b = new ServerBootstrap();
		b.group(pGroup, cGroup)
		 .channel(NioServerSocketChannel.class)
		 .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 sc) throws Exception {
				//设置特殊分隔符
				ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
				sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
				//设置字符串形式的解码
				sc.pipeline().addLast(new StringDecoder());
				sc.pipeline().addLast(new ServerHandler());
			}
		});
		//4 绑定连接
		ChannelFuture cf = b.bind(8765).sync();
		//等待服务器监听端口关闭
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
		cGroup.shutdownGracefully();	
	}	
}

客户端代码:

public class Client {
	public static void main(String[] args) throws Exception {	
		EventLoopGroup group = new NioEventLoopGroup();	
		Bootstrap b = new Bootstrap();
		b.group(group)
		 .channel(NioSocketChannel.class)
		 .handler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel sc) throws Exception {
				//
				ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
				sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
				sc.pipeline().addLast(new StringDecoder());
				sc.pipeline().addLast(new ClientHandler());
			}
		});		
		ChannelFuture cf = b.connect("127.0.0.1", 8765).sync();		
		for(int i = 1 ; i <=100 ; i ++){
			cf.channel().writeAndFlush(Unpooled.wrappedBuffer(("消息" + i + "$_").getBytes()));
		}		
		//等待客户端端口关闭
		cf.channel().closeFuture().sync();
		group.shutdownGracefully();
		
	}
}

3.3 消息头+消息体(自定义消息)

      将消息分为消息头与消息体,消息头中包含消息元数据,消息头一般有固定的格式,例如多少个字节表示消息体的长度,这样接收方就可以计算一条消息结束时的字节偏移量。