どのようにネッティーソースコード解析TCPスティックパッケージのシリーズ、問題解決の半分パック、およびネッティー

Fanger魏コードまたは次のスキャンの公共マイクロチャンネル番号を検索するには菜鸟飞呀飞、あなたは、マイクロチャネルのパブリック番号に焦点を当てるより読むことができるSpring源码分析、とJava并发编程の記事。

マイクロチャンネル公衆数

問題

新しい接続へのアクセスは、以前の記事で読み取りと書き込みのデータ操作を開始することができたときにネッティーはその後、サーバは新しい接続にアクセスしているかを分析します。データ中に、TCPコネクションの目的のために、網状ニーズがTCPスティックパッケージに対処するために、この記事の内容今日の分析の焦点になる質問の半分のパックを、読み取り操作と書き込み。あなたがこの記事を読み始める前に、次の2つの質問を検討することができます。

    1. 半分パック問題TCPスティックパッケージは、何ですか?半分パックスティックパッケージUDPプロトコルの存在?
    1. どのように解決されネッティー

スティックパッケージ開梱とは何ですか

TCP / IPモデルでは、TCPおよびUDPプロトコルは、トランスポート層プロトコル、データ送信処理の存在下で2つのプロトコル間の大きな違いです。

データパケットに基づいて行われるデータの送受信を送信するUDPプロトコルのために、UDPプロトコルヘッダは、16ビットのフィールドは、UDPデータパケットの長さを示すが存在するであろう、ウェル内のアプリケーション層は異なるであろう別個のデータメッセージ領域。データ伝送プロトコルUDPが境界であるとして、それはパッケージがくっつかないように、それは、半分パックの問題が存在する、理解することができます。

ストリーミングバイトの送信データに基づいてTCPプロトコルのための間。アプリケーション層のデータ伝送における問題点は、実際には、TCPへの最初の書き込みデータは、半分のパックをスティックパッケージを引き起こす可能性がある、バッファがいっぱいになると、データが書き出されるバッファソケットう。受信者は、実際に受信したデータがバイトのストリームで受信した場合と、いわゆるフローは、同じ川のように理解することができます。受信者は、バイトストリームから読み取るように、それは複数のデータパケットの間に流れ各境界、およびTCPプロトコルヘッダ、パケット長を表すために別個のフィールドは、アプリケーション層ではないのでデータを取得した後に、データパケットの2つの領域を分離する方法はありません。

スティック包装、半概略パッケージ

送信側は受信側には、2つの連続した完全なデータ・パケットを送信すると、送信のためのTCPプロトコルは、次のような状況が存在する可能性があります。そして、パケット1 packet2図下は、送信者によって送信された2つの完全なデータパケットを表します。

ハーフパック現象、すなわち、受信機は、通常、二つの独立した完全なパケットのパケット1、packet2を受信する第1のケースは、スティックパッケージが発生していない、この状況は正常です。図1に示します。

通常のパッケージ

後者の場合はスティックパック現象を、発生した、送信者が後にバッファソケット自身のTCPパケットにパケット1のデータを書き込み、TCPはすぐに、データを送信しないので、バッファは遅いかもしれません。そして、送信者がTCPソケットバッファを書くには、バッファがいっぱいになった場合、TCPはデータのバッファで発送させていただきます、まだ、パケットpacket2を送信した、この時間は、受信者が受信しますデータは、1つのデータパケットのみに表示されます。TCPプロトコルヘッダに、スティック・パッケージの問題と呼ばれている受信機は、単にパケット1とpacket2を区別することができないように、データパケットの長さを示すために別のフィールド、無し。TCPソケットからデータを読み込むための時間がないので、受信機のTCP層は、アプリケーション層のデータを受信したときにまた、それはまた、現象スティックパッケージを引き起こす可能性があります。図に示すように。

スティックパッケージ

第三の場合、現象が半分パックを発生し、すなわち、送信者がまだ送信された2つのデータパケットを有し、パケット1 packet2が、TCP送信、数分の送信において、各送信の内容が含まれていないパケット1 packet2完全なパッケージ、または一部のみパケット1 packet2、コンテンツは、2個のデータパケットの分割に相当し、したがって現象がアンパックと呼ばれます。図3は、図2に示します。

ハーフパック

原因スティックパッケージ、半分パック

上記の図から、我々は主に半分パックとして実質的に固執パッケージ農産物を知ることができます。

  • 理由スティックパッケージ
      1. 各書き込みデータ送信側は、ソケットバッファのサイズよりも小さいです。
      1. 受信者データソケットバッファを読むことは十分な時間ではありません。
  • ハーフパックの理由
      1. データ送信側の書き込みはソケットバッファのサイズよりも大きいです。
      1. それはアンパックであると、MSSまたはMTUのデータ伝送プロトコルよりも大きいです。MSSは、TCP層の最大セグメントサイズである(TCP層は、IP層にデータを送信し、この値を超えることができない。MTUは、最大伝送単位である物理層は、最大送信データサイズに上位層に提供され、データは、IP層を制限するために使用しました転送サイズ)。

TCPは、バイト指向のデータ伝送、正確に個々のデータ・パケットを区別することができない受信機が得られ、互いへのパケットの間には境界が、あるので、しかし、最終的には、農産物スティックパッケージは、パッケージには、半分の根本的な原因です。

問題のネッティーを開梱スティックパッケージを解決する方法

我々はに似た独自のTCPプロトコルを持っていない限り、アプリケーション層の開発者として、我々は、データのTCPバイトストリーム伝送の特性に基づいて変更することはできませんが、既存のよりもはるかに難しく、必ずしも設計されていないパフォーマンスTCPプロトコルのパフォーマンスは、ほかに現在、TCPプロトコルが広く使用されている使用しています。その後、高性能ネットワークの枠組みとしてネッティー、必然的に、TCPプロトコルをサポートするので、それはスティックのパッケージTCPを解決しなければならない、問題の半分パックTCPプロトコルのサポートがなければならない、または開発者が独自に解決する場合は、時間がかかり、骨の折れます。

ネッティーTCPは、名前が示すようにTCPは一定のルール、エンコードを通じてソケットストリームから読み込まれたバイトを通じて、コーデックは、コーデックのシリーズを提供することで、半分パックの問題をスティックパッケージに対処しますまたはデコードし、または完全なパケットを解析し、バイナリバイトストリームにエンコード。網状デコーダのために、彼らは、抽象クラスを継承し、多くの一般的なコーデックに設けByteToMessageDecoder ;継承抽象クラスであるエンコーダ用MessageToByteEncoder

抽象クラスデコーダ今日のメイン解析簡単にByteToMessageDecoder特定のデコーダの実装のためのソースカテゴリは、エンコーダのために、詳細に2つの記事の背後にある原理を分析する、符号化プロセス及びデコーディングプロセスは正反対であるためではありません一人でそれらを繰り返す、その後、興味を持っている友人は、自分の、してください共有に読むことができます。

ByteToMessageDecoder実際ChannelHandler、その実装クラスが動作するパイプラインに追加する必要があります。場合があり、デコーダにパイプラインに追加するときOP_READのイベントハンドラは、すべての伝播のパイプラインで実行されるchannelRead()メソッド。抽象クラスデコーダにおいて、定義channelRead()メソッド。そのソースコードは次の通り。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        // 用来存放解码出来的数据对象,可以将它当做一个集合
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            ByteBuf data = (ByteBuf) msg;
            first = cumulation == null;
            if (first) {
                //第一次直接赋值
                cumulation = data;
            } else {
                //累加数据
                cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
            }
            // 调用解码的方法
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
            throw e;
        } catch (Exception e) {
            throw new DecoderException(e);
        } finally {
            // 省略部分代码...

            // size是解码出来的数据对象的数量,
            int size = out.size();
            decodeWasNull = !out.insertSinceRecycled();
            // 向下传播,如果size为0,就表示没有解码出一个对象,因此不会向下传播,而是等到下一次继续读到数据后解码
            fireChannelRead(ctx, out, size);
            out.recycle();
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}
复制代码

デコーダのコアロジックにおける網状バイトストリームデータを累積することにより、アキュムレータを読み取ることで、データオブジェクトが復号された場合、蓄積されたデータを復号する復号器の実装を呼び出し、読み出し手段完全なパケットは、その後、デコードされたデータオブジェクトがパイプラインを下に伝播し、ビジネスロジックの背後を実行するためのサービスコードに言及しました。データオブジェクトをデコードすることはできませんが、それは決して完全なパケットを読んでいることを示していない場合は、下に広がるが、データを読み取るために待機して継続していない、データが蓄積されるまでのバイトストリームを蓄積し続けデータは、データオブジェクトを復号化し、次に下方に移動することができます。

三つの重要な点がある:第一に、バイトストリームデータアキュムレータは、第二:特定は、このステップは特定のデコーダの実装クラスで行われ、操作をデコードする;第三:デコードされたデータオブジェクトがデコーダは、バイトストリームのデータ・オブジェクトのデータを復号化した後、具体的には、オブジェクトが中に格納されるリスト、及び、データオブジェクトは、パイプラインを下に伝搬します。次に、我々はこれらの次のステップを分析する上でのソースコードを兼ね備えています。

まず決定MSGをするかどうかByteBufの直接データオブジェクトは下向きに移動するオブジェクトの型が、この時に復号されたデータのバイトストリームをデコードする必要がある、他のロジックを入力するようにしていないので、タイプByteBufこと、及びません。 。バイトストリームデータは、この時点で復号化されていないため、それは、このようにロジックは復号されたブロックに進む場合、ByteBuf MSGタイプです。

ロジックは、最初に定義されている場合アウトオブジェクトを、オブジェクトは、単純に、すなわち正常に復号されたデータオブジェクトを格納するために使用されるオブジェクトの収集、上記第三の点としてそれを置くことができリスト次に遭遇する集計を、それがバイトストリームデータアキュムレータ、デフォルト値であり、このオブジェクトMERGE_CUMULATORそれはそれが蓄積正面を示していない、空の場合、データは最初の読み出しは、あるかどうかを知っているので、それが空であるかどうかを決定することにより、データ、こうして直接MSG等しい累積全てMSGアキュムレータの累積に蓄積された現在のバイトストリームデータという意味で、アキュムレータが空でない場合、アキュムレータの前部の存在を示すデータ(TCPパケット現象の前半が発生)、現在アキュムレータMSGに蓄積されたバイト・ストリーム・データを読み出す必要があります。

どのようにアキュムレータそれに蓄積されたバイトストリームデータへ?それが呼び出さアキュムレータあるCUMULATE()メソッド、ここで戦略のデザインモードは、デフォルトのアキュムレータはMERGE_CUMULATOR次のように、そのソースコードです。

public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
    try {
        final ByteBuf buffer;
        // 如果因为空间满了写不了本次的新数据 就扩容
        // cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes() 可以装换为如下:
        // cumulation.writeIndex() + in.readableBytes()>cumulation.maxCapacity
        // 即 写指针的位置+可读的数据的长度,如果超过了ByteBuf的最大长度,那么就需要扩容
        if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
                || cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
            // 扩容
            buffer = expandCumulation(alloc, cumulation, in.readableBytes());
        } else {
            buffer = cumulation;
        }
        // 将新数据写入
        buffer.writeBytes(in);
        return buffer;
    } finally {
        // 释放内存,防止OOM
        in.release();
    }
}
复制代码

まず、アキュムレータに蓄積されたデータは、容量が、呼に必要とされる場合に容量が、必要とされるか否かを判断する前に、コードに見ることができるexpandCumulation()最初の拡張のための方法。最後の呼び出しのwriteBytes()メソッドは、アキュムレータにデータを書き込むと、アキュムレータ復帰。ここで、アキュムレータによる膨張の方法に関することであるMERGE_CUMULATORは、従って、基礎となるメモリコピーが行われます。:網状でもアキュムレータの別のタイプを提供COMPOSITE_CUMULATOR、それは拡張メモリコピー時間を必要としないが、ByteBufの組み合わせ、すなわち、によってCompositeByteBufのクラス拡張を達成します。

質問ですが、明らかに基づき、メモリコピー操作がより緩やかになり、そのネッティーなぜアキュムレータメモリベースのレプリケーションデフォルトでは、それはそう?次のように内側にネッティーソースは説明しました:

/**
 * Cumulate {@link ByteBuf}s by add them to a {@link CompositeByteBuf} and so do no memory copy whenever possible.
 * Be aware that {@link CompositeByteBuf} use a more complex indexing implementation so depending on your use-case
 * and the decoder implementation this may be slower then just use the {@link #MERGE_CUMULATOR}.
 */
复制代码

一般的にすることを意味します。アキュムレータが唯一の蓄積されたデータで、特定のデコード処理は、抽象クラスはどのようにデコード特定のデコーダの実装クラスを実装し、デコーダは、デコーダの実装クラスのために、行うには、この時点で、我々が知らないということです、それに基づいてもよいCompositeByteBufのデータ構造の種類、必ずしも速くByteBufを使用するよりも、効率が必ずしも直接使用に高いので、網状デフォルトではない、それは遅くなりデコードMERGE_CUMULATORアキュムレータベースのメモリコピーです。

アキュムレータにデータを蓄積したときに、呼び出されcallDecode(CTX、累積、アウト)復号します。その合理化源は次の通り。

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        // 只要有数据可读,就循环读取数据
        while (in.isReadable()) {
            int outSize = out.size();
            // 如果out中有对象,这说明已经解码出一个数据对象了,可以向下传播了
            if (outSize > 0) {
                // 向下传播,并清空out
                fireChannelRead(ctx, out, outSize);
                out.clear();
                if (ctx.isRemoved()) {
                    break;
                }
                outSize = 0;
            }
            // 记录下解码之前的可读字节数
            int oldInputLength = in.readableBytes();
            // 调用解码的方法
            decodeRemovalReentryProtection(ctx, in, out);
            if (ctx.isRemoved()) {
                break;
            }
            // 如果解码前后,out中对象的数量没变,这表明没有解码出新的对象
            if (outSize == out.size()) {
                // 当没解码出新的对象时,累计器中可读的字节数在解码前后也没变,说明本次while循环读到的数据,
                // 不够解码出一个对象,因此中断循环,等待下一次读到数据
                if (oldInputLength == in.readableBytes()) {
                    break;
                } else {
                    continue;
                }
            }
            // out中的对象数量变了,说明解码除了新的对象,但是解码前后,累计器中的可读数据并没有变化,这表示出现了异常
            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);
    }
}
复制代码

方法の2番目のパラメータ、我々は前述のアキュムレータすなわち、第3のパラメータOUTは、正常に復号前述のデータ・オブジェクトに格納されています。この方法は、コアコードが一列に復号化され、上記のコード内のロジックのコメントを参照することができます。

// 调用解码的方法
decodeRemovalReentryProtection(ctx, in, out);
复制代码

このメソッドのソースコード。そのコードでは、それはデコード()メソッドサブクラスデコーダを呼び出すために真となり、データを復号します。

final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
        throws Exception {
    decodeState = STATE_CALLING_CHILD_DECODE;
    try {
        //调用子类的解码方法
        decode(ctx, in, out);
    } finally {
        boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
        decodeState = STATE_INIT;
        if (removePending) {
            handlerRemoved(ctx);
        }
    }
}
复制代码

デコード(CTX、アウト)の方法は、デコーダによって特定の論理実装クラスを達成するための抽象メソッドです。もちろん、親クラスでは、ここで使用されるサブクラス自身によって実装される抽象メソッド抽象メソッドの特定のロジックを呼び出すと、テンプレートメソッドデザインパターンです。以下に示すように一般的に使用される網状デコーダは、以下の通りです。サブクラスコア・ロジック・デコーディングについて、2つの記事の分析を行いました。

FixedLengthFrameDecoder(基于固定长度的解码器)
LineBasedFrameDecoder (基于行分隔符的解码器)
DelimiterBasedFrameDecoder (基于自定义分割符的解码器)
LengthFieldBasedFrameDecoder (基于长度字段的解码器)
复制代码

そして最終的に戻ってchannelRead()最後にステートメントブロック法(以下のコード)は、第一取得するうちに復号されたデータオブジェクトの数を、その後呼び出しfireChannelRead()は、下方伝播プロセスオブジェクト解析されたデータの方法。サイズが0の場合、そのオブジェクトがデコードされていない、それがダウンして伝播しないことを意味しますが、次の復号までのデータを読み出すために続けています。

finally {
    // 省略部分代码...

    // size是解码出来的数据对象的数量,
    int size = out.size();
    decodeWasNull = !out.insertSinceRecycled();
    // 向下传播,如果size为0,就表示没有解码出一个对象,因此不会向下传播,而是等到下一次继续读到数据后解码
    fireChannelRead(ctx, out, size);
    out.recycle();
}
复制代码

概要

この記事では、最初のTCPスティックパッケージ、半分パック現象は、半パッケージの現象の根本原因は、TCPプロトコルに基づいてデータパケットを複数のデータパケットの合成や分割、複数の分離、およびスティックのパッケージを生成することが何であるかを説明しバイトのデータをストリーミング。そして、ソース導入を組み合わせ、解決するためのハーフパックの問題は、コーデックを使用してどのようにネッティースティックパッケージにあります。最後に、特定のソースデコーディング業務分析に関しては、2つの記事の後半で分析されます。

勧告

マイクロチャンネル公衆数

おすすめ

転載: juejin.im/post/5e088c97f265da33ea00aae9