4.0における新機能と注目すべき変更点
このドキュメントでは、Nettyのメジャーリリースにおける注目すべき変更点と新機能の一覧を紹介し、アプリケーションを新しいバージョンに移植するためのアイデアを提供します。
Nettyのパッケージ名は、JBoss.orgから独立したため、org.jboss.nettyからio.nettyに変更されました。
バイナリJARは複数のサブモジュールに分割され、ユーザーがクラスパスから不要な機能を除外できるようになりました。現在の構造は以下のとおりです。
| アーティファクトID | 説明 |
|---|---|
netty-parent |
Maven 親POM |
netty-common |
ユーティリティクラスとロギングファサード |
netty-buffer |
java.nio.ByteBufferを置き換えるByteBuf API |
netty-transport |
Channel APIとコントランスポート |
netty-transport-rxtx |
Rxtx トランスポート |
netty-transport-sctp |
SCTP トランスポート |
netty-transport-udt |
UDT トランスポート |
netty-handler |
便利なChannelHandler実装 |
netty-codec |
エンコーダーとデコーダーの作成に役立つコーデックフレームワーク |
netty-codec-http |
HTTP、Web Sockets、SPDY、RTSP関連のコーデック |
netty-codec-socks |
SOCKSプロトコル関連のコーデック |
netty-all |
上記のすべてのアーティファクトを組み合わせたオールインワンJAR |
netty-tarball |
Tarball配布 |
netty-example |
例 |
netty-testsuite-* |
統合テストのコレクション |
netty-microbench |
マイクロベンチマーク |
すべてのアーティファクト(netty-all.jarを除く)は、OSGiバンドルとなり、お好みのOSGiコンテナで使用できます。
- Nettyのほとんどの操作で、簡潔にするためにメソッドチェーンがサポートされるようになりました。
- 設定以外のゲッターには、
get-プレフィックスがなくなりました。(例:Channel.getRemoteAddress()→Channel.remoteAddress())- ブール値のプロパティには、混乱を避けるために
is-プレフィックスが付いたままです。(例:'empty'は形容詞と動詞の両方なので、empty()は2つの意味を持つ可能性があります。)
- ブール値のプロパティには、混乱を避けるために
- 4.0 CR4と4.0 CR5間のAPI変更については、Netty 4.0.0.CR5 released with new-new APIを参照してください。
上記の構造変更のおかげで、バッファAPIは個別のパッケージとして使用できるようになりました。Nettyをネットワークアプリケーションフレームワークとして採用することに関心がなくても、バッファAPIを使用できます。そのため、型名ChannelBufferはもはや意味をなさず、ByteBufに名前が変更されました。
新しいバッファを作成するユーティリティクラスChannelBuffersは、UnpooledとByteBufUtilの2つのユーティリティクラスに分割されました。その名前Unpooledから推測できるように、4.0ではByteBufAllocator実装を介して割り当てることができるプールされたByteBufが導入されました。
内部パフォーマンステストによると、ByteBufをインターフェースから抽象クラスに変換することで、全体的なスループットが約5%向上しました。
3.xでは、バッファは固定または動的でした。固定バッファの容量は、作成後に変更されませんが、動的バッファの容量は、そのwrite*(...)メソッドがより多くのスペースを必要とするたびに変化します。
4.0以降、すべてのバッファは動的です。ただし、以前の動的バッファよりも優れています。バッファの容量をより簡単かつ安全に減らすことも増やすこともできます。新しいメソッドByteBuf.capacity(int newCapacity)があるため、簡単です。バッファの最大容量を設定して、際限なく増加しないようにできるため、安全です。
// No more dynamicBuffer() - use buffer().
ByteBuf buf = Unpooled.buffer();
// Increase the capacity of the buffer.
buf.capacity(1024);
...
// Decrease the capacity of the buffer (the last 512 bytes are deleted.)
buf.capacity(512);唯一の例外は、wrappedBuffer()によって作成された、単一のバッファまたは単一のバイト配列をラップするバッファです。既存のバッファをラップするという要点、つまりメモリコピーの節約が無効になるため、容量を増やすことはできません。バッファをラップした後に容量を変更する場合は、十分な容量を持つ新しいバッファを作成し、ラップするバッファをコピーする必要があります。
CompositeByteBufという名前の新しいバッファ実装は、複合バッファ実装のためのさまざまな高度な操作を定義します。ユーザーは、比較的負荷の高いランダムアクセスを犠牲にして、複合バッファを使用してバルクメモリコピー操作を節約できます。新しい複合バッファを作成するには、以前のようにUnpooled.wrappedBuffer(...)、Unpooled.compositeBuffer(...)、またはByteBufAllocator.compositeBuffer()を使用します。
3.xでは、ChannelBuffer.toByteBuffer()とそのバリアントの契約は、十分に決定論的ではありませんでした。ユーザーは、共有データを持つビューバッファまたは個別のデータを持つコピーバッファのどちらが返されるかを知ることができませんでした。4.0では、toByteBuffer()がByteBuf.nioBufferCount()、nioBuffer()、およびnioBuffers()に置き換えられました。nioBufferCount()が0を返す場合、ユーザーは常にcopy().nioBuffer()を呼び出すことによってコピーされたバッファを取得できます。
リトルエンディアンのサポートが大幅に変更されました。以前は、ユーザーはリトルエンディアンバッファを取得するために、LittleEndianHeapChannelBufferFactoryを指定するか、既存のバッファを目的のバイト順でラップする必要がありました。4.0では、新しいメソッドByteBuf.order(ByteOrder)が追加されました。これは、目的のバイト順で呼び出し元のビューを返します。
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.ByteOrder;
ByteBuf buf = Unpooled.buffer(4);
buf.setInt(0, 1);
// Prints '00000001'
System.out.format("%08x%n", buf.getInt(0));
ByteBuf leBuf = buf.order(ByteOrder.LITTLE_ENDIAN);
// Prints '01000000'
System.out.format("%08x%n", leBuf.getInt(0));
assert buf != leBuf;
assert buf == buf.order(ByteOrder.BIG_ENDIAN);Netty 4は、jemallocのバリアントである高性能バッファプールを導入しました。これは、バディ割り当てとスラブ割り当てを組み合わせたものです。これにより、以下の利点があります。
- バッファの頻繁な割り当てと解放によって発生するGC圧力の軽減
- 必然的にゼロで埋められる必要がある新しいバッファを作成する際に発生するメモリ帯域幅消費の削減
- ダイレクトバッファのタイムリーな解放
この機能を利用するには、ユーザーがプールされていないバッファを取得したい場合を除き、ByteBufAllocatorからバッファを取得する必要があります。
Channel channel = ...;
ByteBufAllocator alloc = channel.alloc();
ByteBuf buf = alloc.buffer(512);
....
channel.write(buf);
ChannelHandlerContext ctx = ...
ByteBuf buf2 = ctx.alloc().buffer(512);
....
channel.write(buf2)ByteBufがリモートピアに書き込まれると、それが由来するプールに自動的に返されます。
デフォルトのByteBufAllocatorはPooledByteBufAllocatorです。バッファプールを使用しない場合、または独自のallocatorを使用する場合は、UnpooledByteBufAllocatorなどの代替allocatorを使用してChannel.config().setAllocator(...)を使用します。
注:現時点では、デフォルトのallocatorはUnpooledByteBufAllocatorです。 PooledByteBufAllocatorにメモリリークがないことを確認したら、再びデフォルトに戻します。
ByteBufのライフサイクルをより予測可能な方法で制御するために、Nettyはもはやガベージコレクターに依存せず、明示的な参照カウンターを採用しています。基本的なルールは次のとおりです。
- バッファが割り当てられると、初期参照カウントは1です。
- バッファの参照カウントが0に減ると、解放されるか、それが由来するプールに返されます。
- 以下の試行は、
IllegalReferenceCountExceptionをトリガーします。- 参照カウントが0のバッファにアクセスする、
- 参照カウントを負の値に減らす、または
- 参照カウントを
Integer.MAX_VALUEを超えて増やす。
- 派生バッファ(スライスや複製など)とスワップされたバッファ(リトルエンディアンバッファなど)は、派生元のバッファと参照カウントを共有します。派生バッファが作成されても参照カウントは変更されないことに注意してください。
ByteBufがChannelPipelineで使用される場合、覚えておく必要のある追加のルールがあります。
- パイプライン内の各インバウンド(アップストリームとも呼ばれる)ハンドラーは、受信したメッセージを解放する必要があります。Nettyはそれらを自動的に解放しません。
- コーデックフレームワークはメッセージを自動的に解放し、ユーザーはメッセージをそのまま次のハンドラーに渡したい場合は参照カウントを増やす必要があることに注意してください。
- アウトバウンド(ダウンストリームとも呼ばれる)メッセージがパイプラインの先頭に到達すると、Nettyはそれを書き込んだ後に解放します。
参照カウントは非常に強力ですが、エラーが発生しやすいです。ユーザーがバッファの解放を忘れた場所を見つけるのを助けるために、リーク検出器は、リークしたバッファが自動的に割り当てられた場所のスタックトレースを記録します。
リーク検出器はPhantomReferenceに依存しており、スタックトレースの取得は非常に負荷の高い操作であるため、割り当ての約1%のみをサンプリングします。したがって、考えられるすべてのリークを見つけるために、アプリケーションをかなり長い時間実行することをお勧めします。
すべてのリークが見つかり、修正されたら。JVMオプション-Dio.netty.noResourceLeakDetectionを指定することで、この機能をオフにして、ランタイムオーバーヘッドを完全に削除できます。
新しいスタンドアロンバッファAPIに加えて、4.0は、io.netty.util.concurrentと呼ばれる新しいパッケージで、一般的な非同期アプリケーションの作成に役立つさまざまな構成要素を提供します。それらの構成要素の一部は次のとおりです。
-
FutureとPromise-ChannelFutureに似ていますが、Channelへの依存関係はありません -
EventExecutorとEventExecutorGroup- 汎用イベントループAPI
これらは、このドキュメントで後述するチャネルAPIの基礎として使用されます。たとえば、ChannelFutureはio.netty.util.concurrent.Futureを拡張し、EventLoopGroupはEventExecutorGroupを拡張します。

4.0では、io.netty.channelパッケージの下にある多くのクラスが大幅に改訂されているため、単純なテキスト検索と置換では3.xアプリケーションは4.0では動作しません。このセクションでは、すべての変更の網羅的なリソースとなるのではなく、そのような大きな変更の背後にある思考プロセスを示そうとします。
「upstream」と「downstream」という用語は、初心者にとって非常に分かりにくいものでした。4.0では、可能な限り「inbound」と「outbound」を使用します。
3.xでは、ChannelHandlerは単なるタグインターフェースであり、ChannelUpstreamHandler、ChannelDownstreamHandler、およびLifeCycleAwareChannelHandlerが実際のハンドラメソッドを定義していました。Netty 4では、ChannelHandlerはLifeCycleAwareChannelHandlerと、inboundハンドラとoutboundハンドラの両方にとって便利なメソッドをいくつか統合しています。
public interface ChannelHandler {
void handlerAdded(ChannelHandlerContext ctx) throws Exception;
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}次の図は、新しい型の階層を示しています。

3.xでは、すべてのI/O操作でChannelEventオブジェクトが作成されました。さらに、読み取り/書き込みごとに新しいChannelBufferが作成されました。リソース管理とバッファプールをJVMに委任することで、Nettyの内部構造がかなり簡素化されました。しかし、高負荷下でNettyベースのアプリケーションでしばしば観察されるGC圧力と不確実性の根本原因となることがよくありました。
4.0では、イベントオブジェクトを強く型付けされたメソッド呼び出しに置き換えることで、イベントオブジェクトの作成をほぼ完全に排除しています。3.xには、handleUpstream()やhandleDownstream()などのキャッチオールイベントハンドラメソッドがありましたが、これはもはや当てはまりません。すべてのイベントタイプには、独自のハンドラメソッドがあります。
// Before:
void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e);
void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e);
// After:
void channelRegistered(ChannelHandlerContext ctx);
void channelUnregistered(ChannelHandlerContext ctx);
void channelActive(ChannelHandlerContext ctx);
void channelInactive(ChannelHandlerContext ctx);
void channelRead(ChannelHandlerContext ctx, Object message);
void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise);
void connect(
ChannelHandlerContext ctx, SocketAddress remoteAddress,
SocketAddress localAddress, ChannelPromise promise);
void disconnect(ChannelHandlerContext ctx, ChannelPromise promise);
void close(ChannelHandlerContext ctx, ChannelPromise promise);
void deregister(ChannelHandlerContext ctx, ChannelPromise promise);
void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise);
void flush(ChannelHandlerContext ctx);
void read(ChannelHandlerContext ctx);ChannelHandlerContextも、上記の変更を反映するように変更されました。
// Before:
ctx.sendUpstream(evt);
// After:
ctx.fireChannelRead(receivedMessage);これらの変更はすべて、ユーザーがもはや存在しないChannelEventインターフェースを拡張できないことを意味します。では、ユーザーはIdleStateEventなど、独自のイベントタイプをどのように定義するのでしょうか?4.0のChannelHandlerContextには、カスタムイベントをトリガーするためのfireUserEventTriggeredメソッドがあり、ChannelInboundHandlerには、カスタムイベントの処理という特定のユースケース専用のuserEventTriggered()というハンドラメソッドがあります。
3.xで新しい接続済みChannelが作成されると、少なくとも3つのChannelStateEventがトリガーされます:channelOpen、channelBound、およびchannelConnected。Channelが閉じられると、少なくともさらに3つ:channelDisconnected、channelUnbound、およびchannelClosed。

しかし、それほど多くのイベントをトリガーすることには疑問符が付きます。ユーザーにとっては、Channelが読み取りと書き込みを実行できる状態になったときに通知を受ける方が便利です。

channelOpen、channelBound、およびchannelConnectedはchannelActiveに統合されました。channelDisconnected、channelUnbound、およびchannelClosedはchannelInactiveに統合されました。同様に、Channel.isBound()とisConnected()はisActive()に統合されました。
channelRegisteredとchannelUnregisteredは、channelOpenとchannelClosedと同等ではないことに注意してください。これらは、以下に示すように、Channelの動的な登録、登録解除、および再登録をサポートするために導入された新しい状態です。

4.0では、Channelのアウトバウンドバッファを明示的にフラッシュするflush()という新しい操作が導入され、write()操作は自動的にフラッシュしなくなりました。これは、メッセージレベルで動作することを除けば、java.io.BufferedOutputStreamと考えることができます。
この変更により、何かを書き込んだ後にctx.flush()を呼び出すことを忘れないように十分注意する必要があります。代わりに、ショートカットメソッドwriteAndFlush()を使用することもできます。
3.xには、Channel.setReadable(boolean)によって提供される直感に反するインバウンドトラフィック停止メカニズムがありました。ChannelHandler間の複雑な相互作用が発生し、ハンドラが正しく実装されていない場合、互いに干渉しやすくなりました。
4.0では、read()と呼ばれる新しいアウトバウンド操作が追加されました。Channel.config().setAutoRead(false)でデフォルトの自動読み取りフラグをオフにすると、read()操作を明示的に呼び出すまで、Nettyは何も読み取りません。発行したread()操作が完了し、チャネルが再び読み取りを停止すると、channelReadSuspended()というインバウンドイベントがトリガーされるため、別のread()操作を再発行できます。また、read()操作をインターセプトして、より高度なトラフィック制御を実行することもできます。
I/Oスレッドをブロックするか、サーバーソケットを閉じる以外に、ユーザーがNetty 3.xに着信接続の受け入れを停止するように指示する方法はありませんでした。4.0は、通常のチャネルと同様に、自動読み取りフラグが設定されていない場合、read()操作を尊重します。
TCPとSCTPでは、ユーザーはソケットを完全に閉じずに、ソケットのアウトバウンドトラフィックをシャットダウンできます。このようなソケットは「ハーフクローズドソケット」と呼ばれ、ユーザーはSocketChannel.shutdownOutput()メソッドを呼び出すことでハーフクローズドソケットを作成できます。リモートピアがアウトバウンドトラフィックをシャットダウンすると、SocketChannel.read(..)は-1を返し、これは接続が閉じられた状態と区別がつきませんでした。
3.xにはshutdownOutput()操作がありませんでした。また、SocketChannel.read(..)が-1を返すと、常に接続を閉じました。
ハーフクローズドソケットをサポートするために、4.0はSocketChannel.shutdownOutput()メソッドを追加し、ユーザーは「ALLOW_HALF_CLOSURE」ChannelOptionを設定して、SocketChannel.read(..)が-1を返してもNettyが自動的に接続を閉じないようにすることができます。
3.xでは、ChannelはChannelFactoryによって作成され、新しく作成されたChannelは自動的に非表示のI/Oスレッドに登録されます。4.0では、ChannelFactoryが、1つ以上のEventLoopで構成されるEventLoopGroupという新しいインターフェースに置き換えられました。また、新しいChannelはEventLoopGroupに自動的に登録されず、ユーザーは明示的にEventLoopGroup.register()を呼び出す必要があります。
この変更(つまり、ChannelFactoryとI/Oスレッドの分離)のおかげで、ユーザーは異なるChannel実装を同じEventLoopGroupに登録したり、同じChannel実装を異なるEventLoopGroupに登録したりできます。たとえば、NIOサーバーソケット、NIOクライアントソケット、NIO UDPソケット、およびインVMローカルチャネルを同じI/Oスレッドで実行できます。これは、最小限のレイテンシを必要とするプロキシサーバーを作成する場合に非常に役立ちます。
3.xでは、java.nio.channels.SocketChannelなどの既存のJDKソケットから新しいChannelを作成する方法はありませんでした。4.0では可能です。
3.xで新しいChannelが作成されると、基盤となるソケットが閉じられるまで、単一のI/Oスレッドに完全に結び付けられます。4.0では、ユーザーはI/OスレッドからChannelの登録を解除して、基盤となるJDKソケットを完全に制御できます。たとえば、Nettyが提供する高レベルのノンブロッキングI/Oを利用して複雑なプロトコルを処理し、後でChannelの登録を解除してブロッキングモードに切り替えて、可能な限り最大のスループットでファイルを転送できます。もちろん、登録解除されたChannelを再び登録することも可能です。
java.nio.channels.FileChannel myFile = ...;
java.nio.channels.SocketChannel mySocket = java.nio.channels.SocketChannel.open();
// Perform some blocking operation here.
...
// Netty takes over.
SocketChannel ch = new NioSocketChannel(mySocket);
EventLoopGroup group = ...;
group.register(ch);
...
// Deregister from Netty.
ch.deregister().sync();
// Perform some blocking operation here.
mySocket.configureBlocking(true);
myFile.transferFrom(mySocket, ...);
// Register back again to another event loop group.
EventLoopGroup anotherGroup = ...;
anotherGroup.register(ch);ChannelがEventLoopGroupに登録されると、Channelは実際にはEventLoopGroupによって管理されるEventLoopの1つに登録されます。EventLoopはjava.util.concurrent.ScheduledExecutorServiceを実装しています。これは、ユーザーが自分のチャネルが属するI/Oスレッドで任意のRunnableまたはCallableを実行またはスケジュールできることを意味します。後で説明する、新しく明確に定義されたスレッドモデルと合わせて、スレッドセーフなハンドラを記述することが非常に容易になりました。
public class MyHandler extends ChannelOutboundHandlerAdapter {
...
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise p) {
...
ctx.write(msg, p);
// Schedule a write timeout.
ctx.executor().schedule(new MyWriteTimeoutTask(p), 30, TimeUnit.SECONDS);
...
}
}
public class Main {
public static void main(String[] args) throws Exception {
// Run an arbitrary task from an I/O thread.
Channel ch = ...;
ch.executor().execute(new Runnable() { ... });
}
}releaseExternalResources()はもうありません。EventLoopGroup.shutdownGracefully()を呼び出すことで、開いているすべてのチャネルをすぐに閉じて、すべてのI/Oスレッドを停止させることができます。
Nettyでは、Channelのソケットパラメータを設定するには、2つの方法があります。1つは、SocketChannelConfig.setTcpNoDelay(true)のように、ChannelConfigのセッターを明示的に呼び出すことです。これは最もタイプセーフな方法です。もう1つは、ChannelConfig.setOption()メソッドを呼び出すことです。実行時に設定するソケットオプションを決定する必要がある場合があり、このメソッドはそのような場合に最適です。ただし、3.xでは、ユーザーはオプションを文字列とオブジェクトのペアとして指定する必要があるため、エラーが発生しやすくなります。ユーザーが間違ったオプション名または値で呼び出すと、ClassCastExceptionが発生するか、指定されたオプションが無視される可能性があります。
4.0では、ソケットオプションへのタイプセーフなアクセスを提供するChannelOptionという新しいタイプが導入されました。
ChannelConfig cfg = ...;
// Before:
cfg.setOption("tcpNoDelay", true);
cfg.setOption("tcpNoDelay", 0); // Runtime ClassCastException
cfg.setOption("tcpNoDelays", true); // Typo in the option name - ignored silently
// After:
cfg.setOption(ChannelOption.TCP_NODELAY, true);
cfg.setOption(ChannelOption.TCP_NODELAY, 0); // Compile errorユーザーの要望に応えて、ChannelとChannelHandlerContextに任意のオブジェクトを添付できるようになりました。ChannelとChannelHandlerContextが拡張するAttributeMapと呼ばれる新しいインターフェースが追加されました。代わりに、ChannelLocalとChannel.attachmentは削除されました。属性は、関連付けられたChannelがガベージコレクションされるときにガベージコレクションされます。
public class MyHandler extends ChannelInboundHandlerAdapter {
private static final AttributeKey<MyState> STATE =
AttributeKey.valueOf("MyHandler.state");
@Override
public void channelRegistered(ChannelHandlerContext ctx) {
ctx.attr(STATE).set(new MyState());
ctx.fireChannelRegistered();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
MyState state = ctx.attr(STATE).get();
}
...
}ブートストラップAPIは、その目的は同じままであるにもかかわらず、ゼロから書き直されました。サーバーまたはクライアントを稼働させるために必要な典型的な手順を実行し、多くの場合、定型コードで見られます。
新しいブートストラップも流れるようなインターフェースを採用しています。
public static void main(String[] args) throws Exception {
// Configure the server.
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.localAddress(8080)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(handler1, handler2, ...);
}
});
// Start the server.
ChannelFuture f = b.bind().sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
// Wait until all threads are terminated.
bossGroup.terminationFuture().sync();
workerGroup.terminationFuture().sync();
}
}上記の例でお気づきのように、ChannelPipelineFactoryはもうありません。これは、ChannelとChannelPipelineの設定をより細かく制御できるChannelInitializerに置き換えられました。
自分で新しいChannelPipelineを作成しないことに注意してください。これまでに報告された多くのユースケースを観察した後、Nettyプロジェクトチームは、ユーザーが独自のパイプライン実装を作成したり、デフォルトの実装を拡張したりすることにはメリットがないと結論付けました。したがって、ChannelPipelineはユーザーによって作成されなくなりました。ChannelPipelineはChannelによって自動的に作成されます。
ChannelFutureは、ChannelFutureとChannelPromiseに分割されました。これにより、非同期操作のコンシューマーとプロデューサーの契約が明確になるだけでなく、返されたChannelFutureをチェーン(フィルタリングなど)で使用することがより安全になります。これは、ChannelFutureの状態を変更できないためです。
この変更により、一部のメソッドは、状態を変更するためにChannelFutureではなくChannelPromiseを受け入れるようになりました。
3.5でその不整合を修正しようとした試みはありましたが、3.xには明確に定義されたスレッドモデルはありません。4.0は、スレッドの安全性をあまり心配することなく、ユーザーがChannelHandlerを記述するのに役立つ厳密なスレッドモデルを定義しています。
ChannelHandlerが@Sharableでアノテーションされていない限り、NettyはChannelHandlerのメソッドを同時に呼び出しません。これは、ハンドラメソッドのタイプ(インバウンド、アウトバウンド、またはライフサイクルイベントハンドラメソッド)に関係なく適用されます。- ユーザーは、インバウンドまたはアウトバウンドイベントハンドラメソッドを同期する必要がなくなりました。
- 4.0では、
@Sharableでアノテーションされていない限り、ChannelHandlerを複数回追加することはできません。
- Nettyによって行われた各
ChannelHandlerメソッド呼び出しの間に、常にhappens-before関係があります。- ユーザーは、ハンドラの状態を維持するために
volatileフィールドを定義する必要はありません。
- ユーザーは、ハンドラの状態を維持するために
- ユーザーは、
ChannelPipelineにハンドラを追加するときに、EventExecutorを指定できます。- 指定した場合、
ChannelHandlerのハンドラメソッドは、常に指定されたEventExecutorによって呼び出されます。 - 指定されていない場合、ハンドラメソッドは、関連付けられた
Channelが登録されているEventLoopによって常に呼び出されます。
- 指定した場合、
-
ハンドラまたはチャネルに割り当てられた
EventExecutorとEventLoopは、常にシングルスレッドです。- ハンドラメソッドは常に同じスレッドによって呼び出されます。
- マルチスレッドの
EventExecutorまたはEventLoopが指定されている場合、スレッドの1つが最初に選択され、選択されたスレッドは登録解除まで使用されます。 - 同じパイプライン内の2つのハンドラに異なる
EventExecutorが割り当てられている場合、それらは同時に呼び出されます。同じパイプライン内のハンドラによってのみ共有データがアクセスされる場合でも、複数のハンドラが共有データにアクセスする場合、ユーザーはスレッドの安全性を考慮する必要があります。
ChannelFutureに追加されたChannelFutureListenersは、常にfutureに関連付けられたChannelに割り当てられたEventLoopスレッドによって呼び出されます。-
ChannelHandlerInvokerを使用して、Channelイベントの順序を制御できます。DefaultChannelHandlerInvokerは、EventLoopスレッドからのイベントをすぐに実行し、他のスレッドからのイベントをRunnableオブジェクトとしてEventExecutor上で実行します。EventLoopスレッドと他のスレッドからChannelを操作する場合の影響については、以下の例を参照してください。(この機能は削除されました。 関連するコミットを参照してください)
Channel ch = ...;
ByteBuf a, b, c = ...;
// From Thread 1 - Not the EventLoop thread
ch.write(a);
ch.write(b);
// .. some other stuff happens
// From EventLoop Thread
ch.write(c);
// The order a, b, and c will be written to the underlying transport is not well
// defined. If order is important, and this threading interaction occurs, it is
// the user's responsibility to enforce ordering.ChannelHandlerをChannelPipelineに追加するときにEventExecutorを指定して、追加されたChannelHandlerのハンドラメソッドを常に指定されたEventExecutor経由で呼び出すようにパイプラインに指示できます。
Channel ch = ...;
ChannelPipeline p = ch.pipeline();
EventExecutor e1 = new DefaultEventExecutor(16);
EventExecutor e2 = new DefaultEventExecutor(8);
p.addLast(new MyProtocolCodec());
p.addLast(e1, new MyDatabaseAccessingHandler());
p.addLast(e2, new MyHardDiskAccessingHandler());4.0ではハンドラが独自のバッファを作成および管理する必要があるため、コーデックフレームワークに大きな内部変更がありました(このドキュメントのハンドラごとのバッファのセクションを参照)。ただし、ユーザーの観点からは変更はそれほど大きくありません。
- コアコーデッククラスは
io.netty.handler.codecパッケージに移動されました。 -
FrameDecoderはByteToMessageDecoderに名前が変更されました。 -
OneToOneEncoderとOneToOneDecoderは、MessageToMessageEncoderとMessageToMessageDecoderに置き換えられました。 decode()、decodeLast()、encode()のメソッドシグネチャは、ジェネリクスをサポートし、冗長なパラメータを削除するためにわずかに変更されました。
コーデックエンベッダーはio.netty.channel.embedded.EmbeddedChannelに置き換えられ、ユーザーはコーデックを含むあらゆる種類のパイプラインをテストできるようになりました。
HTTPデコーダーは、単一のHTTPメッセージごとに常に複数のメッセージオブジェクトを生成するようになりました
1 * HttpRequest / HttpResponse
0 - n * HttpContent
1 * LastHttpContent
詳細については、更新されたHttpSnoopServerの例を参照してください。単一のHTTPメッセージに対して複数のメッセージを処理したくない場合は、パイプラインにHttpObjectAggregatorを配置できます。 HttpObjectAggregatorは、複数のメッセージを単一のFullHttpRequestまたはFullHttpResponseに変換します。
以下のトランスポートが新たに追加されました
- OIO SCTPトランスポート
- UDTトランスポート
このセクションでは、Factorialの例を3.xから4.0に移植する大まかな手順を示します。 Factorialの例は、io.netty.example.factorialパッケージで既に4.0に移植されています。変更されたすべてのビットを見つけるには、例のソースコードを参照してください。
- 新しいブートストラップAPIを使用するように
FactorialServer.run()メソッドを書き直します。 ChannelFactoryはもうありません。NioEventLoopGroup(1つは着信接続を受け入れるため、もう1つは受け入れられた接続を処理するため)を自分でインスタンス化します。FactorialServerPipelineFactoryの名前をFactorialServerInitializerに変更します。ChannelInitializer<Channel>を拡張するようにします。- 新しい
ChannelPipelineを作成する代わりに、Channel.pipeline()を介して取得します。 FactorialServerHandlerがChannelInboundHandlerAdapterを拡張するようにします。channelDisconnected()をchannelInactive()に置き換えます。- handleUpstream()は使用されなくなりました。
messageReceived()の名前をchannelRead()に変更し、メソッドシグネチャをそれに応じて調整します。ctx.write()をctx.writeAndFlush()に置き換えます。BigIntegerDecoderがByteToMessageDecoder<BigInteger>を拡張するようにします。NumberEncoderがMessageToByteEncoder<Number>を拡張するようにします。-
encode()はバッファを返しません。エンコードされたデータをByteToMessageDecoderによって提供されたバッファに書き込みます。
サーバーの移植とほぼ同じですが、潜在的に大きなストリームを書き込む場合は注意が必要です。
- 新しいブートストラップAPIを使用するように
FactorialClient.run()メソッドを書き直します。 FactorialClientPipelineFactoryの名前をFactorialClientInitializerに変更します。FactorialClientHandlerがChannelInboundHandlerAdapterを拡張するようにします