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
を拡張するようにします