ナビゲーションをスキップ

4.0における新機能と注目すべき変更点

このページはGitHub Wikiページから自動生成されていることをご存知ですか? こちらから改善にご協力いただけます!

このドキュメントでは、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コンテナで使用できます。

一般的なAPIの変更

  • 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の変更

ChannelBufferByteBuf

上記の構造変更のおかげで、バッファAPIは個別のパッケージとして使用できるようになりました。Nettyをネットワークアプリケーションフレームワークとして採用することに関心がなくても、バッファAPIを使用できます。そのため、型名ChannelBufferはもはや意味をなさず、ByteBufに名前が変更されました。

新しいバッファを作成するユーティリティクラスChannelBuffersは、UnpooledByteBufUtilの2つのユーティリティクラスに分割されました。その名前Unpooledから推測できるように、4.0ではByteBufAllocator実装を介して割り当てることができるプールされたByteBufが導入されました。

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

CompositeByteBufという名前の新しいバッファ実装は、複合バッファ実装のためのさまざまな高度な操作を定義します。ユーザーは、比較的負荷の高いランダムアクセスを犠牲にして、複合バッファを使用してバルクメモリコピー操作を節約できます。新しい複合バッファを作成するには、以前のようにUnpooled.wrappedBuffer(...)Unpooled.compositeBuffer(...)、またはByteBufAllocator.compositeBuffer()を使用します。

予測可能なNIOバッファ変換

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がリモートピアに書き込まれると、それが由来するプールに自動的に返されます。

デフォルトのByteBufAllocatorPooledByteBufAllocatorです。バッファプールを使用しない場合、または独自のallocatorを使用する場合は、UnpooledByteBufAllocatorなどの代替allocatorを使用してChannel.config().setAllocator(...)を使用します。

注:現時点では、デフォルトのallocatorはUnpooledByteBufAllocatorです。 PooledByteBufAllocatorにメモリリークがないことを確認したら、再びデフォルトに戻します。

ByteBufは常に参照カウントされます

ByteBufのライフサイクルをより予測可能な方法で制御するために、Nettyはもはやガベージコレクターに依存せず、明示的な参照カウンターを採用しています。基本的なルールは次のとおりです。

  • バッファが割り当てられると、初期参照カウントは1です。
  • バッファの参照カウントが0に減ると、解放されるか、それが由来するプールに返されます。
  • 以下の試行は、IllegalReferenceCountExceptionをトリガーします。
    • 参照カウントが0のバッファにアクセスする、
    • 参照カウントを負の値に減らす、または
    • 参照カウントをInteger.MAX_VALUEを超えて増やす。
  • 派生バッファ(スライスや複製など)とスワップされたバッファ(リトルエンディアンバッファなど)は、派生元のバッファと参照カウントを共有します。派生バッファが作成されても参照カウントは変更されないことに注意してください。

ByteBufChannelPipelineで使用される場合、覚えておく必要のある追加のルールがあります。

  • パイプライン内の各インバウンド(アップストリームとも呼ばれる)ハンドラーは、受信したメッセージを解放する必要があります。Nettyはそれらを自動的に解放しません。
    • コーデックフレームワークはメッセージを自動的に解放し、ユーザーはメッセージをそのまま次のハンドラーに渡したい場合は参照カウントを増やす必要があることに注意してください。
  • アウトバウンド(ダウンストリームとも呼ばれる)メッセージがパイプラインの先頭に到達すると、Nettyはそれを書き込んだ後に解放します。

自動バッファリーク検出

参照カウントは非常に強力ですが、エラーが発生しやすいです。ユーザーがバッファの解放を忘れた場所を見つけるのを助けるために、リーク検出器は、リークしたバッファが自動的に割り当てられた場所のスタックトレースを記録します。

リーク検出器はPhantomReferenceに依存しており、スタックトレースの取得は非常に負荷の高い操作であるため、割り当ての約1%のみをサンプリングします。したがって、考えられるすべてのリークを見つけるために、アプリケーションをかなり長い時間実行することをお勧めします。

すべてのリークが見つかり、修正されたら。JVMオプション-Dio.netty.noResourceLeakDetectionを指定することで、この機能をオフにして、ランタイムオーバーヘッドを完全に削除できます。

io.netty.util.concurrent

新しいスタンドアロンバッファAPIに加えて、4.0は、io.netty.util.concurrentと呼ばれる新しいパッケージで、一般的な非同期アプリケーションの作成に役立つさまざまな構成要素を提供します。それらの構成要素の一部は次のとおりです。

  • FuturePromise - ChannelFutureに似ていますが、Channelへの依存関係はありません
  • EventExecutorEventExecutorGroup - 汎用イベントループAPI

これらは、このドキュメントで後述するチャネルAPIの基礎として使用されます。たとえば、ChannelFutureio.netty.util.concurrent.Futureを拡張し、EventLoopGroupEventExecutorGroupを拡張します。

Event loop type hierarchy diagram

Channel APIの変更

4.0では、io.netty.channelパッケージの下にある多くのクラスが大幅に改訂されているため、単純なテキスト検索と置換では3.xアプリケーションは4.0では動作しません。このセクションでは、すべての変更の網羅的なリソースとなるのではなく、そのような大きな変更の背後にある思考プロセスを示そうとします。

刷新されたChannelHandlerインターフェース

Upstream → Inbound、Downstream → Outbound

「upstream」と「downstream」という用語は、初心者にとって非常に分かりにくいものでした。4.0では、可能な限り「inbound」と「outbound」を使用します。

新しいChannelHandler型の階層

3.xでは、ChannelHandlerは単なるタグインターフェースであり、ChannelUpstreamHandlerChannelDownstreamHandler、およびLifeCycleAwareChannelHandlerが実際のハンドラメソッドを定義していました。Netty 4では、ChannelHandlerLifeCycleAwareChannelHandlerと、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;
}

次の図は、新しい型の階層を示しています。

ChannelHandler type hierarchy diagram

イベントオブジェクトを持たないChannelHandler

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がトリガーされます:channelOpenchannelBound、およびchannelConnectedChannelが閉じられると、少なくともさらに3つ:channelDisconnectedchannelUnbound、およびchannelClosed

Netty 3 Channel state diagram

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

Netty 4 Channel state diagram

channelOpenchannelBound、およびchannelConnectedchannelActiveに統合されました。channelDisconnectedchannelUnbound、およびchannelClosedchannelInactiveに統合されました。同様に、Channel.isBound()isConnected()isActive()に統合されました。

channelRegisteredchannelUnregisteredは、channelOpenchannelClosedと同等ではないことに注意してください。これらは、以下に示すように、Channelの動的な登録、登録解除、および再登録をサポートするために導入された新しい状態です。

Netty 4 Channel state diagram for re-registration

write()は自動的にフラッシュしません

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_CLOSUREChannelOptionを設定して、SocketChannel.read(..)-1を返してもNettyが自動的に接続を閉じないようにすることができます。

柔軟なI/Oスレッド割り当て

3.xでは、ChannelChannelFactoryによって作成され、新しく作成されたChannelは自動的に非表示のI/Oスレッドに登録されます。4.0では、ChannelFactoryが、1つ以上のEventLoopで構成されるEventLoopGroupという新しいインターフェースに置き換えられました。また、新しいChannelEventLoopGroupに自動的に登録されず、ユーザーは明示的にEventLoopGroup.register()を呼び出す必要があります。

この変更(つまり、ChannelFactoryとI/Oスレッドの分離)のおかげで、ユーザーは異なるChannel実装を同じEventLoopGroupに登録したり、同じChannel実装を異なるEventLoopGroupに登録したりできます。たとえば、NIOサーバーソケット、NIOクライアントソケット、NIO UDPソケット、およびインVMローカルチャネルを同じI/Oスレッドで実行できます。これは、最小限のレイテンシを必要とするプロキシサーバーを作成する場合に非常に役立ちます。

既存のJDKソケットからChannelを作成する機能

3.xでは、java.nio.channels.SocketChannelなどの既存のJDKソケットから新しいChannelを作成する方法はありませんでした。4.0では可能です。

I/OスレッドからのChannelの登録解除と再登録

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);

I/Oスレッドによって実行される任意のタスクのスケジューリング

ChannelEventLoopGroupに登録されると、Channelは実際にはEventLoopGroupによって管理されるEventLoopの1つに登録されます。EventLoopjava.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スレッドを停止させることができます。

タイプセーフなChannelOption

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

AttributeMap

ユーザーの要望に応えて、ChannelChannelHandlerContextに任意のオブジェクトを添付できるようになりました。ChannelChannelHandlerContextが拡張するAttributeMapと呼ばれる新しいインターフェースが追加されました。代わりに、ChannelLocalChannel.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

ブートストラップ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();
    }
}

ChannelPipelineFactoryChannelInitializer

上記の例でお気づきのように、ChannelPipelineFactoryはもうありません。これは、ChannelChannelPipelineの設定をより細かく制御できるChannelInitializerに置き換えられました。

自分で新しいChannelPipelineを作成しないことに注意してください。これまでに報告された多くのユースケースを観察した後、Nettyプロジェクトチームは、ユーザーが独自のパイプライン実装を作成したり、デフォルトの実装を拡張したりすることにはメリットがないと結論付けました。したがって、ChannelPipelineはユーザーによって作成されなくなりました。ChannelPipelineChannelによって自動的に作成されます。

ChannelFutureChannelFutureChannelPromise

ChannelFutureは、ChannelFutureChannelPromiseに分割されました。これにより、非同期操作のコンシューマーとプロデューサーの契約が明確になるだけでなく、返された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によって常に呼び出されます。
  • ハンドラまたはチャネルに割り当てられたEventExecutorEventLoopは、常にシングルスレッドです。
    • ハンドラメソッドは常に同じスレッドによって呼び出されます。
    • マルチスレッドのEventExecutorまたはEventLoopが指定されている場合、スレッドの1つが最初に選択され、選択されたスレッドは登録解除まで使用されます。
    • 同じパイプライン内の2つのハンドラに異なるEventExecutorが割り当てられている場合、それらは同時に呼び出されます。同じパイプライン内のハンドラによってのみ共有データがアクセスされる場合でも、複数のハンドラが共有データにアクセスする場合、ユーザーはスレッドの安全性を考慮する必要があります。
  • ChannelFutureに追加されたChannelFutureListenersは、常にfutureに関連付けられたChannelに割り当てられたEventLoopスレッドによって呼び出されます。
  • ChannelHandlerInvokerを使用して、Channelイベントの順序を制御できます。 DefaultChannelHandlerInvokerは、EventLoopスレッドからのイベントをすぐに実行し、他のスレッドからのイベントをRunnableオブジェクトとしてEventExecutor上で実行します。 EventLoopスレッドと他のスレッドからChannelを操作する場合の影響については、以下の例を参照してください。(この機能は削除されました。 関連するコミットを参照してください)
書き込み順序 - EventLoopスレッドと他のスレッドの混在
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.

ExecutionHandlerはもうありません - コアに組み込まれています。

ChannelHandlerChannelPipelineに追加するときに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パッケージに移動されました。
  • FrameDecoderByteToMessageDecoderに名前が変更されました。
  • OneToOneEncoderOneToOneDecoderは、MessageToMessageEncoderMessageToMessageDecoderに置き換えられました。
  • decode()decodeLast()encode()のメソッドシグネチャは、ジェネリクスをサポートし、冗長なパラメータを削除するためにわずかに変更されました。

コーデックエンベッダー → EmbeddedChannel

コーデックエンベッダーはio.netty.channel.embedded.EmbeddedChannelに置き換えられ、ユーザーはコーデックを含むあらゆる種類のパイプラインをテストできるようになりました。

HTTPコーデック

HTTPデコーダーは、単一のHTTPメッセージごとに常に複数のメッセージオブジェクトを生成するようになりました

1       * HttpRequest / HttpResponse
0 - n   * HttpContent
1       * LastHttpContent

詳細については、更新されたHttpSnoopServerの例を参照してください。単一のHTTPメッセージに対して複数のメッセージを処理したくない場合は、パイプラインにHttpObjectAggregatorを配置できます。 HttpObjectAggregatorは、複数のメッセージを単一のFullHttpRequestまたはFullHttpResponseに変換します。

トランスポート実装の変更

以下のトランスポートが新たに追加されました

  • OIO SCTPトランスポート
  • UDTトランスポート

ケーススタディ:Factorialの例の移植

このセクションでは、Factorialの例を3.xから4.0に移植する大まかな手順を示します。 Factorialの例は、io.netty.example.factorialパッケージで既に4.0に移植されています。変更されたすべてのビットを見つけるには、例のソースコードを参照してください。

サーバーの移植

  1. 新しいブートストラップAPIを使用するようにFactorialServer.run()メソッドを書き直します。
  2. ChannelFactoryはもうありません。 NioEventLoopGroup(1つは着信接続を受け入れるため、もう1つは受け入れられた接続を処理するため)を自分でインスタンス化します。
  3. FactorialServerPipelineFactoryの名前をFactorialServerInitializerに変更します。
  4. ChannelInitializer<Channel>を拡張するようにします。
  5. 新しいChannelPipelineを作成する代わりに、Channel.pipeline()を介して取得します。
  6. FactorialServerHandlerChannelInboundHandlerAdapterを拡張するようにします。
  7. channelDisconnected()channelInactive()に置き換えます。
  8. handleUpstream()は使用されなくなりました。
  9. messageReceived()の名前をchannelRead()に変更し、メソッドシグネチャをそれに応じて調整します。
  10. ctx.write()ctx.writeAndFlush()に置き換えます。
  11. BigIntegerDecoderByteToMessageDecoder<BigInteger>を拡張するようにします。
  12. NumberEncoderMessageToByteEncoder<Number>を拡張するようにします。
  13. encode()はバッファを返しません。エンコードされたデータをByteToMessageDecoderによって提供されたバッファに書き込みます。

クライアントの移植

サーバーの移植とほぼ同じですが、潜在的に大きなストリームを書き込む場合は注意が必要です。

  1. 新しいブートストラップAPIを使用するようにFactorialClient.run()メソッドを書き直します。
  2. FactorialClientPipelineFactoryの名前をFactorialClientInitializerに変更します。
  3. FactorialClientHandlerChannelInboundHandlerAdapterを拡張するようにします
最終取得日:2024年7月19日