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

一、粘包出现的原因


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

二、案例展示


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

 1 package com.server;
 2 
 3 import io.netty.bootstrap.ServerBootstrap;
 4 import io.netty.channel.Channel;
 5 import io.netty.channel.ChannelFuture;
 6 import io.netty.channel.ChannelInitializer;
 7 import io.netty.channel.ChannelOption;
 8 import io.netty.channel.nio.NioEventLoopGroup;
 9 import io.netty.channel.socket.nio.NioServerSocketChannel;
10 import io.netty.handler.codec.string.StringDecoder;
11 import io.netty.handler.codec.string.StringEncoder;
12 
13 /**
14  * Netty5服务端
15  * @author zhengzx
16  *
17  */
18 public class ServerSocket {
19     public static void main(String[] args) {
20         
21         //创建服务类
22         ServerBootstrap serverBootstrap = new ServerBootstrap();
23         
24         //boss和worker
25         NioEventLoopGroup boss = new NioEventLoopGroup();
26         NioEventLoopGroup worker = new NioEventLoopGroup();
27         
28         try {
29             //设置线程池
30             serverBootstrap.group(boss,worker);
31             //设置socket工厂,Channel 是对 Java 底层 Socket 连接的抽象
32             serverBootstrap.channel(NioServerSocketChannel.class);
33             //设置管道工厂
34             serverBootstrap.childHandler(new ChannelInitializer<Channel>() {
35 
36                 @Override
37                 protected void initChannel(Channel ch) throws Exception {
38                     //设置后台转换器(二进制转换字符串)
39                     ch.pipeline().addLast(new StringDecoder());
40                     ch.pipeline().addLast(new StringEncoder());
41                     ch.pipeline().addLast(new ServerSocketHandler());
42                 }
43             });
44             
45             //设置TCP参数
46             serverBootstrap.option(ChannelOption.SO_BACKLOG, 2048);//连接缓冲池大小
47             serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);//维持连接的活跃,清除死连接
48             serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);//关闭超时连接
49             
50             ChannelFuture future = serverBootstrap.bind(10010);//绑定端口
51             System.out.println("服务端启动");
52             
53             //等待服务端关闭
54             future.channel().closeFuture().sync();
55         } catch (Exception e) {
56             e.printStackTrace();
57         } finally {
58             //释放资源
59             boss.shutdownGracefully();
60             worker.shutdownGracefully();
61         }
62         
63     }
64 }

【2】ServerSocketHandler处理类展示:

 1 package com.server;
 2 
 3 import io.netty.channel.ChannelHandlerContext;
 4 import io.netty.channel.SimpleChannelInboundHandler;
 5 
 6 public class ServerSocketHandler extends SimpleChannelInboundHandler<String>{
 7 
 8     @Override
 9     protected void messageReceived(ChannelHandlerContext ctx, String msg) throws Exception {
10         System.out.println(msg);
11     }
12     
13 }

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

 1 package com.client;
 2 
 3 import java.io.IOException;
 4 import java.net.Socket;
 5 import java.net.UnknownHostException;
 6 
 7 public class Client {
 8     public static void main(String[] args) throws UnknownHostException, IOException {
 9         //创建连接
10         Socket socket = new Socket("127.0.0.1", 10010);
11         //循环发送请求
12         for(int i=0;i<1000;i++){
13             socket.getOutputStream().write("hello".getBytes());
14         }    
15         //关闭连接
16         socket.close();
17     }
18 }

【4】打印结果。(正常情况应为一行一个hello打印)
   

三、分包


数据包数据被分开一部分发送出去,服务端一次读取数据时可能读取到完整数据包的一部分,剩余部分被第二次读取。具体情况如下图展示:
 

四、解决办法


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

 1 package com.server;
 2 
 3 import java.io.IOException;
 4 import java.net.Socket;
 5 import java.net.UnknownHostException;
 6 import java.nio.ByteBuffer;
 7 
 8 import org.omg.CORBA.PRIVATE_MEMBER;
 9 import org.omg.CORBA.PUBLIC_MEMBER;
10 
11 /**
12  * @category 通过长度+数据包的方式解决粘包分包问题
13  * @author zhengzx
14  *
15  */
16 public class Client {
17     //定义包头
18     public static int BAO = 24323455;
19     public static void main(String[] args) throws UnknownHostException, IOException {
20         //创建连接
21         Socket socket = new Socket("127.0.0.1", 10010);
22         //客户端发送的消息
23         String msg = "hello";
24         //获取消息的字节码
25         byte[] bytes = msg.getBytes();
26         //初始化buffer的长度:4+4表示包头长度+存放数据长度的整数的长度
27         ByteBuffer buffer = ByteBuffer.allocate(8+bytes.length);
28         //将长度和数据存入buffer中
29         buffer.putInt(BAO);
30         buffer.putInt(bytes.length);
31         buffer.put(bytes);
32         //获取缓冲区中的数据
33         byte[] array = buffer.array();
34         //循环发送请求
35         for(int i=0;i<1000;i++){
36             socket.getOutputStream().write(array);
37         }    
38         //关闭连接
39         socket.close();
40     }
41 }

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

 1 package com.server;
 2 
 3 import java.net.InetSocketAddress;
 4 import java.util.concurrent.ExecutorService;
 5 import java.util.concurrent.Executors;
 6 
 7 import org.jboss.netty.bootstrap.ServerBootstrap;
 8 import org.jboss.netty.channel.ChannelPipeline;
 9 import org.jboss.netty.channel.ChannelPipelineFactory;
10 import org.jboss.netty.channel.Channels;
11 import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
12 import org.jboss.netty.handler.codec.string.StringDecoder;
13 import org.jboss.netty.handler.codec.string.StringEncoder;
14 
15 public class Server {
16 
17     public static void main(String[] args) {
18         //服务类
19         ServerBootstrap bootstrap = new ServerBootstrap();
20         
21         //boss线程监听端口,worker线程负责数据读写
22         ExecutorService boss = Executors.newCachedThreadPool();
23         ExecutorService worker = Executors.newCachedThreadPool();
24         
25         //设置niosocket工厂
26         bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
27         
28         //设置管道的工厂
29         bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
30             
31             @Override
32             public ChannelPipeline getPipeline() throws Exception {
33                 ChannelPipeline pipeline = Channels.pipeline();
34                 pipeline.addLast("decoder", new MyDecoder());
35                 pipeline.addLast("handler1", new HelloHandler());
36                 return pipeline;
37             }
38         });
39         
40         bootstrap.bind(new InetSocketAddress(10101));
41         
42         System.out.println("start!!!");
43     }
44 
45 }

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

1 public static void fireMessageReceived(
2     ChannelHandlerContext ctx, Object message, SocketAddress remoteAddress) {
3     ctx.sendUpstream(new UpstreamMessageEvent(
4             ctx.getChannel(), message, remoteAddress));
5     }
6 }

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

 1 while (cumulation.readable()) {
 2             int oldReaderIndex = cumulation.readerIndex();
 3             Object frame = decode(context, channel, cumulation);
 4             if (frame == null) {
 5                 if (oldReaderIndex == cumulation.readerIndex()) {
 6                     // Seems like more data is required.
 7                     // Let us wait for the next notification.
 8                     break;
 9                 } else {
10                     // Previous data has been discarded.
11                     // Probably it is reading on.
12                     continue;
13                 }
14             }
15 }

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

 1 package com.server;
 2 
 3 import org.jboss.netty.buffer.ChannelBuffer;
 4 import org.jboss.netty.channel.Channel;
 5 import org.jboss.netty.channel.ChannelHandlerContext;
 6 import org.jboss.netty.handler.codec.frame.FrameDecoder;
 7 
 8 public class MyDecoder extends FrameDecoder{
 9 
10 
11     @Override
12     protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {
13         //buffer.readableBytes获取缓冲区中的数据 需要 大于基本长度
14         if(buffer.readableBytes() > 4) {
15             //防止socket攻击,当缓冲区数据大于2048时,清除数据。
16             if(buffer.readableBytes() > 2048) {
17                 buffer.skipBytes(buffer.readableBytes());
18             }
19             //循环获取包头,确定数据包的开始位置
20             while(true) {
21                 buffer.markReaderIndex();
22                 if(buffer.readInt() == Client.BAO) {
23                     break;
24                 }
25                 //只读取一个字节
26                 buffer.resetReaderIndex();
27                 buffer.readByte();
28                 
29                 if(buffer.readableBytes() < 4) {
30                     return null;
31                 }
32             }
33             //做标记
34             buffer.markReaderIndex();
35             //获取数据包的发送过来时的长度
36             int readInt = buffer.readInt();
37             //判断buffer中剩余的数据包长度是否大于单个数据包的长度(readInt)
38             if(buffer.readableBytes() < readInt) {
39                 //返回到上次做标记的地方,因为此次数据读取的不是一个完整的数据包。
40                 buffer.resetReaderIndex();
41                 //缓存当前数据,等待剩下数据包到来
42                 return null;
43             }
44             //定义一个数据包的长度
45             byte[] bt = new byte[readInt];
46             //读取数据
47             buffer.readBytes(bt);
48             //往下传递对象
49             return new String(bt);
50         }
51         //缓存当前数据包,等待第二次数据的到来
52         return null;
53     }
54 
55 }

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

 1 package com.server;
 2 
 3 import org.jboss.netty.channel.ChannelHandlerContext;
 4 import org.jboss.netty.channel.MessageEvent;
 5 import org.jboss.netty.channel.SimpleChannelHandler;
 6 
 7 public class HelloHandler extends SimpleChannelHandler {
 8     
 9     private int count = 1;
10 
11     @Override
12     public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
13         
14         System.out.println(e.getMessage() + "  " +count);
15         count++;
16     }
17 }

【5】结果展示(按顺序打印):
 

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


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

posted @ 2020-11-19 17:31  Java程序员进阶  阅读(226)  评论(0编辑  收藏  举报