粘包与分包问题的出现及解决

目录

一、粘包出现的原因

二、案例展示

三、分包

四、解决办法


一、粘包出现的原因


服务端与客户端没有约定好要使用的数据结构。Socket Client实际是将数据包发送到一个缓存buffer中,通过buffer刷到数据链路层。因服务端接收数据包时,不能断定数据包1何时结束,就有可能出现数据包2的部分数据结合数据包1发送出去,导致服务器读取数据包1时包含了数据包2的数据。这种现象称为粘包。

二、案例展示


【1】服务端代码如下,具体注释说明

package com.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * Netty5服务端
 * @author zhengzx
 *
 */
public class ServerSocket {
	public static void main(String[] args) {
		
		//创建服务类
		ServerBootstrap serverBootstrap = new ServerBootstrap();
		
		//boss和worker
		NioEventLoopGroup boss = new NioEventLoopGroup();
		NioEventLoopGroup worker = new NioEventLoopGroup();
		
		try {
			//设置线程池
			serverBootstrap.group(boss,worker);
			//设置socket工厂,Channel 是对 Java 底层 Socket 连接的抽象
			serverBootstrap.channel(NioServerSocketChannel.class);
			//设置管道工厂
			serverBootstrap.childHandler(new ChannelInitializer() {

				@Override
				protected void initChannel(Channel ch) throws Exception {
					//设置后台转换器(二进制转换字符串)
					ch.pipeline().addLast(new StringDecoder());
					ch.pipeline().addLast(new StringEncoder());
					ch.pipeline().addLast(new ServerSocketHandler());
				}
			});
			
			//设置TCP参数
			serverBootstrap.option(ChannelOption.SO_BACKLOG, 2048);//连接缓冲池大小
			serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);//维持连接的活跃,清除死连接
			serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);//关闭超时连接
			
			ChannelFuture future = serverBootstrap.bind(10010);//绑定端口
			System.out.println("服务端启动");
			
			//等待服务端关闭
			future.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			//释放资源
			boss.shutdownGracefully();
			worker.shutdownGracefully();
		}
		
	}
}

【2】ServerSocketHandler处理类展示:

package com.server;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class ServerSocketHandler extends SimpleChannelInboundHandler{

	@Override
	protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
		System.out.println(msg);
	}
	
}

【3】客户端发送请求代码展示:

package com.client;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {
	public static void main(String[] args) throws UnknownHostException, IOException {
		//创建连接
		Socket socket = new Socket("127.0.0.1", 10010);
		//循环发送请求
		for(int i=0;i<1000;i++){
			socket.getOutputStream().write("hello".getBytes());
		}	
		//关闭连接
		socket.close();
	}
}

【4】打印结果。(正常情况应为一行一个hello打印)
         粘包与分包问题的出现及解决_第1张图片

三、分包


数据包数据被分开一部分发送出去,服务端一次读取数据时可能读取到完整数据包的一部分,剩余部分被第二次读取。具体情况如下图展示:
  粘包与分包问题的出现及解决_第2张图片

四、解决办法


方案一定义一个稳定的结构。
【1】包头+length+数据包:客户端代码展示:包头用来防止 socket攻击,length用来获取数据包的长度。

package com.server;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;

import org.omg.CORBA.PRIVATE_MEMBER;
import org.omg.CORBA.PUBLIC_MEMBER;

/**
 * @category 通过长度+数据包的方式解决粘包分包问题
 * @author zhengzx
 *
 */
public class Client {
	//定义包头
	public static int BAO = 24323455;
	public static void main(String[] args) throws UnknownHostException, IOException {
		//创建连接
		Socket socket = new Socket("127.0.0.1", 10010);
		//客户端发送的消息
		String msg = "hello";
		//获取消息的字节码
		byte[] bytes = msg.getBytes();
		//初始化buffer的长度:4+4表示包头长度+存放数据长度的整数的长度
		ByteBuffer buffer = ByteBuffer.allocate(8+bytes.length);
		//将长度和数据存入buffer中
		buffer.putInt(BAO);
		buffer.putInt(bytes.length);
		buffer.put(bytes);
		//获取缓冲区中的数据
		byte[] array = buffer.array();
		//循环发送请求
		for(int i=0;i<1000;i++){
			socket.getOutputStream().write(array);
		}	
		//关闭连接
		socket.close();
	}
}

【2】服务端:需要注意的是,添加了MyDecoder类,此类具体下面介绍

package com.server;

import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;

public class Server {

	public static void main(String[] args) {
		//服务类
		ServerBootstrap bootstrap = new ServerBootstrap();
		
		//boss线程监听端口,worker线程负责数据读写
		ExecutorService boss = Executors.newCachedThreadPool();
		ExecutorService worker = Executors.newCachedThreadPool();
		
		//设置niosocket工厂
		bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
		
		//设置管道的工厂
		bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
			
			@Override
			public ChannelPipeline getPipeline() throws Exception {
				ChannelPipeline pipeline = Channels.pipeline();
				pipeline.addLast("decoder", new MyDecoder());
				pipeline.addLast("handler1", new HelloHandler());
				return pipeline;
			}
		});
		
		bootstrap.bind(new InetSocketAddress(10101));
		
		System.out.println("start!!!");
	}

}

【3】MyDecode类:需要继承FrameDecoder类。此类中用ChannelBuffer缓存没有读取的数据包,等接收到第二次发送的数据包时,会将此数据包与缓存的数据包进行拼接处理。当return一个String时,FarmedDecoder通过判断返回类型,调用相应的sendUpStream(event)向下传递数据。源码展示:

public static void fireMessageReceived(
    ChannelHandlerContext ctx, Object message, SocketAddress remoteAddress) {
	ctx.sendUpstream(new UpstreamMessageEvent(
			ctx.getChannel(), message, remoteAddress));
    }

当返回null时,会进行break,不处理数据包中的数据,源码展示:

while (cumulation.readable()) {
            int oldReaderIndex = cumulation.readerIndex();
            Object frame = decode(context, channel, cumulation);
            if (frame == null) {
                if (oldReaderIndex == cumulation.readerIndex()) {
                    // Seems like more data is required.
                    // Let us wait for the next notification.
                    break;
                } else {
                    // Previous data has been discarded.
                    // Probably it is reading on.
                    continue;
                }
            }

我们自己写的MyDecoder类,代码展示:(包含socket攻击的校验)

package com.server;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;

public class MyDecoder extends FrameDecoder{


	@Override
	protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {
		//buffer.readableBytes获取缓冲区中的数据 需要 大于基本长度
		if(buffer.readableBytes() > 4) {
			//防止socket攻击,当缓冲区数据大于2048时,清除数据。
			if(buffer.readableBytes() > 2048) {
				buffer.skipBytes(buffer.readableBytes());
			}
			//循环获取包头,确定数据包的开始位置
			while(true) {
				buffer.markReaderIndex();
				if(buffer.readInt() == Client.BAO) {
					break;
				}
				//只读取一个字节
				buffer.resetReaderIndex();
				buffer.readByte();
				
				if(buffer.readableBytes() < 4) {
					return null;
				}
			}
			//做标记
			buffer.markReaderIndex();
			//获取数据包的发送过来时的长度
			int readInt = buffer.readInt();
			//判断buffer中剩余的数据包长度是否大于单个数据包的长度(readInt)
			if(buffer.readableBytes() < readInt) {
				//返回到上次做标记的地方,因为此次数据读取的不是一个完整的数据包。
				buffer.resetReaderIndex();
				//缓存当前数据,等待剩下数据包到来
				return null;
			}
			//定义一个数据包的长度
			byte[] bt = new byte[readInt];
			//读取数据
			buffer.readBytes(bt);
			//往下传递对象
			return new String(bt);
		}
		//缓存当前数据包,等待第二次数据的到来
		return null;
	}

}

【4】服务端,处理请求的handler。

package com.server;

import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

public class HelloHandler extends SimpleChannelHandler {
	
	private int count = 1;

	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
		
		System.out.println(e.getMessage() + "  " +count);
		count++;
	}
}

【5】结果展示(按顺序打印):
  粘包与分包问题的出现及解决_第3张图片

方案二在消息的尾部加一些特殊字符,那么在读取数据的时候,只要读到这个特殊字符,就认为已经可以截取一个完整的数据包了,这种情况在一定的业务情况下实用。


----架构师资料,关注公众号获取----

你可能感兴趣的:(Netty)