【源码解析】netty的粘包拆包

569 阅读4分钟

一、什么是粘包拆包?

1、TCP的粘包拆包

TCP是个"流"协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题

2、TCP粘包拆包的问题如何解决?

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下

  1. 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格
  2. 在包尾增加回车换行符进行分割,例如FTP协议;利用分割符
  3. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度;例如http协议
  4. 更复杂的应用层协议

二、netty的粘包拆包

1、DelimiterBasedFrameDecoder

image
image
  1. 消息用指定分隔符进行分割来解决粘包拆包问题
  2. 在使用时需要将该decoder加入到pipeline中,并且需要放到自定义处理数据handler的前面,因为他是用来解析数据的,例如
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                // 指定连接队列大小
                .option(ChannelOption.SO_BACKLOG, 128)
                //KeepAlive
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                //Handler
                .childHandler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                        // DelimiterBasedFrameDecoder以分隔符来进行拆包粘包
                        channel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("$".getBytes())))
                                .addLast(new IdleStateHandler(5, 5, 10)).addLast(new NettyServerHandler());
                    }
                });

1.1 ByteToMessageDecoder

  1. 用来将字节消息转成字符串
  2. 核心逻辑是在chandleRead中,用来进行粘包拆包的处理

1.1.1 channelRead

  1. 先进行粘包处理,将上次没处理完的消息和本次新接受的消息进行合并处理
  2. 调用callDecode对信息进行分隔符处理
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // 只有ByteBuf对象才会对输入信息做处理
    if (msg instanceof ByteBuf) {
        // 存放分割信息的对象列表
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            // first表示是否是首次,这里的目的是为了处理某一次消息未发现分隔符的情况,也就是粘包的处理
            first = cumulation == null;
            cumulation = cumulator.cumulate(ctx.alloc(),
                    first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
            // 对输入信息进行decode
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
            throw e;
        } catch (Exception e) {
            throw new DecoderException(e);
        } finally {
            try {
                if (cumulation != null && !cumulation.isReadable()) {
                    numReads = 0;
                    cumulation.release();
                    cumulation = null;
                } else if (++numReads >= discardAfterReads) {
                    // We did enough reads already try to discard some bytes so we not risk to see a OOME.
                    // See https://github.com/netty/netty/issues/4275
                    numReads = 0;
                    discardSomeReadBytes();
                }

                int size = out.size();
                firedChannelRead |= out.insertSinceRecycled();
                fireChannelRead(ctx, out, size);
            } finally {
                out.recycle();
            }
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}

1.1.2 callDecode

  1. 循环处理多分割符情况
  2. 调用decodeRemovalReentryProtection方法处理分隔符
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        // 循环处理,可能存在多分隔符的情况,比如$为分隔符,消息为1$2$3$
        while (in.isReadable()) {
            int outSize = out.size();

            if (outSize > 0) {
                fireChannelRead(ctx, out, outSize);
                out.clear();

                // Check if this handler was removed before continuing with decoding.
                // If it was removed, it is not safe to continue to operate on the buffer.
                //
                // See:
                // - https://github.com/netty/netty/issues/4635
                if (ctx.isRemoved()) {
                    break;
                }
                outSize = 0;
            }

            int oldInputLength = in.readableBytes();
            // 真正处理decode的方法
            decodeRemovalReentryProtection(ctx, in, out);

            // Check if this handler was removed before continuing the loop.
            // If it was removed, it is not safe to continue to operate on the buffer.
            //
            // See https://github.com/netty/netty/issues/1664
            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 (Exception cause) {
        throw new DecoderException(cause);
    }
}

1.1.3 decodeRemovalReentryProtection

  1. 设置当前状态为调用子类decode(STATE_CALLING_CHILD_DECODE)
  2. 调用子类decode方法来进行处理
final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
            throws Exception {
    decodeState = STATE_CALLING_CHILD_DECODE;
    try {
        // 调用模版方法来进行decode
        decode(ctx, in, out);
    } finally {
        boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
        decodeState = STATE_INIT;
        if (removePending) {
            fireChannelRead(ctx, out, out.size());
            out.clear();
            handlerRemoved(ctx);
        }
    }
}

1.2 DelimiterBasedFrameDecoder

1.2.1 decode

  1. 如果用户设置的分隔符是换行符(\n 和\r\n),那么就会使用LineBasedFrameDecoder来进行decode(这部分的初始化在构造器中)
  2. 遍历所有分割副(可能会有34&),比如这里会找到$
  3. 如果找到了分隔符,那么会将分隔符前面的数据slice出来并添加到out列表中,例如这里的12
  4. 如果没找到分隔符,他会将这单消息保存起来和后面的消息进行合并处理,直到找到分隔符再触发后面的channelRead
  5. 处理过程中会有一些最大frame的判断,该字段在初始化时会设置,超过该大小就会丢弃消息
@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);
    }
}

/**
 * Create a frame out of the {@link ByteBuf} and return it.
 *
 * @param   ctx             the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
 * @param   buffer          the {@link ByteBuf} from which to read data
 * @return  frame           the {@link ByteBuf} which represent the frame or {@code null} if no frame could
 *                          be created.
 */
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
    // 判断是不是以换行符分割,时则使用换行处理器来进行处理,这个详见下面对lineBasedDecoder的解析
    if (lineBasedDecoder != null) {
        return lineBasedDecoder.decode(ctx, buffer);
    }
    // Try all delimiters and choose the delimiter which yields the shortest frame.
    // 找到最近的分隔符
    int minFrameLength = Integer.MAX_VALUE;
    ByteBuf minDelim = null;
    for (ByteBuf delim: delimiters) {
        int frameLength = indexOf(buffer, delim);
        if (frameLength >= 0 && frameLength < minFrameLength) {
            minFrameLength = frameLength;
            minDelim = delim;
        }
    }

    if (minDelim != null) {
        int minDelimLength = minDelim.capacity();
        ByteBuf frame;

        if (discardingTooLongFrame) {
            // We've just finished discarding a very large frame.
            // Go back to the initial state.
            discardingTooLongFrame = false;
            buffer.skipBytes(minFrameLength + minDelimLength);

            int tooLongFrameLength = this.tooLongFrameLength;
            this.tooLongFrameLength = 0;
            if (!failFast) {
                fail(tooLongFrameLength);
            }
            return null;
        }

        if (minFrameLength > maxFrameLength) {
            // Discard read frame.
            buffer.skipBytes(minFrameLength + minDelimLength);
            fail(minFrameLength);
            return null;
        }

        // 找到分隔符后将数据slice出来,并添加到out列表中
        if (stripDelimiter) {
            frame = buffer.readRetainedSlice(minFrameLength);
            buffer.skipBytes(minDelimLength);
        } else {
            frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
        }

        return frame;
    } else {
        if (!discardingTooLongFrame) {
            if (buffer.readableBytes() > maxFrameLength) {
                // Discard the content of the buffer until a delimiter is found.
                tooLongFrameLength = buffer.readableBytes();
                buffer.skipBytes(buffer.readableBytes());
                discardingTooLongFrame = true;
                if (failFast) {
                    fail(tooLongFrameLength);
                }
            }
        } else {
            // Still discarding the buffer since a delimiter is not found.
            tooLongFrameLength += buffer.readableBytes();
            buffer.skipBytes(buffer.readableBytes());
        }
        return null;
    }
}

2、LineBasedFrameDecoder

  1. 消息以换行符为分隔符来解决粘包拆包问题
  2. 原理和DelimiterBasedFrameDecoder差不多,这里不做详细解析

3、FixedLengthFrameDecoder

  1. 消息固定长度字节来解决粘包拆包问题
  2. 原理和DelimiterBasedFrameDecoder差不多,这里不做详细解析

4、LengthFieldBasedFrameDecoder

  1. 消息增加length字段来解决粘包拆包问题
  2. 这个实现非常简单,就是需要初始化LengthFieldBasedFrameDecoder时需要指定消息体中对应length字段对应的offset和length,这样就可以在根据lenght来进行消息处理