LengthFieldBasedFrameDecoder для анализа исходного кода Netty

Java задняя часть исходный код Netty

Принцип распаковки

Последний пост в блоге о принципе распаковкиТайна распаковщика анализа исходного кода nettyЭто было подробно объяснено в , вот краткое резюме: процесс распаковки Netty ничем не отличается от написания ручной распаковки сам по себе, он заключается в накоплении байтов в контейнер, чтобы определить, достигают ли текущие накопленные байтовые данные размера пакета. размер, когда он достигает размера пакета, он будет дизассемблирован, а затем передан обработчику декодирования службы верхнего уровня.

Причина, по которой распаковка netty может быть такой мощной, заключается в том, что netty абстрагируется от того, как распаковать конкретный файл.decodeметод, разные распаковщики реализуют разныеdecodeметод, вы можете реализовать распаковку различных протоколов

Эта статья про универсальный распаковщикLengthFieldBasedFrameDecoder, если вы все еще занимаетесь распаковкой человеческой плоти самостоятельно, вы могли бы также узнать об этом мощном распаковщике, потому что почти все бинарные протоколы, связанные с длиной, могут быть реализованы через TA.Давайте сначала посмотрим на его использование.

Использование длинфильзаSedFramedecoder.

1. Распаковка по длине

Paste_Image.png

Вышеупомянутый тип протокола пакета данных является более распространенным.Первые несколько байтов представляют длину пакета данных (исключая поле длины), за которыми следуют конкретные данные. После разборки пакет данных представляет собой полный пакет данных с полем длины (затем его можно передать декодеру прикладного уровня для декодирования) иLengthFieldBasedFrameDecoderТакое соглашение может быть реализовано

new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4);

в 1. Первый параметрmaxFrameLengthУказывает максимальную длину пакета.Если максимальная длина пакета превышена, netty выполнит специальную обработку, которая будет обсуждаться позже. 2. Второй параметр относится к смещению поля длиныlengthFieldOffsetЗдесь 0, что указывает на отсутствие смещения 3. Третий параметр относится к длине длины.lengthFieldLength, здесь 4, что указывает на то, что длина поля длины равна 4

2. Усечение и распаковка на основе длины

Если нашему декодеру прикладного уровня не нужно использовать поле длины, то мы хотим, чтобы netty выглядела так после распаковки

Paste_Image.png

Поле длины усечено, нам нужно только указать еще один параметр, чтобы добиться этого, этот параметр называетсяinitialBytesToStrip, указывающее, сколько байтов должно быть пропущено netty перед передачей его сервисному декодеру после получения полного пакета данных

new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4, 0, 4);

Значение первых трех параметров такое же, как и выше, четвертый параметр будет рассмотрен позже, а пятый параметр здесьinitialBytesToStrip, здесь 4, что означает, что после получения полного пакета данных первые четыре байта игнорируются, и декодер приложения получает пакет данных без поля длины

3. На основании распаковки длины смещения

Следующий двоичный протокол является более распространенным.Первые несколько фиксированных байтов представляют собой заголовок протокола, который обычно содержит некоторую метаинформацию, такую ​​как magicNumber и версию протокола, за которым следует поле длины, указывающее, сколько пакетов находится в теле.

Paste_Image.png

Нужно только настроить второй параметр на основе первого случая для достижения

new LengthFieldBasedFrameDecoder(Integer.MAX, 4, 4);

lengthFieldOffsetЭто 4, что означает, что поле длины только после пропуска 4 байтов.

4. Распаковка в зависимости от регулируемой длины

Иногда бинарные протоколы могут быть разработаны следующим образом.

Paste_Image.png

То есть поле длины находится впереди, а заголовок сзади.В таком случае, как настроить параметры для достижения нужного эффекта распаковки?

1. Поле длины в пакете указывает на отсутствие смещения фронта,lengthFieldOffset0 2. Длина поля длины 3, то есть.lengthFieldLengthна 3 2. Длина тела пакета, представленная полем длины, пропускает заголовок Здесь есть еще один параметр, называемыйlengthAdjustment, размер регулировки длины тела пакета, длина, представленная значением поля длины плюс это значение коррекции, представляет пакет с заголовком, здесь 12+2, заголовок и тело пакета занимают в общей сложности 14 байтов

Наконец, код реализован как

new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 3, 2, 0);

5. Усеченная распаковка на основе регулируемой длины смещения

Более извращенный бинарный протокол с двумя заголовками, такой как следующий

Paste_Image.png

После демонтажа,HDR1Отменить, домен длины отбрасывается, только второй заголовок и действующий пакет, в этом соглашении, общиеHDR1Он может представлять magicNumber, указывая, что приложение принимает только двоичные данные, начинающиеся с magicNumber, который больше используется в rpc.

Мы все еще можем добиться этого, установив параметры netty

1. Смещение поля длины равно 1, тогдаlengthFieldOffset1 2. Длина поля длины равна 2, тогдаlengthFieldLengthна 2 3. Длина тела пакета, представленная полем длины, пропускает HDR2, но при распаковке HDR2 также удаляется netty как часть тела пакета.Длина HDR2 равна 1, тогдаlengthAdjustment1 4.拆完之后,截掉了前面三个字节,那么initialBytesToStripна 3

Наконец, код реализован как

   new LengthFieldBasedFrameDecoder(Integer.MAX, 1, 2, 1, 3);

6. Усеченная распаковка на основе регулируемой длины мутации смещения

Все предыдущие поля длины представляют длину тела пакета без заголовка.Если значение, представленное полем длины, включает в себя длину всего пакета данных, например, следующая ситуация

Paste_Image.png

Значение поля длины равно 16, длина поля равна 2, длина HDR1 равна 1, длина HDR2 равна 1, длина тела пакета равна 12, 1+1+2+12=16, как задать параметры??

За исключением того, что значение поля длины отличается от предыдущего случая, остальные такие же, так как netty не понимает бизнес-ситуации, вам нужно сообщить netty, что после поля длины, сколько байтов может следовать, чтобы сформировать полный пакет данных, здесь, очевидно, 13 байт, а значение поля длины равно 16, поэтому вычитание 3 - это фактическая длина, необходимая для распаковки,lengthAdjustmentза -3

Шесть случаев здесь - это шесть типичных двоичных протоколов, которые поставляются с исходным кодом netty. Я считаю, что более 90% сценариев были рассмотрены. Если ваш протокол основан на длине, вы можете рассмотреть возможность его реализации без байтов, но напрямую Используйте его или унаследуйте его и внесите несколько простых изменений.

Реализация такого мощного распаковщика тоже очень элегантна.Давайте посмотрим, как реализована netty.

Анализ исходного кода LengthFieldBasedFrameDecoder

Конструктор

оLengthFieldBasedFrameDecoderконструктор, нам нужно только посмотреть на один

public LengthFieldBasedFrameDecoder(
        ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
        int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
    // 省略参数校验部分
    this.byteOrder = byteOrder;
    this.maxFrameLength = maxFrameLength;
    this.lengthFieldOffset = lengthFieldOffset;
    this.lengthFieldLength = lengthFieldLength;
    this.lengthAdjustment = lengthAdjustment;
    lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
    this.initialBytesToStrip = initialBytesToStrip;
    this.failFast = failFast;
}

Конструктор делает очень просто, просто сохраняет входящие параметры в поле.Большинство полей здесь было объяснено ранее, а оставшиеся несколько дополнительных пояснений даны ниже. 1.byteOrderУказывает, являются ли данные, представленные потоком байтов, прямым порядком байтов или прямым порядком байтов, который используется для чтения поля длины. 2.lengthFieldEndOffsetУказывает смещение во всем пакете первого байта, следующего сразу за полем длины. 3.failFast, если оно истинно, то это означает, что поле длины прочитано, и значение TA превышаетmaxFrameLength, просто бросаетTooLongFrameException, а false означает, что только когда байт, представленный значением поля длины, действительно прочитан, он выдастTooLongFrameException, по умолчанию установлен в true, рекомендуется не изменять, иначе может возникнуть переполнение памяти

Реализовать абстракцию распаковки

существуетТайна распаковщика анализа исходного кода netty, мы уже знаем, что конкретный протокол распаковки нужно только реализовать

void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) 

вinУказывает данные, которые не были дизассемблированы до сих пор, и пакет после дизассемблирования добавляется вoutВ этом списке пакет может быть передан

Первый уровень относительно прост в реализации

@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);
    }
}

Перегруженная защищенная функцияdecodeЧтобы выполнить настоящую распаковку, давайте проанализируем эту тяжеловесную функцию в трех частях.

Получить длину кадра

1. Получите размер пакета, который необходимо распаковать

// 如果当前可读字节还未达到长度长度域的偏移,那说明肯定是读不到长度域的,直接不读
if (in.readableBytes() < lengthFieldEndOffset) {
    return null;
}

// 拿到长度域的实际字节偏移 
int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
// 拿到实际的未调整过的包长度
long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);


// 如果拿到的长度为负数,直接跳过长度域并抛出异常
if (frameLength < 0) {
    in.skipBytes(lengthFieldEndOffset);
    throw new CorruptedFrameException(
            "negative pre-adjustment length field: " + frameLength);
}

// 调整包的长度,后面统一做拆分
frameLength += lengthAdjustment + lengthFieldEndOffset;

Вышеприведенный абзац имеет расширениеgetUnadjustedFrameLength, если значение значения, представленного вашим полем длины, не является обычным типом int, short и другими базовыми типами, вы можете переписать эту функцию

protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) {
        buf = buf.order(order);
        long frameLength;
        switch (length) {
        case 1:
            frameLength = buf.getUnsignedByte(offset);
            break;
        case 2:
            frameLength = buf.getUnsignedShort(offset);
            break;
        case 3:
            frameLength = buf.getUnsignedMedium(offset);
            break;
        case 4:
            frameLength = buf.getUnsignedInt(offset);
            break;
        case 8:
            frameLength = buf.getLong(offset);
            break;
        default:
            throw new DecoderException(
                    "unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)");
        }
        return frameLength;
    }

Например, хотя в некоторых полях странной длины 4 байта, таких как 0x1234, но значение ТА десятичное, то есть длина 1234 в десятичном виде, то перезапись этой функцией может реализовать распаковку поля странной длины

2. Проверка длины

// 整个数据包的长度还没有长度域长,直接抛出异常
if (frameLength < lengthFieldEndOffset) {
    in.skipBytes(lengthFieldEndOffset);
    throw new CorruptedFrameException(
            "Adjusted frame length (" + frameLength + ") is less " +
            "than lengthFieldEndOffset: " + lengthFieldEndOffset);
}

// 数据包长度超出最大包长度,进入丢弃模式
if (frameLength > maxFrameLength) {
    long discard = frameLength - in.readableBytes();
    tooLongFrameLength = frameLength;

    if (discard < 0) {
        // 当前可读字节已达到frameLength,直接跳过frameLength个字节,丢弃之后,后面有可能就是一个合法的数据包
        in.skipBytes((int) frameLength);
    } else {
        // 当前可读字节未达到frameLength,说明后面未读到的字节也需要丢弃,进入丢弃模式,先把当前累积的字节全部丢弃
        discardingTooLongFrame = true;
        // bytesToDiscard表示还需要丢弃多少字节
        bytesToDiscard = discard;
        in.skipBytes(in.readableBytes());
    }
    failIfNecessary(true);
    return null;
}

Наконец, позвонитеfailIfNecessaryОпределить, нужно ли выбрасывать исключение

private void failIfNecessary(boolean firstDetectionOfTooLongFrame) {
    // 不需要再丢弃后面的未读字节,就开始重置丢弃状态
    if (bytesToDiscard == 0) {
        long tooLongFrameLength = this.tooLongFrameLength;
        this.tooLongFrameLength = 0;
        discardingTooLongFrame = false;
        // 如果没有设置快速失败,或者设置了快速失败并且是第一次检测到大包错误,抛出异常,让handler去处理
        if (!failFast ||
            failFast && firstDetectionOfTooLongFrame) {
            fail(tooLongFrameLength);
        }
    } else {
        // 如果设置了快速失败,并且是第一次检测到打包错误,抛出异常,让handler去处理
        if (failFast && firstDetectionOfTooLongFrame) {
            fail(tooLongFrameLength);
        }
    }
}

Мы можем знать раньшеfailFastПо умолчанию true, и здесьfirstDetectionOfTooLongFrameверно, поэтому при первом обнаружении большого пакета обязательно будет выброшено исключение

Ниже приведен код, который выдает исключение

private void fail(long frameLength) {
    if (frameLength > 0) {
        throw new TooLongFrameException(
                        "Adjusted frame length exceeds " + maxFrameLength +
                        ": " + frameLength + " - discarded");
    } else {
        throw new TooLongFrameException(
                        "Adjusted frame length exceeds " + maxFrameLength +
                        " - discarding");
    }
}

Обработка режима сброса

Если читатель сталкивается с исходным кодом при чтении этой статьи, он обнаружит, чтоLengthFieldBasedFrameDecoder.decoderТакже есть фрагмент кода на входе в функцию, который я пропустил в нашем предыдущем анализе.Цель его размещения в этом подразделе — продолжить предыдущий подраздел и упростить понимание обработки режима отбрасывания.

if (discardingTooLongFrame) {
    long bytesToDiscard = this.bytesToDiscard;
    int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
    in.skipBytes(localBytesToDiscard);
    bytesToDiscard -= localBytesToDiscard;
    this.bytesToDiscard = bytesToDiscard;

    failIfNecessary(false);
}

Как и выше, если вы в настоящее время находитесь в режиме отбрасывания, сначала подсчитайте, сколько байтов нужно отбросить, возьмите минимальное значение текущих отбрасываемых байтов и читаемых байтов, а после отбрасывания введитеfailIfNecessary, по сравнению с этой функцией, по умолчанию она не будет продолжать генерировать исключения, а если установленоfailFastЕсли оно ложно, то после отбрасывания будет выброшено исключение, и читатели смогут его проанализировать самостоятельно.

пропустить указанную длину байта

После того, как обработка режима сброса и проверка длины пройдены, введите ссылку пропуска указанной длины байта

int frameLengthInt = (int) frameLength;
if (in.readableBytes() < frameLengthInt) {
    return null;
}

if (initialBytesToStrip > frameLengthInt) {
    in.skipBytes(frameLengthInt);
    throw new CorruptedFrameException(
            "Adjusted frame length (" + frameLength + ") is less " +
            "than initialBytesToStrip: " + initialBytesToStrip);
}
in.skipBytes(initialBytesToStrip);

Сначала проверьте, было ли прочитано достаточно байтов, если да, то перед извлечением полного пакета данных на следующем шаге вам необходимоinitialBytesToStripНастройка пропускать определенные байты (см. начало статьи), разумеется, пропущенные байты не могут быть больше длины пакета данных, иначе выкинетCorruptedFrameExceptionисключение

извлечь кадр

int readerIndex = in.readerIndex();
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
in.readerIndex(readerIndex + actualFrameLength);

return frame;

В конце концов, на самом деле очень просто извлечь пакет данных.Получите указатель чтения текущих накопленных данных, а затем получите фактическую длину пакета данных, который необходимо извлечь для извлечения.После извлечения переместите указатель чтения

protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
    return buffer.retainedSlice(index, length);
}

Процесс экстракции называется простоByteBufизretainedSliceAPI, у которого нет накладных расходов на копирование памяти

Взгляните на действительно извлекаемый пакет данных, входящие параметрыintПоэтому можно судить, что в кастомном протоколе, если у вас поле длины 8 байт, то первые четыре байта в принципе бесполезны.

Суммировать

1. Если вы используете netty, а бинарный протокол основан на длине, рассмотрите возможность использованияLengthFieldBasedFrameDecoderЧто ж, регулируя различные параметры, он точно удовлетворит ваши потребности. 2.LengthFieldBasedFrameDecoderРаспаковка включает в себя проверку допустимых параметров, обработку пакетов исключений и окончательный вызов.ByteBufизretainedSliceЧтобы добиться отсутствия памяти, КОПИРУЙТЕ распаковку

Если вы хотите систематически изучать Нетти, мой буклет«Введение и практика Netty: имитация системы обмена мгновенными сообщениями WeChat IM»могу помочь тебе

image.png

Если вы хотите систематически изучать принципы Netty, не пропустите мою серию видеороликов по анализу исходного кода Netty:Углубленный анализ чтения исходного кода Netty для Java

image.png
image.png
image.png
image.png