netty粘包拆包之LineBasedFrameDecoder解码器

​目录

          一、背景简介

          二、应用

          三、源码


一、背景简介

LineBasedFrameDecoder基于回车换行符解码器,他能够按照我们输入的回车换行符(\r or \r\n)对接收到的消息进行解码,在服务端接收到信息进行解析的时候LineBasedFrameDecoder的构造方法需要传递一个参数maxLength,用来限制对接收到的数据进行解码的时候一次解码的最大长度。如果在解码的过程中对传递的数据搜索到了换行符,那么换行符之前的数据(readerindex之后,换行符之前)length会和maxLength进行比较,如果大于maxLength,则readerindex到本次搜索到的换行符之间的数据就会被丢弃。反之,如果在一次解码的过程中对传递的数据没有搜索到换行符,那么readerindex之后的数据(缓冲区中可读部分)就全部会被丢弃,也就是客户端在发送数据的时候如果没有键入换行符,则数据服务端是不会获取到的(实际上数据还是在ByteBuf缓冲区中的,只是没有被读取到)。

二、应用

下面是LineBasedFrameDecoder的一个简单小应用测试,模拟发送不同信息服务端使用LineBasedFrameDecoder解码出来的信息是什么样的。

服务端:

public class LineBasedFrameDecoderTestServer {
​
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                   .addLast(new LineBasedFrameDecoder(1024))
                                    .addLast(new ChannelInboundHandlerAdapter() {
                                        @Override
                                        public void channelRead(ChannelHandlerContext ctx, Object msg) {
                                            if (msg instanceof ByteBuf) {
                                                ByteBuf packet = (ByteBuf) msg;
                                                System.out.println(packet.toString(Charset.defaultCharset()));
                                            }
                                        }
                                    });
                        }
                    });
            ChannelFuture f = b.bind(9000).sync();
            System.out.println("Started LineBasedFrameDecoderTestServer...");
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

客户端:

public class LineBasedFrameDecoderTestClient {
​
    public static void main(String[] args) throws Exception {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast(new ChannelInboundHandlerAdapter() {
                                        public void channelActive(ChannelHandlerContext ctx) {
                                            ByteBuf byteBuf = Unpooled.buffer().writeBytes("ABCDEFG杀杀\r\n杀杀杀杀是\n".getBytes());
                                            ctx.writeAndFlush(byteBuf);
                                        }
                                    });
                        }
                    });
            ChannelFuture f = b.connect("127.0.0.1", 9000).sync();
            System.out.println("Started LineBasedFrameDecoderTestClient...");
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

这里LineBasedFrameDecoder构造方法中传递的参数maxLength为1024字节。

①、客户端发送的数据不带换行符

可以看到服务端并没有输出任何东西。
②、客户端发送的数据不带有\r or \r\n

③、最后一个换行符后还有数据。

如果最后一个换行符后还有数据,那么之后的数据都没有解析出来。

可以看到服务端会以\r or \r\n为分隔进行数据的解析输出。

三、源码

netty提供的各种解码器统一都继承了ByteToMessageDecoder,ByteToMessageDecoder负责读取字节流并转换成其他消息,而ByteToMessageDecoder又继承了ChannelInboundHandlerAdapter,而ChannelInboundHandlerAdapter正是我们刚才在测试数据的时候在客户端重写了他的方法channelActive(),使管道生效并且数据写入缓冲区并发送数据。

在客户端代码流程原理并不复杂,我们主要看服务端是怎么解码、获取到数据的。

当客户端通过Channel发送数据的时候,服务端会在ByteToMessageDecoder的channelRead方法接收到。我们以channelRead方法为入口看服务端是怎么进行消息解码的。

在channelRead方法中首先会判断消息msg是不是ByteBuf类型的:

①、如果msg不是ByteBuf类型的,直接调用ctx.fireChannelRead(msg)方法,fireChannelRead方法实际上是调用pipeline管道的下一个Handler去继续处理消息其他的逻辑去了,我们进入到AbstractChannelHandlerContext的fireChannelRead方法,发现只有一个方法invokeChannelRead,寻找下一个绑定的Handler并且调用ChannelRead方法。

    @Override
    public ChannelHandlerContext fireChannelRead(final Object msg) {
        //寻找下一个绑定的Handler并且调用ChannelRead方法
        invokeChannelRead(findContextInbound(), msg);
        return this;
    }

继续进入到invokeChannelRead。

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(() -> next.invokeChannelRead(m));
        }
    }

最终都会调用invokeChannelRead,而invokeChannelRead就是将Handler转换成ChannelInboundHandler类型去执行具体的channelRead方法,去进一步的读取消息并处理相关Handler逻辑。

   private void invokeChannelRead(Object msg) {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelRead(this, msg);
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireChannelRead(msg);
        }
    }

②、如果msg是ByteBuf类型的,去调用解码逻辑callDecode方法去解析数据。

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        try {
            while (in.isReadable()) {
                int outSize = out.size();
                //将out里的事件执行并且清空out
                if (outSize > 0) {
                    fireChannelRead(ctx, out, outSize);
                    out.clear();
                    // 在继续解码之前,请检查是否已删除此处理程序
                    // 如果已将其删除,则继续在缓冲区上操作是不安全的。
                    if (ctx.isRemoved()) {
                        break;
                    }
                    outSize = 0;
                }
                int oldInputLength = in.readableBytes();
                // 开始解析数据,如果解析出来数据,那么out的长度一定会改变
                decode(ctx, in, out);
​
                // 在继续循环之前,请检查是否已删除此处理程序。
                // 如果已将其删除,则继续在缓冲区上操作是不安全的。
                if (ctx.isRemoved()) {
                    break;
                }
                // 如果没有解析出来数据
                if (outSize == out.size()) {
                    if (oldInputLength == in.readableBytes()) {
                        break;
                    } else {
                        continue;
                    }
                }
​
                if (oldInputLength == in.readableBytes()) {
                    throw new DecoderException(
                            StringUtil.simpleClassName(getClass()) +
                            ".decode() did not read anything but decoded a message.");
                }
                if (isSingleDecode()) {
                    break;
                }
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable cause) {
            throw new DecoderException(cause);
        }
    }

在callDecode方法中,使用while循环去解析数据,这是因为一次发送的数据有可能包含多个换行符,对于循环中的每次解析,首先判断缓冲区中是否还有可读空间,有可读空间执行while循环体,第一次执行循环体的时候out肯定是没有数据的,也就是out的size为0,下面会调用解码逻辑decode(ctx, in, out)去进行数据的第一次解析,我们先不看decode(ctx, in, out)到底执行了什么逻辑,现在假设已经执行了一次decode(ctx, in, out)逻辑,也就是out里面有数据了,就是第一次解析出来的数据,现在out的size为1,现在进入outSize > 0逻辑判断中,可以看到这个if判断逻辑核心就是fireChannelRead(ctx, out, outSize),然后将out清空,size置位0,关于fireChannelRead方法,其实在上面刚才我们也说过了,就是调用下一个Handler处理器去执行下一步处理逻辑,我们再次跟进去。

static void fireChannelRead(ChannelHandlerContext ctx, List<Object> msgs, int numElements) {
        if (msgs instanceof CodecOutputList) {
            fireChannelRead(ctx, (CodecOutputList) msgs, numElements);
        } else {
            for (int i = 0; i < numElements; i++) {
                ctx.fireChannelRead(msgs.get(i));
            }
        }
    }

继续进入到fireChannelRead,来到AbstractChannelHandlerContext的fireChannelRead。

    @Override
    public ChannelHandlerContext fireChannelRead(final Object msg) {
        //寻找下一个绑定的Handler并且调用ChannelRead方法
        invokeChannelRead(findContextInbound(), msg);
        return this;
    }

继续往里走。

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(() -> next.invokeChannelRead(m));
        }
    }
​
    private void invokeChannelRead(Object msg) {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelRead(this, msg);
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireChannelRead(msg);
        }
    }

重点看((ChannelInboundHandler) handler()).channelRead(this, msg),这里将handler转成ChannelInboundHandler类型,执行他的channelRead方法,其实就是我们刚才服务端的重写的channelRead。

也就是说解析完一次数据时候立刻就去执行我们自定义的处理逻辑去了。当然在这里只是一个测试代码,如果真正我们自定义的处理逻辑比较复杂,比如涉及到操作数据库、磁盘IO等,可以考虑放入线程池中进行业务处理,避免造成解析数据缓慢,网络迟钝。

回到我们刚才while循环解析数据的逻辑,进入到decode(ctx, in, out)中。进入到其实现类LineBasedFrameDecoder中。

这里我们先看一下LineBasedFrameDecoder中核心属性。

    // ==========================关键属性=====================
    /** 我们解码的最大长度*/
    private final int maxLength;
    /** 一旦解码超过maxLength,是否触发快速失败机制 默认为false*/
    private final boolean failFast;
    /** 是否开启丢弃模式,默认false.*/
    private final boolean stripDelimiter;
    /** 是否开启丢弃模式,则为true.*/
    private boolean discarding;
    /** 丢弃字节的长度*/
    private int discardedBytes;

接着进入到decode方法中去。

    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }

decoded就是解析完的对象,如果这个对象不为空,就放到out中去,再看decode方法。

    protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        // 在buffer中搜索换行符
        final int eol = findEndOfLine(buffer);
        // 如果是非丢弃模式,正常往下执行
        if (!discarding) {
            // 如果缓冲区中存在换行符
            if (eol >= 0) {
                // frame用以保存下面读取到的数据
                final ByteBuf frame;
                // readerindex和eol之间就是可读范围
                final int length = eol - buffer.readerIndex();
                //标识\r or \r\n
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
                // 如果本次可读的范围超过了最大阈值,那么跳过这段数据
                if (length > maxLength) {
                    // 设置readerIndex,跳到该换行符之后
                    buffer.readerIndex(eol + delimLength);
                    // 抛出异常信息
                    fail(ctx, length);
                    return null;
                }
                // 最终读取的数据是否要跳过换行符
                if (stripDelimiter) {
                    //跳过
                    frame = buffer.readRetainedSlice(length);
                    buffer.skipBytes(delimLength);
                } else {
                    //不跳过
                    frame = buffer.readRetainedSlice(length + delimLength);
                }
                return frame;
            } else {
                // 如果在buffer中没有搜索到换行符
                final int length = buffer.readableBytes();
                // 且可读的范围超过了最大阈值,那么直接跳过这段
                if (length > maxLength) {
                    discardedBytes = length;
                    // 将readerindex移动到writerindex,表示这一段数据全部丢弃
                    buffer.readerIndex(buffer.writerIndex());
                    // 进入丢弃模式
                    discarding = true;
                    // 如果开启了快速失败机制,则触发
                    if (failFast) {
                        fail(ctx, "over " + discardedBytes);
                    }
                }
                return null;
            }
            // 如果是丢弃模式
        } else {
            // 如果搜索到换行符, 那么这一段直接丢弃
            if (eol >= 0) {
                // 计算丢弃长度
                final int length = discardedBytes + eol - buffer.readerIndex();
                //标识\r or \r\n
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
                // 直接跳到换行符之后
                buffer.readerIndex(eol + delimLength);
                // 丢弃模式转正常模式
                discardedBytes = 0;
                discarding = false;
                // 如果开启了快速失败机制,则触发
                if (!failFast) {
                    fail(ctx, length);
                }
            } else {
                // 如果还没有找到换行符, 那么这次的全部丢弃
                discardedBytes += buffer.readableBytes();
                buffer.readerIndex(buffer.writerIndex());
            }
            return null;
        }
    }

这部分代码是LineBasedFrameDecoder解析数据的核心逻辑,代码中注释已经比较清楚了,首先findEndOfLine(buffer)作用就是从缓冲区中搜索换行符,我们进去看其执行逻辑。

    private static int findEndOfLine(final ByteBuf buffer) {
        int i = buffer.forEachByte(ByteProcessor.FIND_LF);
        if (i > 0 && buffer.getByte(i - 1) == '\r') {
            i--;
        }
        return i;
    }

在这里buffer.forEachByte是从readerindex开始依次搜索寻找换行符,返回的i值就是换行符的下标(代表\n的位置)。下面对i进行判断,如果i大于零并且i代表\n的位置前面还有个\r,那么i的值就减一,也就是说i始终是指向一行的结束的下一个字符。

继续往forEachByte里走。

     @Override
    public int forEachByte(ByteProcessor processor) {
        ensureAccessible();
        try {
            return forEachByteAsc0(readerIndex, writerIndex, processor);
        } catch (Exception e) {
            PlatformDependent.throwException(e);
            return -1;
        }
    }
    
    private int forEachByteAsc0(int start, int end, ByteProcessor processor) throws Exception {
        for (; start < end; ++start) {
            if (!processor.process(_getByte(start))) {
                return start;
            }
        }
        return -1;
    }

到这里从应用的层面来看已经逻辑很清楚了,就是从readerIndex开始进行一次搜索到writerIndex看是否有换行符(\n),如果有,返回其在缓冲区中的位置。

好了,现在我们回到之前decode方法中调用findEndOfLine的地方,继续往下走,调用完findEndOfLine方法,返回的eol就是从readerindex开始第一次\n出现的位置,然后判断是否开启了丢弃模式,默认是没有开启的,所以接下来正常执行,判断缓冲区中是否存在换行符:

①、如果我们搜索到了换行符的话,readerindex和eol之间就是我们需要读取数据的范围,然后delimLength 标识\r or \r\n , 接下来判断如果本次可读的范围超过了最大阈值,那么跳过这段数据,其实就是设置readerIndex,跳到该换行符之后。

②、如果在buffer中没有搜索到换行符,可读的范围如果超过了最大阈值,那么直接跳过这段数据。将readerindex移动到writerindex,表示这一段数据全部丢弃。如果没有超过最大阈值,则直接返回null,读写索引位置都不变。

继续回到之前判断是否丢弃模式的逻辑,现在如果是丢弃模式,而且还没有搜索到换行符的话,那么认为发送的数据还不是一个完成的数据包,所有的数据都会被再次丢弃,反之如果搜索到换行符, 直接把分隔符之前的数据(也包括分隔符)全部丢弃,在此过程中丢弃模式转正常模式、如果开启了快速失败机制,则触发。

也就是说如果在某次数据传输过程中,传输的数据大于了阈值maxLength,而且传输的数据中没有换行符,那么就会进入丢弃模式,之后传输的数据如果一直没有换行符,那么数据就会一直被丢弃,直到出现了一次传输的数据中出现了换行符,直接把分隔符之前的数据(也包括分隔符)全部丢弃,丢弃之后后面再次发送的数据就有可能是正常的数据包,下一次解包的时候就会进入正常的解包流程那么这次出现了丢弃模式被解除。但是这里有个疑问,解除丢弃模式为什么要抛出异常信息,是为了作为解除丢弃模式的提示信息?

至此,服务端利用LineBasedFrameDecoder解码器解析数据的流程也就分析完了。

个人才疏学浅、信手涂鸦,netty框架更多模块解读相关源码持续更新中,感兴趣的朋友请移步至个人公众号,谢谢支持😜😜......

公众号:wenyixicodedog

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值