Принцип распаковки
Последний пост в блоге о принципе распаковкиТайна распаковщика анализа исходного кода nettyЭто было подробно объяснено в , вот краткое резюме: процесс распаковки Netty ничем не отличается от написания ручной распаковки сам по себе, он заключается в накоплении байтов в контейнер, чтобы определить, достигают ли текущие накопленные байтовые данные размера пакета. размер, когда он достигает размера пакета, он будет дизассемблирован, а затем передан обработчику декодирования службы верхнего уровня.
Причина, по которой распаковка netty может быть такой мощной, заключается в том, что netty абстрагируется от того, как распаковать конкретный файл.decode
метод, разные распаковщики реализуют разныеdecode
метод, вы можете реализовать распаковку различных протоколов
Эта статья про универсальный распаковщикLengthFieldBasedFrameDecoder
, если вы все еще занимаетесь распаковкой человеческой плоти самостоятельно, вы могли бы также узнать об этом мощном распаковщике, потому что почти все бинарные протоколы, связанные с длиной, могут быть реализованы через TA.Давайте сначала посмотрим на его использование.
Использование длинфильзаSedFramedecoder.
1. Распаковка по длине
Вышеупомянутый тип протокола пакета данных является более распространенным.Первые несколько байтов представляют длину пакета данных (исключая поле длины), за которыми следуют конкретные данные. После разборки пакет данных представляет собой полный пакет данных с полем длины (затем его можно передать декодеру прикладного уровня для декодирования) иLengthFieldBasedFrameDecoder
Такое соглашение может быть реализовано
new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4);
в
1. Первый параметрmaxFrameLength
Указывает максимальную длину пакета.Если максимальная длина пакета превышена, netty выполнит специальную обработку, которая будет обсуждаться позже.
2. Второй параметр относится к смещению поля длиныlengthFieldOffset
Здесь 0, что указывает на отсутствие смещения
3. Третий параметр относится к длине длины.lengthFieldLength
, здесь 4, что указывает на то, что длина поля длины равна 4
2. Усечение и распаковка на основе длины
Если нашему декодеру прикладного уровня не нужно использовать поле длины, то мы хотим, чтобы netty выглядела так после распаковки
Поле длины усечено, нам нужно только указать еще один параметр, чтобы добиться этого, этот параметр называетсяinitialBytesToStrip
, указывающее, сколько байтов должно быть пропущено netty перед передачей его сервисному декодеру после получения полного пакета данных
new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 4, 0, 4);
Значение первых трех параметров такое же, как и выше, четвертый параметр будет рассмотрен позже, а пятый параметр здесьinitialBytesToStrip
, здесь 4, что означает, что после получения полного пакета данных первые четыре байта игнорируются, и декодер приложения получает пакет данных без поля длины
3. На основании распаковки длины смещения
Следующий двоичный протокол является более распространенным.Первые несколько фиксированных байтов представляют собой заголовок протокола, который обычно содержит некоторую метаинформацию, такую как magicNumber и версию протокола, за которым следует поле длины, указывающее, сколько пакетов находится в теле.
Нужно только настроить второй параметр на основе первого случая для достижения
new LengthFieldBasedFrameDecoder(Integer.MAX, 4, 4);
lengthFieldOffset
Это 4, что означает, что поле длины только после пропуска 4 байтов.
4. Распаковка в зависимости от регулируемой длины
Иногда бинарные протоколы могут быть разработаны следующим образом.
То есть поле длины находится впереди, а заголовок сзади.В таком случае, как настроить параметры для достижения нужного эффекта распаковки?
1. Поле длины в пакете указывает на отсутствие смещения фронта,lengthFieldOffset
0
2. Длина поля длины 3, то есть.lengthFieldLength
на 3
2. Длина тела пакета, представленная полем длины, пропускает заголовок Здесь есть еще один параметр, называемыйlengthAdjustment
, размер регулировки длины тела пакета, длина, представленная значением поля длины плюс это значение коррекции, представляет пакет с заголовком, здесь 12+2, заголовок и тело пакета занимают в общей сложности 14 байтов
Наконец, код реализован как
new LengthFieldBasedFrameDecoder(Integer.MAX, 0, 3, 2, 0);
5. Усеченная распаковка на основе регулируемой длины смещения
Более извращенный бинарный протокол с двумя заголовками, такой как следующий
После демонтажа,HDR1
Отменить, домен длины отбрасывается, только второй заголовок и действующий пакет, в этом соглашении, общиеHDR1
Он может представлять magicNumber, указывая, что приложение принимает только двоичные данные, начинающиеся с magicNumber, который больше используется в rpc.
Мы все еще можем добиться этого, установив параметры netty
1. Смещение поля длины равно 1, тогдаlengthFieldOffset
1
2. Длина поля длины равна 2, тогдаlengthFieldLength
на 2
3. Длина тела пакета, представленная полем длины, пропускает HDR2, но при распаковке HDR2 также удаляется netty как часть тела пакета.Длина HDR2 равна 1, тогдаlengthAdjustment
1
4.拆完之后,截掉了前面三个字节,那么initialBytesToStrip
на 3
Наконец, код реализован как
new LengthFieldBasedFrameDecoder(Integer.MAX, 1, 2, 1, 3);
6. Усеченная распаковка на основе регулируемой длины мутации смещения
Все предыдущие поля длины представляют длину тела пакета без заголовка.Если значение, представленное полем длины, включает в себя длину всего пакета данных, например, следующая ситуация
Значение поля длины равно 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
изretainedSlice
API, у которого нет накладных расходов на копирование памяти
Взгляните на действительно извлекаемый пакет данных, входящие параметрыint
Поэтому можно судить, что в кастомном протоколе, если у вас поле длины 8 байт, то первые четыре байта в принципе бесполезны.
Суммировать
1. Если вы используете netty, а бинарный протокол основан на длине, рассмотрите возможность использованияLengthFieldBasedFrameDecoder
Что ж, регулируя различные параметры, он точно удовлетворит ваши потребности.
2.LengthFieldBasedFrameDecoder
Распаковка включает в себя проверку допустимых параметров, обработку пакетов исключений и окончательный вызов.ByteBuf
изretainedSlice
Чтобы добиться отсутствия памяти, КОПИРУЙТЕ распаковку
Если вы хотите систематически изучать Нетти, мой буклет«Введение и практика Netty: имитация системы обмена мгновенными сообщениями WeChat IM»могу помочь тебе
Если вы хотите систематически изучать принципы Netty, не пропустите мою серию видеороликов по анализу исходного кода Netty:Углубленный анализ чтения исходного кода Netty для Java