¿Cómo resuelve Netty el problema de los paquetes adhesivos y los medios paquetes de TCP?

Paquete adhesivo TCP, medio paquete Netty todo listo

¿Qué son los paquetes adhesivos y los medios paquetes?

¿Qué es una bolsa adhesiva?

Por lo tanto, el nombre implica que los paquetes de datos enviados entre el cliente y el servidor se unen, y los paquetes de datos que deberían haberse enviado en varias partes se unen y se envían juntos.

¿Qué es un medio paquete?

Significa que un paquete de datos se divide en múltiples transmisiones.

¿Por qué se producen paquetes pegajosos y medios paquetes en las aplicaciones TCP?

Razones principales de las bolsas adhesivas

  • El remitente escribe datos cada vez <tamaño de búfer de socket;
  • El receptor no leyó los datos del búfer del socket de manera oportuna.

TCPNagleAlgoritmo utilizado (habilitado por defecto, se puede deshabilitar manualmente):

  • NagleEl algoritmo se utiliza para tratar el problema del envío de pequeños segmentos (pequeños paquetes), y se utiliza especialmente para reducir la cantidad de pequeños paquetes de datos en la red.
  • Cuando un paquete de datos es menor que MSS( MSSes el valor límite máximo de transmisión de datos de un paquete de datos ( datalos datos que contiene), si es mayor que este valor, se segmentará), el algoritmo intentará agrupar todos los datos similares paquetes en grupos Los datos se envían para el mismo paquete.

Evite enviar una gran cantidad de paquetes de datos pequeños, ya que el remitente generalmente envía el siguiente paquete de datos solo después de recibir la confirmación del paquete anterior.

La razón principal del medio paquete.

  • El remitente escribe los datos> el tamaño del búfer del socket (los datos deben dividirse en este momento, de lo contrario, el búfer no se puede escribir).
  • Si los datos enviados son más grandes que el protocolo MTU(Maximum Transmission Unit,最大传输单元), se deben descomprimir.image-20220714160629456

razón central

  1. TCPEs un protocolo de transmisión y los mensajes no tienen límites.
  2. UDPAl igual que los paquetes enviados por correo, aunque se transportan varios paquetes a la vez, cada paquete tiene un "límite" y se recibe una señal, por lo que no hay problema con los paquetes pegajosos y los medios paquetes.

Varios métodos comunes para resolver el problema de la bolsa pegajosa y la media bolsa

Varios métodos comunes:

El medio fundamental para resolver el problema: encontrar el límite del mensaje .

image-20220714160005410

Netty admite tres métodos comunes de sellado de marcos

image-20220714160049700

La solución de Netty al problema de desempacar y pegar

Procesamiento de desempaquetado de paquetes adhesivos en Netty

Flujo de datos de longitud fija

Cuando el cliente y el servidor se comunican entre sí, la longitud del buen mensaje se determina de antemano. Por ejemplo, si la longitud de los datos enviados por el cliente es inferior a 10 bytes, utilice caracteres en blanco para completar los 10 bytes.

carácter final especial

Divida los datos en función de los caracteres especiales.

acuerdo especial

将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段。

解读 Netty 处理粘包、半包的源码

解码核心工作流程

三种类型的解码器都是继承自 ByteToMessageDecoder,所以我们可以从这个类入手。

(1)ByteToMessageDecoder 的类结构

image-20220717102943194

很明显,它继承了 ChannelInboundHandlerAdapter 类,这个类有个核心方法就是 channelRead(),负责处理数据;

(2)ByteToMessageDecoderchannelRead() 方法源码,解释包含在源码中了

// 参数 msg 就是我们的传输数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            // 转换为 Netty 的 ByteBuf
            ByteBuf data = (ByteBuf) msg;
            /**
             * cumulation 就是数据积累器
             * 判断 cumulation 是否为 null,如果为 null,说明是第一笔数据,直接复制给 cumulation 即可;
             * 如果不为 null,说明之前已经有数据被接收了,就追加到 cumulation 中。
             * 其实核心工作就是一个数据积累的过程。
  			 */
            first = cumulation == null;
            if (first) {
                cumulation = data;
            } else {
                // 追加到 cumulation
                cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
            }
            // 此时调用了 callDecode,请看下文
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
            throw e;
        } catch (Exception e) {
            throw new DecoderException(e);
        } finally {
            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);
            out.recycle();
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}

(3)callDecode() 方法源码(在 channelRead() 方法中调用):

// 参数 in 代表数据积累器
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        while (in.isReadable()) {
            // 此时肯定为 0,因为没有数据
            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 中时,不能执行完 handler remove 清理操作。
            //那 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);
    }
}

(4)decodeRemovalReentryProtection() 方法源码(在 callDecode() 方法中被调用):

    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) {
                handlerRemoved(ctx);
            }
        }
    }

上面有个核心逻辑,就是调用了 decode() 方法.

(5)decode() 方法如下:image-20220717105601805

我们发现这是一个抽象方法,它的实现类中有那三种解码器,所以不难知道,这就是解码的核心逻辑;

(5)我们选择其中一个解码器的 decode() 方法来分析,FixedLengthFrameDecoderdecode() 方法源码:

@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 方法调用
// 参数 in 表示被数据积累器收集到的数据
protected Object decode(
        @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    // 如果小于我们定义的固定长度,就不解码了,没有意义
    if (in.readableBytes() < frameLength) {
        return null;
    } else {
        // 如果大于等于我们定义的数据长度,说明可以解码
        // 方法就是根据我们定义的数据长度来解码就醒了
        return in.readRetainedSlice(frameLength);
    }
}

(6)这样就完成了数据解码的过程。

解码中两种数据积累器(Cumulator)的区别?

(1)在这里调用了数据积累器的方法

image-20220717110654156

(2)我们跟进去 cumulate() 方法,发现它有两种实现:

image-20220717110738532

(3)我们点进第二种的实现(默认实现,内存复制):

public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
    @Override
    public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
        try {
            final ByteBuf buffer;
            if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
                || cumulation.refCnt() > 1 || cumulation.isReadOnly()) { // 按需扩容
                // Expand cumulation (by replace it) when either there is not more room in the buffer
                // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or
                // duplicate().retain() or if its read-only.
                //
                // See:
                // - https://github.com/netty/netty/issues/2327
                // - https://github.com/netty/netty/issues/1764
                // 如果空间不够则扩容
                buffer = expandCumulation(alloc, cumulation, in.readableBytes());
            } else {
                buffer = cumulation;
            }
            // 将数据追加到数据积累器中,使用内存复制的方式
            buffer.writeBytes(in);
            return buffer;
        } finally {
            // We must release in in all cases as otherwise it may produce a leak if writeBytes(...) throw
            // for whatever release (for example because of OutOfMemoryError)
            in.release();
        }
    }
};

而另一种实现(组合的方式):

public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
    @Override
    public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
        ByteBuf buffer;
        try {
            if (cumulation.refCnt() > 1) {
                // Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the
                // user use slice().retain() or duplicate().retain().
                //
                // See:
                // - https://github.com/netty/netty/issues/2327
                // - https://github.com/netty/netty/issues/1764
                // 空间扩容
                buffer = expandCumulation(alloc, cumulation, in.readableBytes());
                buffer.writeBytes(in);
            } else {
                CompositeByteBuf composite;

                // 创建 composite bytebuf,如果已经创建过,就不用了
                if (cumulation instanceof CompositeByteBuf) {
                    composite = (CompositeByteBuf) cumulation;
                } else {
                    composite = alloc.compositeBuffer(Integer.MAX_VALUE);
                    composite.addComponent(true, cumulation);
                }
                // 避免内存复制
                composite.addComponent(true, in);
                in = null;
                buffer = composite;
            }
            return buffer;
        } finally {
            if (in != null) {
                // We must release if the ownership was not transferred as otherwise it may produce a leak if
                // writeBytes(...) throw for whatever release (for example because of OutOfMemoryError).
                in.release();
            }
        }
    }
};

(3)总结就是,两种数据积累器,一种使用的方式是内存复制(默认),而另一种使用的是组合的方式,提供一个逻辑的视图;

为什么会选择内存复制的方式,而不选择组合方式,明明组合的方式效率更高。

源码中注释已经解释了:image-20220717111811112

三种解码器的常用额外控制参数有哪些?

DelimiterBasedFrameDecoder

image-20220717111934933

这是一个数组,它不仅仅可以支持一个分隔符,还可以支持多个分隔符。

FixedLengthFrameDecoder

image-20220717112130911

自定义长度;

LengthFieldBasedFrameDecoder

image-20220717112359556

Supongo que te gusta

Origin juejin.im/post/7121178344225243144
Recomendado
Clasificación