クライアントの読み取りおよび書き込み分析
イベントを受け入れる
NioEventLoop.runメソッドは最終的にprocessSelectedKeysメソッドを実行します。これは、この時点でクライアント接続イベントが発生し、acceptイベントが発生した後、SelectedKeysが発生し、この時点でSelectedKeysを操作するためです。
private void processSelectedKeys() {
if (selectedKeys != null) {
//执行这里
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}
private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
// null out entry in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
selectedKeys.keys[i] = null;
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
****************************
}
*************************************
}
}
クライアントのacceptイベントがある場合、このロジック
に従います。NioServerSocketChannelはNioMessageUnsafe.readメソッドを呼び出します。
NioMessageUnsafe.readメソッドでは、図に示すように、主にdoReadMessagesとpipeline.fireChannelRead(readBuf.get(i));メソッドに焦点を当てています。
NioServerSocketChannel.doReadMessages
この時点でacceptイベントを処理しているため、サーバーがここで受信するのはクライアントのSocketChannelであり、NettyのNioSocketChannelにSocketChannelをカプセル化します。
public NioSocketChannel(Channel parent, SocketChannel socket) {
super(parent, socket);
config = new NioSocketChannelConfig(this, socket.socket());
}
親クラスの呼び出しを続行します
この時点で聞いているのは読み取りイベントですが、それはNioと一致していますか?引き続き親クラスを呼び出し、読み取りイベントをSocketChannelに登録し、並行SocketChannelをnettyのNioSocketChannelに関連付けます。そして、親クラスを呼び出してパイプラインを初期化し、安全ではありません。
protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
super(parent, ch, SelectionKey.OP_READ);
}
新しくカプセル化されたNioSocketChannelをreadBufコレクションに保存します。
pipe.fireChannelRead(readBuf.get(i))
パイプラインのChannelReadメソッドを実行します。このパイプラインはNioServerSocketChannelのパイプラインであることに注意してください。前に述べたように、この時点のパイプラインはおおよそ次のようになっています。このパラメーターは、コレクションでフェッチするデータがNioSocketChannelであることです。前述のように、パイプラインのChannelReadメソッドが実行されます。ヘッドのChannelReadは引き続き親クラスのメソッドを呼び出すため、デフォルトでは引き続きfireChannelReadを呼び出します。 (これは次のインバウンドハンドラーメソッドのChannelReadです)ので、主にServerBootstrapAcceptorのChannelReadメソッドに焦点を当てます。
head-> ServerBootstrapAcceptor-> tail
コードを確認します。
サーバーの登録時に実行されたロジックの前のセクションで、ServerBootstrapAcceptorがパイプラインに追加されました。currentChildHandlerパラメーターは、自分で定義したChannelInitializerオブジェクトであることに注意してください。ChannelInitializerのハンドラーはまだハンドラーに追加されていません。
ServerBootstrapAcceptor.ChannelRead
前のコードから、childHandlerが作成したChannelInitializerオブジェクトであることがわかります。これは、現時点で
クライアントのNioSocketChannelのパイプラインがどのようになっているのかを示しています。
ヘッド-> ChannelInitializer(これは自分で定義したChannelInitializerオブジェクトです)->テール
ChannelInitializerのinitChannelがまだ呼び出されていないため、カスタムハンドラーがパイプラインに追加されていないことに注意してください。引き続き、コードを見ていきましょう。
childGroup.register
パイプラインが終了したばかりですが、引き続きコードを確認します。childGroupとは、ワークスレッドグループです。
ワークスレッドグループからNioEventLoopを取得し、registerメソッドを実行します。
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
同NioServerSockerChannel
@Override
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
最後に、NioEventLoopをNioSocketChannelに関連付けて登録イベントを実行するためにここで呼び出されます。このとき、読み取りイベントが登録されます。
NioSocketChannel登録イベント
これらのステップのコードを再度
確認してください。doRegister();
NioSocketChannelはjdkの下部にあるSocketChannelを取得して、読み取りイベントをセレクターに登録します。このセレクターはNioEventLoopに関連付けられたセレクターであることに注意してください。
pipe.invokeHandlerAddedIfNeeded ();前の分析から、NioSocketChannelのパイプラインは次のようになっていることがわかります。
ヘッド-> ChannelInitializer->テール
このステップでは、ChannelInitializerのinitChannelメソッドが呼び出され、ChannelInitializerが削除されます。現時点でのNioSocketChannelパイプラインは次のようになります
ヘッド-> serverHandler->テール
pipeline.fireChannelRegistered()
クライアント登録成功方法
先頭から、次のインバウンドハンドラーのChannelRegisteredメソッドを実行します。ハンドラーがChannelRegisteredメソッドをオーバーライドする場合、この時点で実行されます。詳細なデバッグコード。
pipe.fireChannelActive();
クライアント接続のアクティブな実行方法
頭から始めて、次のインバウンドハンドラーのChannelActiveメソッドを実行します。ハンドラーがChannelActiveメソッドをオーバーライドする場合、この時点で実行されます。詳細なデバッグコード。
この時点で、acceptイベントが登録されます
イベントを読む
クライアントとサーバー間の接続が確立されると、つまり受け入れた後、クライアントはデータ要求を開始する必要があります。これで読み取りイベントが実行されます。読み取りイベントを実行するスレッドは作業スレッドであり、ワークスレッドが処理します読み取りおよび書き込みイベントの場合、ボスはイベントを受け入れ、イベントをワークに渡し、読み取りイベントに登録することのみを担当します。作業スレッドの数は、CPUコアの数の2倍です。acceptイベントは非常に高速であるため、1つのスレッドで数千のリクエストを処理できますが、作業スレッドは次のメソッドに従って値を1つずつ取得します。1つずつ実行します。
読み取りイベントは以前と同じです。最初にデータをダイレクトメモリに読み取ります。ダイレクトメモリには、カーネルスペースとユーザースペースからのコピーイベントを保存できるという利点があります。
ここのパイプラインはNioSocketChannelのもので、次のようになります。
ヘッド-> serverHandler->テール
byteBuf = allocHandle.allocate(allocator):ダイレクトメモリを取得します
allocHandle.lastBytesRead(doReadBytes(byteBuf));:チャネルデータをダイレクトメモリのバッファに書き込みます
pipe.fireChannelRead(byteBuf)
は、メソッドを実行してデータの読み取りを開始し、ヘッドから次のハンドラーを実行して、bufをパラメーターとして渡します。
pipe.fireChannelReadComplete();
次のハンドラーを実行するために、先頭から開始してデータの終わりを読み取るためのメソッド
注:ハンドラーが受け渡したい場合は、pipeline.fireXXXXX()メソッドを実行する必要があります
この時点で、netty接続イベントは完了しています。
総括する
nettyから何を学びましたか
- マスタースレーブリアクタースレッドモデル
- NIO多重化ノンブロッキング
- ロックフリーのシリアル化設計のアイデア
- 高性能シリアル化プロトコルをサポート
- ゼロコピー(直接メモリの使用)
- ByteBufメモリプールの設計
- 柔軟なTCPパラメータ設定機能
- 並行性の最適化
高性能シリアル化プロトコルをサポート
支持想java对象序列化,可以直接传java 对象,netty会自动进行序列化与
反序列化
ロックフリーのシリアル化設計のアイデア
在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于
共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。
为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理
尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和
同步锁。
为了尽可能提升性能,Netty采用了串行无锁化设计,在IO线程内部进行
串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU
利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启
动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多
个工作线程模型性能更优。
Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的
fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop
调用到用户的Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操
作导致的锁的竞争,从性能角度看是最优的。
ダイレクトメモリの使用
利点
- ヒープメモリスペースを占有しないため、GCの可能性が低くなります
- ava仮想マシンの実装では、ローカルIOがダイレクトメモリを直接操作し(ダイレクトメモリ=>システムコール=>ハードディスク/ネットワークカード)、非ダイレクトメモリにはセカンダリコピーが必要です(ヒープメモリ=>ダイレクトメモリ=>システムコール) =>ハードディスク)/ネットワークカード)
不利益
- 初期割り当てが遅い
- メモリの管理に直接役立つJVMがないと、メモリオーバーフローが発生しやすくなります。FULL GCがないことを回避するために、物理メモリは最終的にダイレクトメモリによって使い果たされます。ダイレクトメモリの最大値は、-XX:MaxDirectMemorySizeで指定できます。しきい値に達したら、system.gcを呼び出してFULL GCを実行し、未使用のダイレクトメモリを間接的に再利用します。
ByteBufメモリプールの設計
随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级
的工作。但是对于缓冲区Buffer(相当于一个内存块),情况却稍有不同,特别是
对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,
Netty提供了基于ByteBuf内存池的缓冲区重用机制。需要的时候直接从池子里
获取ByteBuf使用即可,使用完毕之后就重新放回到池子里去。
例:チャネルからbufにデータを読み取るときに、メモリプール+ダイレクトメモリ形式を使用します。
並行性の最適化
- 揮発性物質の大規模かつ正しい使用;
- CASおよびアトミッククラスの広範な使用。
- スレッドセーフなコンテナの使用。
- 読み取り/書き込みロックにより、同時実行のパフォーマンスを向上させます。