netty TCP 粘包拆包

一 什么是TCP 粘包拆包

TCP 协议是流数据,流数据的特点就是没有分界线;TCP 会将数据流 缓冲进 缓冲池,缓冲池对数据流进行推送;

缓冲池对数据发送有可能完整的2个包回黏在一起发送,称为粘包

缓冲池中有可能会对数据流进行拆包 发送数据,有可能数据包1中包含数据包2, 数据包2中包含数据包1;

二 粘包拆包产生的原因

  1. 发送的数据大于TCP发送缓冲区剩余空间大小,TCP会发生拆包。
  2. 发送数据大于MSS(最大报文长度),TCP会在传输前进行拆包。
  3. 发送数据远小于TCP缓冲区的大小,TCP将多次写入缓冲区的数据一次发送,将会发生粘包。
  4. 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

三 粘包拆包的解决方案

  • 在报文末尾增加换行符表明一条完整的消息,接收端可以根据换行符来判断消息是否完整。
  • 将消息分为消息头、消息体。可以在消息头中声明消息的长度,根据这个长度来获取报文(比如 808 协议)。
  • 规定好报文长度,不足的空位补齐,接收端取的时候按照长度截取

四 模拟粘包

我们在netty 入门应用中 改造 客户端的激活方法, 加入100个循环;

    /* *
     * @Author lsc
     * <p>触发回调 </p>
     * @Param [ctx]
     * @Return void
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i=0;i<100;i++){
            byte[] bytes = "关注公众号知识追寻者回复netty获取本教程源码".getBytes();
            // 创建节字缓冲区
            ByteBuf message = Unpooled.buffer(bytes.length);
            // 将数据写入缓冲区
            message.writeBytes(bytes);
            // 写入数据
            ctx.writeAndFlush(message);
        }

    }

然后我们服务端就接收到的消息如下,发现发送的数据都连接在了一起,即发生了数据包的粘包情况;

五 解决拆包粘包

LineBasedFrameDecoder 解码器

netty 中默认提高了多种节码器,和编码器;我们可以利用不同的编码,节码器进行解决粘包拆包的问题;

比如 利用 LineBasedFrameDecoder 进行粘包问题;

我们在服务端的通道初始化的时候加上编码器即可

 /**
     * @Author lsc
     * <p>通道初始化 </p>
     * @Param
     * @Return
     */
    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {

        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            // 管道(Pipeline)持有某个通道的全部处理器
            ChannelPipeline pipeline = socketChannel.pipeline();
            // 解决粘包问题
            pipeline.addLast(new LineBasedFrameDecoder(1024));
            pipeline.addLast(new StringDecoder());
            // 添加处理器
            pipeline.addLast(new NettyServerHandler());

        }
    }

当然我们在 客户端发送消息的时候就需要加上分割符号(知识追寻者这边以换行符号作为分割),要不然服务端没法接收消息

 /* *
     * @Author lsc
     * <p>触发回调 </p>
     * @Param [ctx]
     * @Return void
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i=0;i<100;i++){
            byte[] bytes = "关注公众号知识追寻者回复netty获取本教程源码\n".getBytes();
            // 创建节字缓冲区
            ByteBuf message = Unpooled.buffer(bytes.length);
            // 将数据写入缓冲区
            message.writeBytes(bytes);
            // 写入数据
            ctx.writeAndFlush(message);
        }

    }

最终打印效果如下

如果想在客户端也解决此类问题,加上解码器即可;

注:LineBasedFrameDecoder 支持 \n 或者 \n\r 进行解码;

DelimiterBasedFrameDecoder 解码器

DelimiterBasedFrameDecoder 解码器的应用和 LineBasedFrameDecoder 相似,不过 优点是我们可以自定义分割符号;

比如我们修改服务端的解码器为 DelimiterBasedFrameDecoder ,指定分割符合为 %

 /**
     * @Author lsc
     * <p>通道初始化 </p>
     * @Param
     * @Return
     */
    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {

        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            // 管道(Pipeline)持有某个通道的全部处理器
            ChannelPipeline pipeline = socketChannel.pipeline();
            pipeline.addLast(new DelimiterBasedFrameDecoder(10240, Unpooled.copiedBuffer("%".getBytes())));
            pipeline.addLast(new StringDecoder());
            // 添加处理器
            pipeline.addLast(new NettyServerHandler());

        }
    }

我们在客户端发送数据的时候以 % 为结尾;

  /* *
     * @Author lsc
     * <p>触发回调 </p>
     * @Param [ctx]
     * @Return void
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i=0;i<100;i++){
           
            byte[] bytes = "关注公众号知识追寻者回复netty获取本教程源码%".getBytes();
            // 创建节字缓冲区
            ByteBuf message = Unpooled.buffer(bytes.length);
            // 将数据写入缓冲区
            message.writeBytes(bytes);
            // 写入数据
            ctx.writeAndFlush(message);
        }

    }

FixedLengthFrameDecoder 解码器

FixedLengthFrameDecoder是按固定的数据长度来进行解码,我们就不用关系分割符号的问题,所以这种节码器也非常实用;

我们 需要精确的计算客户端发送的数据长度,然后在服务端解码器中配置,否则会出现乱码等情况;

/**
     * @Author lsc
     * <p>通道初始化 </p>
     * @Param
     * @Return
     */
    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {

        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            // 管道(Pipeline)持有某个通道的全部处理器
            ChannelPipeline pipeline = socketChannel.pipeline();
            pipeline.addLast(new FixedLengthFrameDecoder(63));
            pipeline.addLast(new StringDecoder());
            // 添加处理器
            pipeline.addLast(new NettyServerHandler());

        }
    }

效果如下

本套教程

posted @ 2021-03-10 17:36  知识追寻者  阅读(222)  评论(0编辑  收藏  举报