記事ディレクトリ
シリーズ記事ディレクトリ
Nettyコアのソースコード解析(1)、Nettyのサーバーサイド起動処理のソースコード解析
Nettyのコアソースコード解析(2)、Nettyのサーバーサイドのリクエスト受信処理のソースコード解析
Nettyコアのソースコード解析(3) ビジネスリクエストの鍵実行 - ChannelPipeline、ChannelHandler、ChannelHandlerContext のソースコード解析
Netty コアのソースコード解析 (4) ハートビート検出のソースコード解析
Netty コアのソースコード解析 (5) コアコンポーネント EventLoop のソースコード解析
一、ChannelPipeline、ChannelHandler、ChannelHandlerContext
1. 3人の関係
- ServerSocket が新しい接続を作成するたびに、ターゲット クライアントに対応するソケットが作成されます。
2) 新しく作成された各ソケットには、新しい ChannelPipeline (以下、パイプラインと呼びます) が割り当てられます。 - 各 ChannelPipeline には複数の ChannelHandlerContext (以下、Context と呼びます) が含まれています4) これらは一緒に二重リンク リストを形成し、これらの Context は
、addLast メソッドを呼び出すときに追加されるChannelHandler (以下、
ハンドラーと呼びます)をラップするために使用されます。
: ChannelSocket と ChannelPipeline は 1 対 1 の関連付けであり、パイプライン内の複数の Context はリンクされたリストを形成し、Context は Handler の単なるカプセル化です。
リクエストが到着すると、ソケットに対応するパイプラインに入り、パイプライン内のすべてのハンドラーを通過します。これがデザイン モードのフィルター モードです。
2、ChannelPipeline ソースコード分析
1. ChannelPipeline インターフェイスの設計
ChannelPipeline が ChannelInboundInvoker、ChannelOutboundInvoker、および Iterable インターフェイスを継承していることがわかります。これは、データの送信メソッドと受信メソッドを呼び出すことができ、反復トラバーサルをサポートしていることを意味します。
ChannelPipeline の内部メソッドは基本的に、ハンドラーのリンクされたリストの追加、削除、変更、確認を目的としています。
2. ChannelPipeline がイベントを処理する
ChannelPipeline インターフェイスに画像が表示されます。
これはハンドラーのリストです。ハンドラーは受信イベントおよび送信イベントの処理またはインターセプトに使用されます。パイプラインは高度な形式のフィルターを実装しているため、ユーザーはイベントの処理方法とハンドラーがパイプラインに存在する方法を制御できます。やりとりの仕方。
上の図は、一般的なハンドラーがパイプラインで IO イベントを処理する方法を示しています。IO イベントは、inboundHandler または outBoundHandler によって処理され、ChannelHandlerContext.fireChannelRead メソッドを呼び出すことによって最も近いハンドラーに転送されます。
// 在ChannelInboundHandlerAdapter中的channelRead方法中有着对该方法的默认实现,默认就是调用了ChannelHandlerContext的fireChannelRead方法:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
// io.netty.channel.AbstractChannelHandlerContext#fireChannelRead
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(), msg);
return this;
}
その中で、findContextInbound メソッドはループによって次のboundHandlerを見つけます。
// io.netty.channel.AbstractChannelHandlerContext#findContextInbound
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}
invokeChannelRead は、次に見つかったハンドラーの channelRead メソッドを実行するために使用されます。これはエグゼキューター チェーンに相当します。
// io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead(io.netty.channel.AbstractChannelHandlerContext, java.lang.Object)
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
// io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead(java.lang.Object)
private void invokeChannelRead(Object msg) {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelRead(this, msg);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelRead(msg);
}
}
outBound にも同じことが当てはまりますが、最後から前にトラバースする点が異なります。
これらのハンドラーはパイプラインに保存されます。
通常、パイプラインには複数のハンドラーがあります。たとえば、一般的なサーバーには、各チャネルのパイプラインにコーデック + ビジネス ハンドラーというハンドラーがあります。
ビジネス プログラムはスレッドをブロックできないため、IO の速度に影響し、ひいては Netty プログラム全体のパフォーマンスに影響します。ビジネス プログラムが高速な場合は、IO スレッドに配置できますが、そうでない場合は (MQ を使用して) 非同期で実行する必要があります。または、ハンドラーを追加するときにスレッド プールを追加します。例:
// 下面这个任务执行的时候,将不会阻塞IO线程,执行的线程来自group线程池
pipeline.addLast(group, "handler", new MyHandler());
3、ChannelHandlerのソースコード分析
1. ChannelHandlerインターフェース
public interface ChannelHandler {
// 当把ChannelHandler添加到pipeline时调用
void handlerAdded(ChannelHandlerContext ctx) throws Exception;
// 当从pipeline中移除时调用
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
// 当处理过程在pipeline发生异常时调用
@Deprecated
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Sharable {
// no value
}
}
Netty の @Sharable アノテーションは、ChannelHandler が複数の ChannelPipeline 間で共有できるかどうか、つまり ChannelHandler がステートレスであるかどうかを識別するために使用されます。@Sharable アノテーションが付けられた ChannelHandler は、複数の ChannelPipeline 間で共有できるため、リソースの消費が削減されます。Netty では、ChannelHandler のコンストラクターが呼び出されるか、ChannelPipeline に追加されるたびに、新しいインスタンスが作成されます。したがって、ステートフルな ChannelHandler の場合、@Sharable でマークすることはできません。
ChannelHandler を再利用する場合、スレッドの安全性の問題が考慮されることに注意してください。@Sharable アノテーションが付けられた ChannelHandler クラスの場合、複数の ChannelPipeline 間で共有するときにスレッドの安全性を保証する必要があります。ChannelHandler クラスにスレッド セーフでないメンバー変数が含まれている場合は、スレッド セーフの問題を防ぐためにメンバー変数で同期する必要があります。したがって、これも @Sharable アノテーションを使用する際に注意が必要な点です。
ChannelHandler の役割は、IO イベントを処理するか、IO イベントをインターセプトして、次のハンドラー ChannelHandler に転送することです。Handler はイベントを処理する際に受信と送信に分かれており、双方向の動作が異なるため、Netty は ChannelHandler を継承する 2 つのサブインターフェース、ChannelInboundHandler と ChannelOutboundHandler を定義します。
2. ChannelInboundHandler インバウンドインターフェース
public interface ChannelInboundHandler extends ChannelHandler {
// channel注册时调用
void channelRegistered(ChannelHandlerContext ctx) throws Exception;
// channel取消注册时调用
void channelUnregistered(ChannelHandlerContext ctx) throws Exception;
// channel处于活动状态时调用
void channelActive(ChannelHandlerContext ctx) throws Exception;
// channel非活动状态时调用
void channelInactive(ChannelHandlerContext ctx) throws Exception;
// 从channel读取数据时被调用
void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
// 数据读取完成时被调用
void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
// 用户事件触发
void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;
// 通道可写状态被触发时调用
void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception;
// 发生异常时调用
@Override
@SuppressWarnings("deprecation")
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}
必要な特定のイベントに注意を払うために、これらのメソッドの一部を書き直し、書き換えられたメソッドに独自のロジックを実装する必要があります。Netty イベントが発生すると、Netty は対応する書き換えられたメソッドをコールバックします。
3. ChannelOutboundHandler アウトバウンド インターフェイス
public interface ChannelOutboundHandler extends ChannelHandler {
// 当请求将channel绑定到本地端口时调用
void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception;
// 发生连接时调用
void connect(
ChannelHandlerContext ctx, SocketAddress remoteAddress,
SocketAddress localAddress, ChannelPromise promise) throws Exception;
// 断开连接时调用
void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
// 当请求关闭channel时调用
void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
// 取消注册时调用
void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception;
void read(ChannelHandlerContext ctx) throws Exception;
// 在进行写操作时调用。写操作将通过ChannelPipeline写入消息。一旦调用Channel.flush(),它们就准备好被刷新到实际的Channel
void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception;
// 在执行刷新操作时调用。flush操作将尝试flush所有以前写的挂起的消息
void flush(ChannelHandlerContext ctx) throws Exception;
}
アウトバウンド操作は、データに接続するかデータを書き出すメソッドです。
4. ChannelDuplexHandler は送信イベントと受信イベントを処理します
ChannelDuplexHandler は、ChannelInboundHandlerAdapter クラスを継承し、ChannelOutboundHandler インターフェイスを実装し、すべての受信イベントと送信イベントを処理できます。
イベントの処理に ChannelDuplexHandler の使用を避けるようにしています。インバウンド イベントとアウトバウンド イベントは独立したクラスで処理するのが最適ですが、そうしないと混乱が生じやすくなります。
4、ChannelHandlerContextのソースコード分析
1. ChannelHandlerContextインターフェイス
public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker
ChannelHandlerContext インターフェイスは、AttributeMap、ChannelInboundInvoker、および ChannelOutboundInvoker インターフェイスを継承します。
ChannelHandlerContext は、ChannelInboundInvoker インターフェイスと ChannelOutboundInvoker インターフェイスのメソッドを継承するだけでなく、独自のメソッドの一部も定義します。これらのメソッドは、対応するチャネル、エグゼキュータ、ハンドラ、パイプライン、メモリ アロケータ、およびコンテキスト コンテキスト内の関連するハンドラが削除されるかどうかを取得できます。 。
コンテキストは、パイプラインでハンドラーを簡単に操作できるように、ハンドラーに関連するすべてのクラスをラップします。
2. ChannelInboundInvoker インターフェイスと ChannelOutboundInvoker インターフェイス
これら 2 つの呼び出し側は、受信または送信メソッド用です。つまり、受信または送信ハンドラーの外側の層に層をラップして、メソッドの前後で特定の操作をインターセプトして実行するという目的を達成します。
5. 作成過程のソースコード解析
1. SocketChannel 作成プロセスによりパイプラインが作成されます
前回の記事では、doReadMessages メソッドが NioSocketChannel を作成することを解析しました:
Netty コアのソースコード解析 (2)、Netty のサーバーのリクエスト受信プロセスのソースコード解析
// io.netty.channel.socket.nio.NioServerSocketChannel#doReadMessages
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
// 实际上调用NIO的的accept方法,获取SocketChannel
SocketChannel ch = SocketUtils.accept(javaChannel());
try {
if (ch != null) {
// 将NIO的SocketChannel包装成NioSocketChannel
buf.add(new NioSocketChannel(this, ch));
return 1;
}
} catch (Throwable t) {
logger.warn("Failed to create a new channel from an accepted socket.", t);
try {
ch.close();
} catch (Throwable t2) {
logger.warn("Failed to close a socket.", t2);
}
}
return 0;
}
new NioSocketChannel() のプロセスで親クラスを探し続けると、AbstractChannel の構築メソッドが見つかります。
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
protected DefaultChannelPipeline newChannelPipeline() {
return new DefaultChannelPipeline(this);
}
このとき、DefaultChannelPipeline が作成され、DefaultChannelPipeline のコンストラクター内でパイプラインが初期化されます。
protected DefaultChannelPipeline(Channel channel) {
// 将channel赋值给channel字段
this.channel = ObjectUtil.checkNotNull(channel, "channel");
// 创建future和promise,用于异步回调使用
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
// 创建inbound的TailContext
tail = new TailContext(this);
// 创建outbound的HeadContext(实际上实现了inbound和outbound两者)
head = new HeadContext(this);
// 形成双向链表
head.next = tail;
tail.prev = head;
}
2. パイプラインの addLast メソッド
サーバー側でパイプラインの addLiast メソッドを通じてハンドラーを追加するときのソース コード:
// io.netty.channel.DefaultChannelPipeline#addLast(io.netty.channel.ChannelHandler...)
@Override
public final ChannelPipeline addLast(ChannelHandler... handlers) {
return addLast(null, handlers);
}
// io.netty.channel.DefaultChannelPipeline#addLast(io.netty.util.concurrent.EventExecutorGroup, io.netty.channel.ChannelHandler...)
@Override
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
if (handlers == null) {
throw new NullPointerException("handlers");
}
for (ChannelHandler h: handlers) {
if (h == null) {
break;
}
addLast(executor, null, h);
}
return this;
}
// io.netty.channel.DefaultChannelPipeline#addLast(io.netty.util.concurrent.EventExecutorGroup, java.lang.String, io.netty.channel.ChannelHandler)
@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
// 多线程安全问题
// 检查这个handler实例是否是共享的,如果不是,并且已经被别的pipeline使用了,则抛出异常
checkMultiplicity(handler);
// 创建一个Context,我们可以看出,每添加一个Handler都会关联一个Context
newCtx = newContext(group, filterName(name, handler), handler);
// 将Context追加到链表中
addLast0(newCtx);
// If the registered is false it means that the channel was not registered on an eventloop yet.
// In this case we add the context to the pipeline and add a task that will call
// ChannelHandler.handlerAdded(...) once the channel is registered.
/*
如果这个通道还没有注册到selector上,就将这个Context添加到这个pipeline的待办任务中。
当注册好了以后,就会调用callHandlerAdded0方法(默认是什么都不做,用户可以实现这个方法)
*/
if (!registered) {
newCtx.setAddPending();
callHandlerCallbackLater(newCtx, true);
return this;
}
EventExecutor executor = newCtx.executor();
if (!executor.inEventLoop()) {
newCtx.setAddPending();
executor.execute(new Runnable() {
@Override
public void run() {
callHandlerAdded0(newCtx);
}
});
return this;
}
}
callHandlerAdded0(newCtx);
return this;
}
// io.netty.channel.DefaultChannelPipeline#addLast0
// 实际上是插在了尾的前一个,而不是尾,因为尾部的Handler用于框架做最后的处理用
private void addLast0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext prev = tail.prev;
newCtx.prev = prev;
newCtx.next = tail;
prev.next = newCtx;
tail.prev = newCtx;
}
3. まとめ
この時点で、3 つのオブジェクトの作成プロセスは終了です。ChannelSocket が作成されるたびに、バインドされたパイプラインが作成され、1 対 1 の関係になります。パイプラインが作成されると、テール ノードとヘッド ノードも作成されます。初期リンクリストを形成します。
tail は受信タイプのハンドラーであり、head は受信タイプと送信タイプの両方のハンドラーです。
パイプラインの addLast メソッドが呼び出されると、指定されたハンドラーに従って Context が作成され、この Context がリンクされたリストの最後 (末尾の前) に挿入されれば OK になります。
コンテキストはハンドラーをラップし、複数のコンテキストはパイプライン内で二重にリンクされたリストを形成します。
インバウンド メソッドは、ヘッド ノードから始まるインバウンドと呼ばれます。アウトバウンド メソッドはアウトバウンドと呼ばれ、末尾ノードから開始されます。
六、ChannelPipelineのスケジューリングハンドラーのソースコード分析
リクエストが受信されると、ChannelPipeline はパイプラインの関連メソッドを最初に呼び出します。受信イベントの場合、これらのメソッドは fire で始まります。これは、パイプラインのフローを開始することを意味します。後続のハンドラーに処理を継続させます。
上記で分析したように、ChannelPipeline は DefaultChannelPipeline を作成します。
また、上記では、DefaultChannelPipeline に一連のイベント メソッドが含まれていることも分析しました。イベントがトリガーされると、関連するメソッドが実行されます。
1、インバウンド——fireChannelRead
fireChannelRead メソッドを例としてソース コードをトレースしてみましょう。
クライアントのビジネス データが送信されると、DefaultChannelPipeline の fireChannelRead メソッドがトリガーされ、読み取るデータがあることが示されます。
// io.netty.channel.DefaultChannelPipeline#fireChannelRead
@Override
public final ChannelPipeline fireChannelRead(Object msg) {
// 将head传入
AbstractChannelHandlerContext.invokeChannelRead(head, msg);
return this;
}
// io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead(io.netty.channel.AbstractChannelHandlerContext, java.lang.Object)
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor(); // 获取下一个执行器
if (executor.inEventLoop()) {
next.invokeChannelRead(m); // 执行器调度ChannelRead事件
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
この時点で、handler() メソッドは、現在の ChannelHandler である this を返し、Handler の channelRead メソッドを呼び出します。
// io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead(java.lang.Object)
private void invokeChannelRead(Object msg) {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelRead(this, msg);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelRead(msg);
}
}
パイプラインの最初のハンドラーはシステムによって作成された HeadContext であり、この時点で HeadContext の channelRead メソッドが呼び出されることを上記で分析しました (このメソッドは親クラス DefaultChannelPipeline に実装されています)。
// io.netty.channel.DefaultChannelPipeline.HeadContext#channelRead
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
// io.netty.channel.AbstractChannelHandlerContext#fireChannelRead
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(), msg);
return this;
}
// io.netty.channel.AbstractChannelHandlerContext#findContextInbound
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}
// io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead(io.netty.channel.AbstractChannelHandlerContext, java.lang.Object)
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
AbstractChannelHandlerContext にも fireChannelRead メソッドがあることがわかりました。このメソッドは、findContextInbound メソッドを呼び出してパイプラインの次のハンドラーを見つけ、次に次のハンドラーの channelRead メソッドを呼び出します。
つまり、Handler の channelRead メソッドで、ChannelHandlerContext の fireChannelRead メソッドを呼び出すと、メッセージが次の Handler に渡されます。
2、アウトバウンド——接続
connect メソッドを使用して、送信ソース コードを分析します。
// io.netty.channel.DefaultChannelPipeline#connect(java.net.SocketAddress, java.net.SocketAddress, io.netty.channel.ChannelPromise)
@Override
public final ChannelFuture connect(
SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
// 从tail开始调用connect事件
return tail.connect(remoteAddress, localAddress, promise);
}
// io.netty.channel.AbstractChannelHandlerContext#connect(java.net.SocketAddress, java.net.SocketAddress, io.netty.channel.ChannelPromise)
@Override
public ChannelFuture connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
if (remoteAddress == null) {
throw new NullPointerException("remoteAddress");
}
if (isNotValidPromise(promise, false)) {
// cancelled
return promise;
}
final AbstractChannelHandlerContext next = findContextOutbound();
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeConnect(remoteAddress, localAddress, promise);
} else {
safeExecute(executor, new Runnable() {
@Override
public void run() {
next.invokeConnect(remoteAddress, localAddress, promise);
}
}, promise, null);
}
return promise;
}
// io.netty.channel.AbstractChannelHandlerContext#invokeConnect
private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
if (invokeHandler()) {
try {
((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
} else {
connect(remoteAddress, localAddress, promise);
}
}
ChannelOutboundHandler の connect メソッドもチェーンで呼び出されていることがわかりました。これはインバウンドとまったく同じです。
3. まとめ
コンテキストはハンドラーをラップし、複数のコンテキストはパイプライン内の双方向リンク リストを形成します。受信方向はヘッド ノードから開始してインバウンドと呼ばれ、送信メソッドは末尾ノードから開始してアウトバウンドと呼ばれます。
ノード間の送信は、AbstractChannelHandlerContext 内の fire シリーズ メソッドを介して行われ、現在のノードの次のノードが検出されて継続的に送信され、ハンドラーのスケジューリングが連鎖的に完了します。
この時点で、リクエストの確立、リクエストの受信、リクエストの処理までのプロセス全体の Netty ソース コードが完全に解析されました。