03-Netty如何解决TCP粘包拆包

Netty中的粘包拆包

粘包拆包问题重现

关于什么是粘包拆包呢,我们先通过一个demo来看一下。

Server端代码

public class NettyServer {
    public static void main(String[] args) {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new NettyServerHandler());
                    }
                });

        System.out.println("netty server start...");
        ChannelFuture future;
        try {
            future = bootstrap.bind(8080).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

public class NettyServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(" ======> [server] received message: " + msg);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("=== " + ctx.channel().remoteAddress() + " is active ===");
    }
}

Client端代码

public class NettyClient {
    public static void main(String[] args) {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group).channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new NettyClientHandler());
                    }
                });
        System.out.println("netty client start...");

        try {
            bootstrap.connect("127.0.0.1", 8080).sync().channel();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }
}

public class NettyClientHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 100; i++) {
            ctx.writeAndFlush("hello world");
        }
    }
}

上面这段代码主要实现一个功能,启动服务端,然后在启动客户端,客户端启动后会循环向服务端发送100次hello world,然后服务端收到消息打印一下。那么我们看一下执行的结果会是什么样。执行后我们发现并没有像我们预期的一样,到一次收到消息打印一次hello world。而是每次收到的很没有规律,有时候一次收到三四个消息,有时候一连收到十几个消息,这个呢就叫粘包

 ======> [server] received message: hello worldhello world
 ======> [server] received message: hello worldhello worldhello worldhello world
 ======> [server] received message: hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world
 ======> [server] received message: hello worldhello world
 ======> [server] received message: hello world
 // 删去一部分

粘包拆包的原因

那么为什么会造成上面这种状况呢?主要是因为TCP协议是基于流传输的协议,数据在整个传输过程中是没有界限区分的,如果一个包过大的话,可能会被切成多个包进行分批传输,如果一个包过小的话,也会放到缓冲区,等到将好几个小包整合成一个大包进行传输。因此我们读取数据,往往不一定能获取到一个完整的预期的数据包。

粘包拆包的描述

上面我们浮现了一个粘包的现象,那么我们知道除了粘包,还有拆包。这里呢,我们通过几张图详细的解释一下粘包拆包。

  1. 这张图中客户端向服务端发送两个数据包:HelloWorld。服务端按照预期收到两个数据包HelloWorld。属于正常现象。
    在这里插入图片描述

  2. 这张图中客户端向服务端发送两个数据包:HelloWorld。然而服务端只收到一个数据包HelloWorld。两个本来要分开的包粘在了一起。就叫粘包。
    在这里插入图片描述

  3. 这张图中又分为出现两种情况。虽然两种情况服务端收到的数据不一样,但都有一个特点,那就是一个完整的包被拆成两部分,我们就叫做拆包

    1. 第一张图中客户端向服务端发送一个数据包:HelloWorld,然而服务端收到两个包:HelloWorld
    2. 第二张图中客户端向服务端发送一个数据包:HelloWorld,然而服务端收到两个包:HelloWorld
      在这里插入图片描述

粘包拆包的解决方案

Netty为了解决粘包拆包的问题,给我提供了很多方案,我们简单介绍常用的几种:

  1. 根据固定长度来分包

    上面的示例代码中我们常常收到多个数据包粘到一起的情况,那么如果我们在服务端限定每次收到的消息长度固定为11,如果收到超过11位,也只截取11位作为一个包,那么其实就可以解决粘包的问题。这种方案Netty已经为我们提供了一个Handler,就是FixedLengthFrameDecoder,它支持传入一个int类型的参数frameLength,如果出现粘包,那么我们根据frameLength对数据包进行拆分;如果出现拆包,则可以等待下一个数据包过来,然后根据frameLength对下一个包进行截取和上一个不完整的包进行拼接,直到得到一个完整的包。

    然后我们对上述示例代码进行改造,就可以解决粘包拆包的问题。

    public class NettyServer {
     public static void main(String[] args) {
         EventLoopGroup boss = new NioEventLoopGroup();
         EventLoopGroup worker = new NioEventLoopGroup();
    
         ServerBootstrap bootstrap = new ServerBootstrap();
         bootstrap.group(boss, worker)
                 .channel(NioServerSocketChannel.class)
                 .childHandler(new ChannelInitializer<SocketChannel>() {
                     @Override
                     protected void initChannel(SocketChannel ch) throws Exception {
                         ChannelPipeline pipeline = ch.pipeline();
                         // 通过FixedLengthFrameDecoder解决粘包拆包问题
                         pipeline.addLast(new FixedLengthFrameDecoder(11));
                         pipeline.addLast(new StringDecoder());
                         pipeline.addLast(new StringEncoder());
                         pipeline.addLast(new NettyServerHandler());
                     }
                 });
    
         System.out.println("netty server start...");
         ChannelFuture future;
         try {
             future = bootstrap.bind(8080).sync();
             future.channel().closeFuture().sync();
         } catch (InterruptedException e) {
             e.printStackTrace();
         } finally {
             boss.shutdownGracefully();
             worker.shutdownGracefully();
         }
     }
    }
    
  2. 特殊分隔符来分包

    除过上述的方案,我们还可以根据某一个特殊符号来进行分包,比如我们每次发送消息的时候在结尾多加一个自定义的特殊符号,那么服务端在收到数据包后,如果粘包,就根据自定义的特殊符号进行拆分;如果出现拆包,就等下一个数据包发过来拼接到特殊符号为止凑成一个完整的包,也可以解决粘包拆包。这种方案,针对这种方案,Netty为我们提供了DelimiterBasedFrameDecoder这个handler,他的最基础的构造函数如下:

    // maxFrameLength 最大的长度限制,超过这个长度,会抛异常,防止数据过大,导致内存溢出
    // delimiter就是我们那个自定义的特殊符号
    public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
        this(maxFrameLength, true, delimiter);
    }
    

    同样对上述示例代码进行改造,也可以解决粘包拆包的问题

    public class NettyServer {
    
        private static final String DELIMITER = "-";
    
        public static void main(String[] args) {
            EventLoopGroup boss = new NioEventLoopGroup();
            EventLoopGroup worker = new NioEventLoopGroup();
    
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            // DelimiterBasedFrameDecoder解决粘包拆包
                            pipeline.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.wrappedBuffer(DELIMITER.getBytes())));
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new StringEncoder());
                            // 自定义收到心跳包如何处理的handler
                            pipeline.addLast(new NettyServerHandler());
                        }
                    });
    
            System.out.println("netty server start...");
            ChannelFuture future;
            try {
                future = bootstrap.bind(8080).sync();
                future.channel().closeFuture().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                boss.shutdownGracefully();
                worker.shutdo
                    wnGracefully();
            }
        }
    }
    
  3. 自定义分包解码器解决粘包拆包

    上面的方式总是有局限性的,比如我们按照-这个特殊字符分包,那么假如我们本身发的消息内容就包括-,就会导致一个完整的包也会被拆分。所以我们还可以自定义编解码器去解决分包拆包。这里我们提供一种方案就是每次客户端发送消息的时候都把消息本身的内容和消息的长度都发送过去,这样在服务端解码的时候就可以根据长度来解析数据包。

    @Data
    public class MessageProtocol {
    
        // 消息长度
        private int length;
    
        // 消息内容
        private byte[] content;
    }
    
    public class MessageEncoder extends MessageToByteEncoder<MessageProtocol> {
    
        @Override
        protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
            out.writeInt(msg.getLength());
            out.writeBytes(msg.getContent());
        }
    }
    
    public class MessageDecoder extends ByteToMessageDecoder {
    
        int length = 0;
    
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
            if(in.readableBytes() >= 4) {
                // 先解析出消息的长度
                if (length == 0){
                    length = in.readInt();
                }
                // 在读取收到的内容,如果收到的内容长度小于length,说明发生了拆包,在等新的包发来
                if (in.readableBytes() < length) {
                    return;
                }
                // 走到这里说明完整的包已经收到了,按照根据长度读出内容
                byte[] content = new byte[length];
                if (in.readableBytes() >= length){
                    in.readBytes(content);
    
                    // 封装成MyMessageProtocol对象,传递到下一个handler业务处理
                    MessageProtocol messageProtocol = new MessageProtocol();
                    messageProtocol.setLength(length);
                    messageProtocol.setContent(content);
                    out.add(messageProtocol);
                }
                length = 0;
            }
        }
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半__夏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值