blog

Netty学習シリーズ(VII) - まとめ

今回はNettyに関する最後の投稿で、Nettyについて学んだことや私自身の洞察をまとめます。さらに、Nettyで使われているいくつかのデザインパターンを分析し、最後にこのシリーズで学んだことをまとめ...

Jan 16, 2021 · 26 min. read
シェア

序文

今回でNettyの連載は最終回となります。この連載では、これまで学んできた知識と、Nettyに関する私なりの考察をまとめたいと思います。さらに、Nettyで使われているいくつかのデザインパターンを分析し、最後にこの連載で得た洞察をまとめます。

ネッティのファーストルック

このセクションでは、Nettyとは何か、なぜNettyというライブラリが作られたのか、Nettyの利点について説明します。

Nettyとは

Netty in Actionでの定義:「Nettyは非同期、イベント駆動型のWebアプリケーションフレームワークであり、保守可能で高性能なプロトコル指向のサーバーとクライアントの迅速な開発をサポートします。Nettyの内部実装は複雑ですが、Nettyはビジネスロジックをネットワーク処理コードから切り離すための使いやすいAPIを提供しています。Nettyは、DubboElasticsearchZookeeperHadoopRocketMQなど、多くのフレームワークやオープンソースのRPCコンポーネントに採用されています。NettyはTrustin Leeという人物によって始められ、バージョン4.0ではJBossに帰属し、その後はNettyコミュニティによって管理されています。開発の経緯は以下の通り:

  • Netty2は2004年6月にリリースされ、当時はJavaコミュニティ初の使いやすいイベント駆動型Webベースフレームワークであると主張していました。
  • Netty3 2008年10月発売
  • 2013年7月にリリースされたNetty4
  • 5.0.0.Alpha1 2013年12月リリース
  • 2015年1月に5.0.0を廃止

ここで、なぜバージョン5.0が非推奨になったのかが気になるところですが、NettyのコミュニティでNettyのメンテナンスをしている人の一人が投稿した次のような文章をご覧ください:

ここでの主なポイントは、ForkJoinPoolの使用は、コードが複雑になりますが、有意なパフォーマンスの向上は、あまりにも多くのブランチを維持することに加えて、実際には、それらの多くは必要ありませんが、メンテナンスが終わっていない多くの作業を必要としないということです。実際には、Nettyは、基本的なネットワークプログラミングのネイティブNiOOiOとEpollのカプセル化され、なぜここで直接それらを使用しないが、Nettyを選択し、次の分析。

Nettyが生まれた理由

Nettyは主にNIOをカプセル化したもので、ここではNettyとjava NIOを比較し、Nettyを生み出す原動力となったものを見てみましょう。まずjava.nioフルネームJavaノンブロッキングIOは、JDKで提供される新しいAPI(新しいIO)のJDK 1.4以上のバージョンを指し、データコンテナのすべての原始的なタイプのキャッシュサポートを提供するために、それを使用して、ノンブロッキングの高いスケーラビリティネットワークを提供することができます。以下は、ネイティブNIOを使用せずに、Nettyを使用する理由の分析です。 まず、あなたはより良いJDK NIOのバグを回避することができます。ここでは、例えば、古典的なEpollのバグ:異常なウェイクアップアイドリングは、図に示すように、100%のCPUにつながる:

ここでは、彼の解決策は、このバグを解決するためのヘルプで修正不可、およびNettyの結果、具体的には、ソースコードを見ることができます見ることができます:

/**
 * NioEventLoop.java 788 
 */
 if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
 selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
 // The selector returned prematurely many times in a row.
 // Rebuild the selector to work around the problem.
 logger.warn(
 "Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
 selectCnt, selector);
 rebuildSelector();
 selector = this.selector;
 // Select again to populate selectedKeys.
 selector.selectNow();
 selectCnt = 1;
 break;
 }

私が3番目の記事で分析したコードは、空のポーリングの数がSELECTOR_AUTO_REBUILD_THRESHOLDの値よりも大きいと判断することです、それはrebuildSelector() メソッドを呼び出します、このメソッドの役割は、新しいセレクタに古いセレクタ上のすべてのSelector.Keyを登録することです。もちろん、JavaのNIOのバグを解決するためのNettyはこれだけではありません、他にもありますが、あまり紹介することはありません。 その直後、Nettyの方が優れているもう一つの理由は、NettyのAPIがよりフレンドリーで強力だからです。1つ目は、 jdkのByteBufferは使い勝手が悪く、関数も弱いのですが、Nettyは独自のByteBufをカプセル化しています。次に Nettyは、実装の多くの詳細を保護し、多くの変更を分離するのに役立ちます。例えば、NettyはJDKのNIO実装をブロックするのに役立ちました。BIO(OIO)、NIO、Epollの変更を分離し、それらを切り替えたい場合は、テーブル内のわずか数行のコードで可能です。 最後に、プログラムのJDKのNIOの実装で行きたい場合は、どのように多くの問題を解決する必要があり、ここで最初にNettyでどのように多くの問題を解決するために、図などを見てください:

ここでは、Nettyは4000以上のバグを解決したことがわかりますが、もちろん、ネットワークプログラムを実装する場合はNettyほどフル機能ではないかもしれない、非常に多くのバグが発生しませんが、それはまた、より多くのバグを生成する可能性があるとして、レベルは高くありません:

このように、JDK NIOには千を超えるバグがあり、ネイティブのJDK NIOを使ってネットワークアプリケーションを実装する場合、これらのバグを回避する必要があり、それは巨大なプロジェクトになります。以上の点から、Nettyの発展と成長は偶発的なものであると同時に必然的なものであるとも言えます。

Nettyのメリット

前節の分析で、ネッティの優位性は容易に理解できたと思いますが、ここではまだ以下の点をまとめておきます:

  • APIは使いやすく、開発の敷居も低い。
  • 強力で、さまざまなコーデックがあらかじめ設定されており、さまざまな主流プロトコルをサポートしています。
  • 強力なカスタマイズ機能、channelHandlerによるコミュニケーションフレームワークの柔軟な拡張。
  • 高性能。
  • 成熟し、安定しており、多くのjdk nioのバグが修正されています。
  • コミュニティは活発です。
  • 大規模な商用アプリケーションで実証された品質

この章では、Nettyの利点や開発の歴史などについてお話しました。次の章では、Nettyの全体的なアーキテクチャについて分析します。

II ネッティのアーキテクチャ

このセクションでは、主にNettyの全体的な枠組みを分析します。具体的な部分の詳細については、先に分析しましたので、このセクションではあまり分析しません:

ここでは、コアの底の値は、彼がゼロコピーする機能だけでなく、より一般的なAPIとスケーラブルなイベントドライバをサポートしていることがわかります。上記では、トランスポートサービスは、Http、Protobuf、mqttなどを含むプロトコルのサポートのために、NIOとOIOをサポートし、セキュリティ、コンテナの統合など、対応する実装を持っています。

タスク処理モデル。

前回までは主にソースコードの視点からNettyの重要な部分を解説してきましたが、今回はマクロの視点からNettyのスレッドモデルを解説します。

シリアル処理モデル

このモデルでは、ネットワーク要求接続とタスク処理を処理するためにスレッドが使用されます:

ここでわかるように、ワーカーがタスクを受け付けると、そのタスクは即座に処理されます。つまり、タスクの受け付けとタスクの処理は、区別なく同じワーカースレッドで行われます。多くの場合、タスクの処理はタスクの受け入れよりもずっと遅いものです。たとえば、タスクを処理するときに、ネットワークIOの一種であるリモートデータベースにアクセスする必要があるかもしれません。通常、IO操作はより時間がかかり、次のタスクの受け入れに直接影響します。この欠点には、次のような改善策があります。

ここでは、タスクの受信と処理の2つの段階を別々に処理し、1つのスレッドがタスクを受信してタスクキューに入れ、もう1つのスレッドがタスクキュー内のタスクを非同期に処理します。これにより、スループットが少し改善されましたが、まだ完全ではなく、次のセクションを参照してください。

処理モデルの並列化

上のセクションの最適化により、ワーカースレッドをさらにプールすることができます:

ここでは、タスクの処理が一般的に遅いため、長い時間のタスクバックログ内のタスクキューにつながる処理することはできませんし、マルチスレッドに対処するために使用することができます。ここではパブリックタスクキューが使用され、マルチスレッド環境は、スレッドの安全性を確保するためにロックすることは避けられない、一般的に使用されるスレッドプールは、このモデルです。このモデルは、各スレッドのタスクキューを維持することによって改善することができます。上記の分析、スレッド処理モデルの次の分析を設定します。

Nettyのスレッド処理モデル

NettyにはThreadPerConnectionReactorProactorなど多くのスレッドモデルがあります。まずThreadPerConnectionモードはBIOスレッドモデルに相当し、上記のシリアルタスク処理モデルにも相当します。次にReactorモードはNIOスレッドモデルに対応し、上記の並列処理モデルに対応します。なお、Netty は AIO の実装を非推奨としたため、ここでは紹介せず、Reactor モードを中心に説明します。

原子炉モデル

リアクタースレッドモデルが懸念している:タスクが受け入れられた後、処理プロセスは、異なるステップの数に分割され、カットされ続け、異なるスレッドで各ステップが対処するために、つまり、スレッドで処理される元のタスクは、現在、スレッドの数によって処理され、独自のステップの処理では、各スレッドが、また、スレッドの次の段階にタスクを転送する必要がある処理を続行します。具体的には、次のようなイメージを見ることができます:

Nettyにおけるリアクター・スレッド・モデル

Nettyでは、それぞれ、シングルスレッドリアクタマルチスレッドリアクタとマスタスレーブマルチスレッドリアクタの3つのリアクタースレッドモデルがあります。以下は別々に分析するために、まず、シングルスレッドリアクタ、または最初のイメージを見て、次のように:

実際、タスクを分配するのは主にアクセプターであることがここでわかります。 では、Nettyではどのようなことが起こるのでしょうか? 次の使い方はこのスレッドモデルで、ソースコードは以下の通りです:

EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);

上記の例を念頭に置いて、マルチスレッドのREACTORとは何かを見てみるのはどうでしょうか。具体的には、まず以下のような図をご覧ください:

ここで、ビジネス・ロジックを処理するためのThreadPoolスレッド・プールが追加されているのがわかりますが、実は、前述の2番目のシリアル・タスク処理モードと似ています。具体的なプログラム例のソースコードは以下の通りです:

EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);

あなたは、この例のプログラムと以前の違いの唯一の1つの場所は、NioEventLoopGroupの作成で上記は1を渡されることがわかりますが、実際には、デフォルトでは、ここでパラメータを渡さない場合は、私は前の記事で持っているスレッドプールの数の2倍のCPUコアで構築されますされる、あなたは前の記事の具体的な分析を見ることができます。ここで最後のスレッドモデルマスタースレーブマルチスレッドモデルですが、ここでも推測することができます、それは上記の並列タスク処理モデルである必要があります、具体的または次のことを示すためにイメージ付き:

mainReacotor、subReactor、ThreadPoolは3つのスレッドプールです。mainReactorはクライアントの接続要求を処理し、受け付けられた接続をsubReactorスレッドの1つに登録する責任を負い、subReactorはクライアントチャネル上のデータの読み書きを処理する責任を負います;ThreadPoolは特定のビジネスを処理する特定のビジネスロジックスレッドプールです。ここでは具体的にどのような場所でそれを使用することがあり、具体的には、次のように、サンプルプログラムのソースコードを見てください:

EventLoopGroup bossGroup = newNioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup(); 
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup); 

私は、上記のコードは、おそらくNettyのNIOのスレッドモデルの上にある多くの人々が使用していると信じて、その後Nettyは具体的にこれらのモデルを使用する方法、その実装の分析の次の分析。

Nettyのランタイムフロー

上記のスレッド・モデルがNettyでどのように使われているかを具体的に見るには、まず次のような図を見る必要があります:

これにはいくつか注意すべき点があるので、ここで簡単に分析します。まず、NioEventLoop と NioEventLoopGroup についてですが、NioEventLoop は実際にはワーカースレッドで、直接的にはスレッドとして理解できます。つまり、アプリケーションが 1 つのポートをリッスンする必要がある場合、 bossGroup には 1 つの NioEventLoop スレッドしかありません。各NioEventLoopはSelectorにバインドされているので、Nettyのスレッドモデルでは、複数のSelecotrがIO準備完了イベントをリッスンしています。そしてChannelはSelectorに登録されます。 ChannelはNioEventLoopにバインドされ、これはスレッドにバインドされたコネクションに相当し、このコネクションのすべてのChannelHandlerは単一のスレッドで実行され、マルチスレッドの干渉を回避します。さらに重要なことは、ChannelPiplineチェーンは厳密な順序で実行されなければならないということです。シングルスレッド設計は、ChannelHandlersの連続実行を保証します。NioEventLoopセレクタは、複数のChannelによって登録することができます。つまり、複数のChannelが一つのEventLoopを共有し、EventLoopのセレクタによってチェックされます。前回分析した他の関連概念については、ここではあまり分析しません。

サービスの開始

このサブセクションの初めから主にNettyの実行プロセスを分析することです、このセクションでは、主にサービスの分析を開始します。実際には、前の分析では非常に明確になっているここでは、主にこれらの2つの視点のワーカースレッドとボスのスレッドを分析するために、単なる要約です。

ワーカースレッド

  • selector
  • serverSocketChannel
  • serverSocketChannel
  • bossGroup から serverSocketChannel に NioEventLoop を与えます。

ボス・スレッド

  • 選択した NioEventLoop のセレクタに serverSocketChannel を登録します。
  • バウンドアドレスの開始
  • セレクタへのaccept connectionイベントの登録

これは、時間の前にソースコードの詳細な分析は、主にいくつかの初期化操作であるこれらの手順の話されており、次にバインド()メソッドを呼び出すと、この特定のソースコードは、次のように、イメージを置くために前の記事を分析することはありません:

上記のいくつかの重要なポイントは、Selectorが新しいNioEventLoopGroup()で作成され、最終的なリスニングOP_ACCEPTは、NioEventLoopをトリガするfireChannelActive()のバインド完了を介して、起動を完了するために操作を登録する実行を介してです。このセクションでは、ほぼこれらの重要なポイントです。

コネクションの構築

サービスが起動したら、コネクションを構築し、コネクションにアクセスされるのを待つ必要があります。 このサブセクションでは、やはり前のパターンを参照して、主にワーカースレッドとボスレッドを以下のように分析します:

ボススレ

  • NioEventLoop での接続作成イベントのセレクタ・ポーリング
  • socketChannel
  • socketChannel を初期化し、workerGroup から NioEventLoop を選択します。

ワーカースレッド

  • 選択した NioEventLoop のセレクタに socketChannel を登録します。
  • セレクタへの読み取りイベントの登録

このセクションの焦点はworkerGroupにあることを除けば、このセクションのステップの多くは、実際には前のセクションの知識ポイントと似ています:

前節の図に明確に示されているように、ワーカースレッドの NioEventLoop は Register オペレーションによって開始されます。また、コネクションからの読み込み操作を受け付けると、デフォルトでは16回までしか読み込まれません。

データの受信

ここでデータを受信する主な考慮事項は、Nettyは、適応的なデータサイズのアロケータAdaptiveRecvByteBufAllocatorを使用して、コンテナは、次の時間を予測するためにデータの現在の量に基づいてされるどのくらいの大きさのコンテナは、データをロードするために取る必要がありますコンテナの大きさを取る最初の時間です。もう一つの考慮事項は、コンテナが満杯であり、確かに新しいコンテナを埋めるために取り続けたい、この方法は、実際には、最大16回のデフォルトの最後に上記のサブセクションでは、常にそれをロードされていません。これらは、ワーカースレッドで行われ、今私は行を整理します。

  • まず、マルチプレクサはOP_READイベントを特定のSELECTストラテジーを介して受信します。
  • そして、そのイベントの処理があり、またそのイベントを処理するための一定のプロセスがあります。
  • 最初に1024バイトのbyteBufがデータを受け取るために割り当てられます。
  • そして、ChannelからbyteBufにデータを受け入れ、受け入れたデータの実際のサイズを記録し、次の割り当てのためにbyteBufのサイズを調整します。
  • その直後にpipeline.fireChannelRead(byteBuf)がトリガーされ、読み取りデータが伝播されます。
  • write書き込み先buffer

Above there are a few points to note is that read data essence: sun.nio.ch.SocketChannelImpl#read(java.nio.Byte Buffer); NioSocketChannel read() is to read data, NioServerSocketChannel read() is topipeline.fireChannelReadComplete()は読み込みイベント処理が完了したことを意味します。pipeline.fireChannelRead(byte Buf)は読み込みデータが完了したことを意味します。最後に注意すべき点は、AdaptiveRecvByteBufAllocatorのbytebufに対する推測で、ズームインは直接ズームインすることが非常に決定的ですが、ズームインするときは、ズームインする前に、本当にズームアウトする必要があるかどうかを2回判断する必要があります。

業務処理

ここでは、ビジネスの本質に対処するために:パイプライン内のデータは、すべてのハンドラchannelRead()の実行プロセスは、ここで私はまだ図の前の記事を引用し、次のように:

データの送信

データ送信を理解する前に理解しておくべきいくつかの操作を以下に示します:

  • write書き込み先buffer
  • flush: バッファからデータを送信
  • writeAndFlush: バッファに書き込み、すぐに送信
  • WriteこれとFlushの間にはChannelOutboundBuffer

Nettyはデータを書き込む際、書き込めなくなったら書き込みを停止し、書き込めるようになったらOP_WRITEイベントを登録して通知し、再度書き込みを行います。また、Nettyのバッチ書き込みでは、すべての書き込みを行いたい場合は、maxBytesPerGatheringWriteを調整することで、より高速に書き込むことができます。Nettyは、書き込むデータがある限り、書き出すことができ、書き出すことができなくなるまで書き込もうとしてきたり、16回書き込んでもまだ書き終わらない場合は、書き込みイベントを登録してトリガするのではなく、直接タスクを書き込んで書き込みを継続するように計画されており、より強力です。代わりに、トリガする書き込みイベントを登録するには、より簡潔かつ強力です。最後に、Nettyは、あまりにも多くのデータを書き込むには、特定の水ラインよりも、フラグビットが偽に変更されます書き込みますので、データを送信する独自の意思決定のアプリケーション側。次のように、データを送信するための具体的な手順を見てください:

切断

切断操作は実際には非常に簡単ですが、主に受信したOP_READイベントに基づいて区別するために、次の行はまだです。

  • マルチプレクサがOP_READイベントを受信
  • OP_READ NioSocketChannel.NioSocketChannelUnsafe.read()
  • データの受け入れ
  • 受け付けたデータのサイズが<0であるかどうかを判断し、「はい」であればクロージャであると判断し、クロージャの実行を開始します。
  • チャンネルを閉じる
  • Clear messages: 新しいメッセージを受け付けず、キュー内のすべてのメッセージを破棄します。
  •               fireChannelInactiveとfireChannelUnregisteredのトリガー。

接続をクローズするとOP_READメソッドがトリガーされます。クローズの場合、読み込んだバイト数は -1 になります。データ読み取り中に強制的にクローズすると IOException が発生し、クローズが実行されます。

サービスの停止

サービスのシャットダウンは、実は非常に面倒なもので、主に bossGroup と workerGroup の shutdownGracefully() メソッドを呼び出すことで、全ての Groups 内の NioEventLoop がシャットダウンされ、その中で一連の判定が行われるのですが、その具体的な内容は以下の図の通りです:

上記の具体的な分析は、ここでは実施されません、サービスを終了することの本質は、すべての接続とセレクタを閉じて、他の商品を受信することではない、手元のジョブを終了しようとすると、最後にすべてのスレッドを閉じて、イベントループを終了します。ここで私はNettyの全体のランタイムプロセスの分析が完了すると、次の1つは、私は主にNettyの導入にいくつかのデザインパターンで使用されます。

III ネッティのデザインパターン

Nettyは、ライブラリとして非常に良い、彼は非常に多くの自然のデザインパターンで使用され、もちろん、この記事はここで私はちょうど分析のいくつかの典型的な例を選択します。ここでは、まずNettyで使用されているデザインパターンを示すために絵を使用して、次のとおりです:

このセクションでは、上記のデザインパターンのすべてを分析するのではなく、主にいくつかのデザインパターンをピックアップして分析し、デザインパターンの分類を確認します。

デザインパターンの分類

実際、伝統的なデザインパターンは、創造的デザインパターン構造的デザインパターン行動的デザインパターンの3つに分けられ、それぞれ異なる焦点で問題を解決します。 創造的デザインパターン:彼は主にオブジェクトの作成問題を解決し、複雑な作成プロセスをカプセル化し、オブジェクトの作成とコードの使用を切り離すことです。主なパターンにはシングルトンパターンファクトリーパターンビルダーパターンプロトタイプパターンなどがあります。 構造パターン:主に、いくつかのクラスやオブジェクトを組み合わせた古典的な構造をまとめ、これらの古典的な構造は、保留中のアプリケーションシナリオの問題を解決することができます。主に、ProxyパターンAdapterパターンDecoratorパターンBridgeパターンFacadeパターンCombinationパターンHedonicパターンなどがあります。 行動デザインパターン:主にクラスやオブジェクト間の相互作用の問題を解決します。このタイプのパターンはもっとあり、11種類あります。ObserverパターンChain of ResponsibilityパターンIteratorパターンTemplateパターンStrategyパターンStateパターンVisitorパターンMemoパターンCommandパターンInterpreterパターンMediatorパターンです。上記はデザインパターンのほとんどで、Nettyはその半分近くですが、ここではあまりに多すぎて、十分に吸収・学習できていないので、後日分析記事を書く機会の一部を分析します。ここでは、主に戦略パターン装飾パターンテンプレートパターンについて分析します。

装飾パターン

あなたが継承よりも柔軟な選択肢を提供するためにデコレータの機能を拡張する場合は、上記のサブセクションのデザインパターンの分類を埋めるために、装飾パターンが構造パターンに属していることがわかります、彼の文章の要約は次のとおりです:動的にオブジェクトに責任を添付します。ここで直接NettyにWrappedByteBufソースコードでは、このデザインパターンを分析するために、ソースコードは次のとおりです:

class WrappedByteBuf extends ByteBuf {
 protected final ByteBuf buf;
 protected WrappedByteBuf(ByteBuf buf) {
 if (buf == null) {
 throw new NullPointerException("buf");
 }
 this.buf = buf;
 }
 
 @Override
 public final boolean hasMemoryAddress() {
 return buf.hasMemoryAddress();
 }
 
 @Override
 public final long memoryAddress() {
 return buf.memoryAddress();
 }
 ...
}

ここでは、最初にコンストラクタを見て、ByteBufインスタンスを渡し、この渡されたインスタンスは、装飾され、その動作は、現在のクラスによって動的に変更することができ、つまり、WrappedByteBufです。このWrappedByteBufのため、それだけでデコレータの基本クラスなので、彼は唯一の単純なリターンを行うために装飾されたの動作を渡し、任意の変更を行わなかった、より多くのメソッドが続いて、装飾されたのメソッドへの直接呼び出しです、彼のサブクラスを見て、最初はUnreleasableByteBuf、ソースコードは次のとおりです:

final class UnreleasableByteBuf extends WrappedByteBuf {
 
 private SwappedByteBuf swappedBuf;
 
 UnreleasableByteBuf(ByteBuf buf) {
 super(buf);
 }
 ...
 @Override
 public boolean release() {
 return false;
 }
 ...
}

まず親クラス WrappedByteBuf のコンストラクタを呼び出します。その中のrelease()メソッドを見ると、releaseするものは何でもreleaseし、releaseするものはfalseを返すだけです。別のWrappedByteBufのサブクラス SimpleLeakAwareByteBufを見てみましょう。このクラスはメモリリークを自動的に認識するByteBufで、ソースコードは以下の通りです:

final class SimpleLeakAwareByteBuf extends WrappedByteBuf {
 private final ResourceLeak leak;
 SimpleLeakAwareByteBuf(ByteBuf buf, ResourceLeak leak) {
 super(buf);
 this.leak = leak;
 }
 ...
 @Override
 public boolean release() {
 boolean deallocated = super.release();
 if (deallocated) {
 leak.close();
 }
 return deallocated;
 }
 ...
}

コンストラクタまたは親クラスのメソッドを呼び出すには、リリースメソッドでは、メモリリークが見つかった場合、leak.close()メソッドの実行、および戻り、実際には、また、動的に装飾の動作を変更し、装飾の変更です。ここで別のサブクラスでは、AdvancedLeakAwareByteBuf、ソースコードは次のとおりです:

final class SimpleLeakAwareByteBuf extends WrappedByteBuf {
 private final ResourceLeak leak;
 AdvancedLeakAwareByteBuf(ByteBuf buf, ResourceLeak leak) {
 super(buf);
 this.leak = leak;
 }
 ...
 @Override
 public boolean release() {
 boolean deallocated = super.release();
 if (deallocated) {
 leak.close();
 } else {
 leak.record();
 }
 return deallocated;
 }
 ...
}

このクラスは上のクラスと同じルーチンを持っていることがわかります。コンストラクタは依然として親クラスのメソッドを呼び出しています。まず、メモリリークが見つかった場合、leak.close()メソッドが実行され、その後リターンされます。しかし、メモリリークがなく、releaseメソッドが呼ばれた場合は、その記録が残るので、スタックトレースをトラブルシューティングに利用することができます。ここでは、ByteBufの具体的な使い方を次のように示します:

/**
 * AbstractByteBufAllocator.java 53 
 */
 protected static CompositeByteBuf toLeakAwareBuffer(CompositeByteBuf buf) {
 ResourceLeak leak;
 switch (ResourceLeakDetector.getLevel()) {
 case SIMPLE:
 leak = AbstractByteBuf.leakDetector.open(buf);
 if (leak != null) {
 buf = new SimpleLeakAwareCompositeByteBuf(buf, leak);
 }
 break;
 case ADVANCED:
 case PARANOID:
 leak = AbstractByteBuf.leakDetector.open(buf);
 if (leak != null) {
 buf = new AdvancedLeakAwareCompositeByteBuf(buf, leak);
 }
 break;
 default:
 break;
 }
 return buf;
 }

ここでは、さまざまな戦略に従って butebuf にさまざまな変更を加えているのがわかります。装飾パターンの主な点は、装飾されたクラスは一度変更されると、別の変更されたクラスを追加することによって再び変更することができるということです。一つは、デコレータクラスと元のクラスが同じ親クラスを継承していることで、 元のクラスを複数のデコレータクラスにネストさせることができます。上記の例では、異なる戦略に従って選択された異なるデコレータークラスが、 戦略パターンに直面しているように見えます。

戦略モデル

このセクションで説明するStrategyパターン 次のセクションで説明するTemplateパターンはすべて振る舞いパターンです。ストラテジーパターンは、各アルゴリズムを個別にカプセル化したアルゴリズムクラスの集合を定義することで、互いを置き換えることができるようにするものです。これには、ストラテジー・インターフェイスと、このインターフェイスを実装するストラテジー・クラスのセットが含まれます。すべての戦略クラスは、同じインターフェイスを実装しているため、クライアントコードは、プログラミングの実装ではなく、インターフェイスに基づいて、柔軟に異なる戦略を置き換えることができるように、NettyEventExecutorChooserに固有の記述を分析します。ここで私はここで最初の前の知識のポイントを確認し、以前は次のように、次のメソッドを分析しました:

public EventExecutorChooser newChooser(EventExecutor[] executors) {
 if (isPowerOfTwo(executors.length)) {
 return new PowerOfTwoEventExecutorChooser(executors);
 } else {
 return new GenericEventExecutorChooser(executors);
 }
}

これはセレクタ EventExecurotChooser の作成で、新しい接続が来たときに NioEventLoop選択するために使用されます。 異なるチョーザが作成されますが、選択する考え方は同じで、循環的に配列から選択します。例えば、ここでのNioEventLoop配列の長さは8で、サーバに接続するクライアントを追加した後、サーバはセレクタEventExecutorChooserを通して最初のNioEventLoopを選択し、次に2番目のクライアントがサーバに接続し、セレクタEventExecutorChooserは2番目のNioEventLoopをサーバとして選択します。その後、2番目のクライアントがサーバに接続し、セレクタEventExecutorChooserが2番目のNioEventLoopを選択し、これが繰り返され、8番目のクライアントがサーバに接続すると、8番目のNioEventLoopによって処理されます。 9番目のクライアントがサーバに接続すると、再び1番目のNioEventLoopによって処理されます。 結局、NioEventLoopによってラップされたSelectorはここで多重化されます。上記の最初の配列の長さを決定するためにisPowerOfTwo()関数を呼び出します別の戦略を選択する2の倍数ではありませんが、ここでなぜ異なる戦略は非常に明確な特定の実装の分析以下。まず、次のように、戦略のインターフェイスを見てください:

 /**
 * EventExecutorChooserFactory.java 34 
 */
 @UnstableApi
 interface EventExecutorChooser {
 /**
 * Returns the new {@link EventExecutor} to use.
 */
 EventExecutor next();
 }

ここでは上記の非常に単純な次の関数であり、次のように、関数の彼の具体的な実装を見て、次のとおりです:

/**
 * DefaultEventExecutorChooserFactory.java 46 
 */
 private static final class PowerOfTowEventExecutorChooser implements EventExecutorChooser {
 private final AtomicInteger idx = new AtomicInteger();
 private final EventExecutor[] executors;
 PowerOfTowEventExecutorChooser(EventExecutor[] executors) {
 this.executors = executors;
 }
 @Override
 public EventExecutor next() {
 return executors[idx.getAndIncrement() & executors.length - 1];
 }
 }
 private static final class GenericEventExecutorChooser implements EventExecutorChooser {
 private final AtomicInteger idx = new AtomicInteger();
 private final EventExecutor[] executors;
 GenericEventExecutorChooser(EventExecutor[] executors) {
 this.executors = executors;
 }
 @Override
 public EventExecutor next() {
 return executors[Math.abs(idx.getAndIncrement() % executors.length)];
 }
 }

上の2つのクラスはどちらもEventExecutorChooserの実装で、nextの実装が異なることがわかります。ここでは、モーダルな"%"操作と"&"操作を実装していることがわかりますが、後者の方が前のものより効率的です。実際には、上記の実装は、単に戦略パターンの非常に単純な実装であり、実際には、ほとんどの場合、戦略パターンは、if - else分岐判定ロジックを回避することです。彼は主に作成し、使用する戦略の定義から切り離され、コードの複雑さを制御するので、各部分があまりにも複雑ではない、コードの量が多すぎます。同時に複雑なコードは、戦略のパターンを追加し、戦略を最小限に抑えることができるときに集中コードの順序を変更します。

テンプレートモデル

このセクションでは、メソッドでアルゴリズムの骨格を定義し、実装のために特定のステップをサブクラスに委ねるテンプレート・パターンを分析・解析します。テンプレートメソッドによって、サブクラスはアルゴリズム全体の構造を変更することなく、アルゴリズムのデフォルトステップを再定義することができます。NettyのByteToMessageDecoderの例を見てみましょう。ここでは、その実装を確認するために、まず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);
	.....
 }

ここでは、callDecodeメソッドについて、以下のようにフォローします:

 protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
 ....
 int oldInputLength = in.readableBytes();
 decode(ctx, in, out);
	....
 }

ここでのメインはデコード・メソッドで、このメソッドをここで続けて見ると次のようになります:

 /**
 * Decode the from one {@link ByteBuf} to an other. This method will be called till either the input
 * {@link ByteBuf} has nothing to read when return from this method or till nothing was read from the input
 * {@link ByteBuf}.
 *
 * @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
 * @param in the {@link ByteBuf} from which to read data
 * @param out the {@link List} to which decoded messages should be added
 * @throws Exception is thrown if an error accour
 */
 protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;

あなたは、このメソッドが抽象的なメソッドであることがわかりますが、実際には、アルゴリズムの大部分は、スケルトンの前面であり、その後、デコードプロセスの具体的な手順は、サブクラスに達成するために残されています。このサブクラスの私の例では、このような最も単純な固定長デコーダ、およびHttpデコーダ、mqttデコーダなど、多くのです。テンプレート・パターンには、再利用と拡張という2つの主な目的があります。再利用とは、すべてのサブクラスが親クラスで提供されるテンプレートメソッドのコードを再利用できることを意味します。拡張は、フレームワークのユーザーがフレームワークのソースコードを変更できないように、フレームワークの機能をカスタマイズするために取る拡張に基づいて、ポイントを拡張する機能を提供するテンプレートパターンを通してフレームワークの価値があります。Nettyで私は上記の例では、これらの3つのパターンを分析し、デザインパターンに加えて、確かにこれら以上のものです。実際、彼のデザインパターンやデザイン思考に関するソースコードは、あまりに多くのことを含み、経験や感覚にゆっくりと置き去りにすることができます。

概要

  • 行動するネッティ
  • ネッティの決定版

注:分析にはNetty 4.1.6のソースコードを使用しています。

Read next

マルチコアCPUとマルチスレッドを理解する

プロセスはスレッドを含み、プロセスは複数のスレッドを含みます。 スレッドはCPUのスケジューリングと割り当ての基本単位であり、プロセスはオペレーティングシステムによるリソース割り当ての最小単位です。 複数のプロセスを実装するには、タイムスライスローテーションアルゴリズムなど、オペレーティングシステムのプロセススケジューリングアルゴリズムに依存します。例えば、実行中のプログラムが3つある場合、オペレーティングシステムはシングルコアのCPUにこれらのプログラムを順番に実行させます。

Jan 16, 2021 · 2 min read