Netty源码分析——拆包器之LineBasedFrameDecoder

基于Netty源代码版本:netty-all-4.1.33.Final

前言

什么是粘包、拆包
粘包、拆包是Socket编程中最常遇见的一个问题,本文来研究一下Netty是如何解决粘包、拆包的,首先我们从什么是粘包、拆包开始说起:

TCP是个"流"协议,所谓流,就是没有界限的一串数据,TCP底层并不了解上层业务的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上:

  • 一个完整的包可能会被TCP拆分为多个包进行发送(拆包)
  • 多个小的包也有可能被封装成一个大的包进行发送(粘包)
    这就是所谓的TCP粘包与拆包

下图演示了粘包、拆包的场景:


基本上有四种情况:

  • Data1、Data2都分开发送到了Server端,没有产生粘包与拆包的情况
  • Data1、Data2数据粘在了一起,打成了一个大的包发送到了Server端,这种情况就是粘包
  • Data1被分成Data1_1与Data1_2,Data1_1先到服务端,Data1_2与Data2再到服务端,这种情况就是拆包
  • Data2被分成Data2_1与Data2_2,Data1与Data2_1先到服务端,Data2_2再到服务端,同上,这也是一种拆包的场景

粘包、拆包产生的原因
上面我们详细了解了TCP粘包与拆包,那么粘包与拆包为什么会发生呢,大致上有三种原因:

  • 应用程序写入的字节大小大于Socket发送缓冲区大小
  • 进行MSS大小的TCP,MSS是最大报文段长度的缩写,是TCP报文段中的数据字段最大长度,MSS=TCP报文段长度-TCP首部长度
  • 以太网的Payload大于MTU,进行IP分片,MTU是最大传输单元的缩写,以太网的MTU为1500字节

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

  • 消息定长,例如每个报文的大小固定为200字节,如果不够空位补空格
  • 包尾增加回车换行符进行分割,例如FTP协议
  • 将消息分为消息头和消息体,消息头中包含表示长度的字段,通常涉及思路为消息头的第一个字段使用int32来表示消息的总长度
  • 更复杂的应用层协议

Netty中内置了多个编解码器,可以很简单的处理包界限问题。典型的几个:

  • LengthFieldBasedFrameDecoder
    通过在包头增加消息体长度的解码器,解析数据时首先获取首部长度,然后定长读取socket中的数据。
  • LineBasedFrameDecoder
    换行符解码器,报文尾部增加固定换行符rn,解析数据时以换行符作为报文结尾。
  • DelimiterBasedFrameDecoder
    分隔符解码器,使用特定分隔符作为报文的结尾,解析数据时以定义的分隔符作为报文结尾
  • FixedLengthFrameDecoder
    定长解码器,这个最简单,消息体固定长度,解析数据时按长度读取即可

本文介绍 LineBasedFrameDecoder,换行符解码器。

行拆包器

下面,以一个具体的例子来看看业netty自带的拆包器是如何来拆包的

这个类叫做 LineBasedFrameDecoder,基于行分隔符的拆包器,TA可以同时处理 \n以及\r\n两种类型的行分隔符,核心方法都在继承的 decode 方法中

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

netty 中自带的拆包器都是如上这种模板,我们来看看decode(ctx, in);

public class LineBasedFrameDecoder extends ByteToMessageDecoder {
    protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        final int eol = findEndOfLine(buffer);
        if (!discarding) {
            if (eol >= 0) {
                final ByteBuf frame;
                final int length = eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;

                if (length > maxLength) {
                    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 {
                final int length = buffer.readableBytes();
                if (length > maxLength) {
                    discardedBytes = length;
                    buffer.readerIndex(buffer.writerIndex());
                    discarding = true;
                    offset = 0;
                    if (failFast) {
                        fail(ctx, "over " + discardedBytes);
                    }
                }
                return null;
            }
        } else {
            if (eol >= 0) {
                final int length = discardedBytes + eol - buffer.readerIndex();
                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());
                // We skip everything in the buffer, we need to set the offset to 0 again.
                offset = 0;
            }
            return null;
        }
    }

    private int findEndOfLine(final ByteBuf buffer) {
        int totalLength = buffer.readableBytes();
        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
        if (i >= 0) {
            offset = 0;
            if (i > 0 && buffer.getByte(i - 1) == '\r') {
                i--;
            }
        } else {
            offset = totalLength;
        }
        return i;
    }
}

public interface ByteProcessor {
    /**
     * Aborts on a {@code LF ('\n')}.
     */
    ByteProcessor FIND_LF = new IndexOfProcessor(LINE_FEED);
}

找到换行符位置

public class LineBasedFrameDecoder extends ByteToMessageDecoder {
    protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        final int eol = findEndOfLine(buffer);
        ......
    }

    private int findEndOfLine(final ByteBuf buffer) {
        int totalLength = buffer.readableBytes();
        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
        if (i >= 0) {
            offset = 0;
            if (i > 0 && buffer.getByte(i - 1) == '\r') {
                i--;
            }
        } else {
            offset = totalLength;
        }
        return i;
    }
}

public interface ByteProcessor {
    /**
     * Aborts on a {@code LF ('\n')}.
     */
    ByteProcessor FIND_LF = new IndexOfProcessor(LINE_FEED);
}

for循环遍历,找到第一个 \n 的位置,如果\n前面的字符为\r,那就返回\r的位置

非discarding模式的处理
接下来,netty会判断,当前拆包是否属于丢弃模式,用一个成员变量来标识

/** 
 * True if we're discarding input because we're already over maxLength.  
 */
private boolean discarding;

第一次拆包不在discarding模式

非discarding模式下找到行分隔符的处理

if (!discarding) {
    if (eol >= 0) {
        // 1.计算分隔符和包长度
        final ByteBuf frame;
        final int length = eol - buffer.readerIndex();
        final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
        // 丢弃异常数据
        if (length > maxLength) {
            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;
    } 
}
  • 1、首先,新建一个帧,计算一下当前包的长度和分隔符的长度(因为有两种分隔符)
  • 2、然后判断一下需要拆包的长度是否大于该拆包器允许的最大长度(maxLength),这个参数在构造函数中被传递进来,如超出允许的最大长度,就将这段数据抛弃,返回null
  • 3、最后,将一个完整的数据包取出,如果构造本解包器的时候指定 stripDelimiter为false,即解析出来的包包含分隔符,默认为不包含分隔符

非discarding模式下未找到分隔符的处理

没有找到对应的行分隔符,说明字节容器没有足够的数据拼接成一个完整的业务数据包,进入如下流程处理

final int length = buffer.readableBytes();
if (length > maxLength) {
    discardedBytes = length;
    buffer.readerIndex(buffer.writerIndex());
    discarding = true;
    offset = 0;
    if (failFast) {
        fail(ctx, "over " + discardedBytes);
    }
}
return null;

首先取得当前字节容器的可读字节个数,接着,判断一下是否已经超过可允许的最大长度,如果没有超过,直接返回null,字节容器中的数据没有任何改变,否则,就需要进入丢弃模式

使用一个成员变量 discardedBytes 来表示已经丢弃了多少数据,然后将字节容器的读指针移到写指针,意味着丢弃这一部分数据,设置成员变量discarding为true表示当前处于丢弃模式。如果设置了failFast,那么直接抛出异常,默认情况下failFast为false,即安静得丢弃数据

discarding模式
如果解包的时候处在discarding模式,也会有两种情况发生

discarding模式下找到行分隔符

在discarding模式下,如果找到分隔符,那可以将分隔符之前的都丢弃掉

if (eol >= 0) {
    final int length = discardedBytes + eol - buffer.readerIndex();
    final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
    buffer.readerIndex(eol + delimLength);
    discardedBytes = 0;
    discarding = false;
    if (!failFast) {
        fail(ctx, length);
    }
}

计算出分隔符的长度之后,直接把分隔符之前的数据全部丢弃,当然丢弃的字符也包括分隔符,经过这么一次丢弃,后面就有可能是正常的数据包,下一次解包的时候就会进入正常的解包流程

discarding模式下未找到行分隔符

这种情况比较简单,因为当前还在丢弃模式,没有找到行分隔符意味着当前一个完整的数据包还没丢弃完,当前读取的数据是丢弃的一部分,所以直接丢弃

discardedBytes += buffer.readableBytes();
buffer.readerIndex(buffer.writerIndex());
// We skip everything in the buffer, we need to set the offset to 0 again.
offset = 0;

特定分隔符拆包

这个类叫做 DelimiterBasedFrameDecoder,可以传递给TA一个分隔符列表,数据包会按照分隔符列表进行拆分,读者可以完全根据行拆包器的思路去分析这个DelimiterBasedFrameDecoder

参考:
https://www.cnblogs.com/java-chen-hao/p/11445297.html

https://www.cnblogs.com/xrq730/p/8724391.html

你可能感兴趣的:(Netty源码分析——拆包器之LineBasedFrameDecoder)