NettyはTCPスティッキーパケットとハーフパケットの問題をどのように解決しますか?

TCPスティッキーパケット、ハーフパケットNettyはすべて完了

スティッキーパックとハーフパックとは何ですか?

スティッキーバッグとは?

したがって、この名前は、クライアントとサーバー間で送信されるデータパケットが結合され、複数の部分で送信されるはずのデータパケットが結合されて送信されることを意味します。

ハーフパックとは何ですか?

これは、データパケットが複数の送信に分割されることを意味します。

TCPアプリケーションでスティッキーパケットとハーフパケットが発生するのはなぜですか?

粘着性のあるバッグの主な理由

  • 送信者は毎回データを書き込みます<ソケットバッファサイズ;
  • 受信者は、ソケットバッファデータをタイムリーに読み取れませんでした。

TCP使用されるNagleアルゴリズム(デフォルトで有効、手動で無効にできます):

  • Nagleこのアルゴリズムは、小さなセグメント(小さなパケット)を送信する問題に対処するために使用され、ネットワーク内の小さなデータパケットの数を減らすために特別に使用されます。
  • データパケットがMSSMSSデータパケット(その中のデータ)の最大データ送信制限値であり、dataこの値より大きい場合はセグメント化されます)より小さい場合、アルゴリズムはすべての類似データをグループ化しようとしますパケットをグループに分けます。データは同じパケットに送信されます。

送信者は通常、前のパケットの確認を受信した後にのみ次のデータパケットを送信するため、多数の小さなデータパケットを送信することは避けてください。

ハーフパックの主な理由

  • 送信者はデータを>ソケットバッファサイズに書き込みます(この時点でデータを分割する必要があります。そうしないと、バッファを書き込むことができません)。
  • 送信されるデータがプロトコルよりも大きい場合は、MTU(Maximum Transmission Unit,最大传输单元)解凍する必要があります。image-20220714160629456

主な理由

  1. TCPこれはストリーミングプロトコルであり、メッセージには境界がありません。
  2. UDP郵送された荷物と同様に、一度に複数の荷物が輸送されますが、各荷物には「境界」があり、1つのサインが届くので、粘着性のある荷物や半分の荷物の問題はありません。

スティッキーバッグとハーフバッグの問題を解決するためのいくつかの一般的な方法

いくつかの一般的な方法:

問題を解決するための基本的な手段:メッセージの境界を見つけます

image-20220714160005410

Nettyは、3つの一般的なフレームシーリング方法をサポートしています

image-20220714160049700

開梱と貼り付けの問題に対するNettyのソリューション

Nettyでのスティッキーパッケージの開梱処理

固定長データストリーム

クライアントとサーバーが相互に通信するとき、適切なメッセージの長さは事前に決定されます。たとえば、クライアントから送信されるデータの長さが10バイト未満の場合は、空白文字を使用して10バイトを構成します。

スペシャルエンドキャラクター

特殊文字に基づいてデータを分割します。

特別契約

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

解读 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

おすすめ

転載: juejin.im/post/7121178344225243144