Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

How to Migrate Netty 3 to 4 (Netty 番外編)

こんにちは!新しい物好きなエンジニアのツカノ(@snuffkin)です。

強い風を吹かせて台風が通り過ぎましたが、皆さんはいかがお過ごしでしょうか。
このブログで紹介しているVert.xもそうですが、Nettyは様々なプロダクトで採用され、追い風を感じる状況です。はい、今回は久しぶりのNettyです。

これまでのおさらい

さて、これまで何回かに分けてNettyの話をしてきました。まずは、簡単に振り返ってみましょう。

導入編ではEchoサーバ的なプロトコルを使って、Nettyの使いやすさ・見通しの良さについて説明しました。Nettyを使うとどんな実装になるか分かったと思います。
次に、実践編ではオリジナルなプロトコルを実装する方法について説明しました。オリジナルなプロトコルを見通し良く書けるのはNettyの良い点でしたね。
また、Nettyの概要や利用する動機については、以下のページにまとめてあります。(これが第0回みたいなものです^^)

今回のテーマ

さて、それでは本題に入ります。これまで何度か、Netty4をチラつかせてきましたが、ず~っとNetty3の話ばかりしてきました。いよいよ今回こそNetty4の登場です。

Netty3からNetty4になるにあたって、性能周りで改善が行われています。特に、GC周りは大幅に改善されており、Twitter社のブログ「Netty 4 at Twitter: Reduced GC Overhead | Twitter Blogs」によると、GCによる停止時間が5分の1になったそうです。これに加えて、API自体の改善も行われているため、Netty3とNetty4では互換性がなくなっています。Nettyのjarを入れ替えただけ、単純にコード置換しただけ、では移行できなくなってまいす。

そこで、今回はNetty3からNetty4への移行方法について、説明します。


題材は、実践編で利用したコードです。これをNetty4に置き換えます。なので、Netty4でオリジナルなプロトコルの実装方法はこれを見てもらえれば分かるようになってます^^

Netty3 Netty4
実践編 番外編(今回です)


移行に際しては、以下にリンクを記したNetty公式ページの説明が参考になると思います。このブログでは、公式ページにはあまり詳しく書いていない部分も書いてありますので、是非ご参考ください。

Netty.docs: User guide for 4.x Netty4の概要・使い方が書いてあります。
Netty.docs: New and noteworthy Netty3から4への移行について書いてあります。

ここでは、次の順序で移行していきます。

  1. pom.xml
  2. packeage
  3. Decoder
  4. Encoder
  5. Handler
  6. Bootstrap

pom.xml

まずは、Netty4を利用するためのpom.xmlです。Netty4ではコンポーネント毎にjarが分かれました。実践編で説明した範囲だと、pom.xmlのdependenciesに以下を記述すれば良いです。

    <dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-common</artifactId>
            <version>4.0.10.Final</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-buffer</artifactId>
            <version>4.0.10.Final</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-transport</artifactId>
            <version>4.0.10.Final</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-handler</artifactId>
            <version>4.0.10.Final</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-codec</artifactId>
            <version>4.0.10.Final</version>
        </dependency>
    </dependencies>

コンポートネント毎に記述するのが面倒な方は、コンポーネント入りのjarも提供されているので、以下のように指定してください。

    <dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.0.10.Final</version>
        </dependency>
    </dependencies>

packeage

NettyはJBossプロジェクトから独立したため、Netty4ではパッケージ名が以下のように変わっています。まずは、パッケージ名を置き換えましょう。

Netty3 Netty4
org.jboss.netty io.netty

パッケージ名だけでなくクラス名も変わっているため、この段階では大量のビルドエラーが発生すると思います。このあとの手順で解決させていくので、今のところは気にせず進みましょう。
この段階で、ビルドが通っているのはentityくらいです(元々Nettyに依存していないので、当たり前ですが)。

Decoder

ここでは、Nettyに対する依存関係が少ない箇所からNetty4に移行します。そうすると、段々とビルドエラーが解決されていくので進み具合が分かりやすいです^^

それでは、まずはDecoderクラスをNetty4に移行しましょう。以下、移行後のコードとポイントを解説します。

  • MyProtocolDecoder
package netty_sample;

// (1)
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

public class MyProtocolDecoder extends ByteToMessageDecoder { // (2)
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf buffer,
            List<Object> out) throws Exception { // (3)
        // ID
        long id = buffer.readLong();
        
        // Name
        int length = buffer.readInt();
        byte[] byteName = new byte[length];
        buffer.readBytes(byteName);
        String name = new String(byteName, "UTF-8");
        
        // Age
        int age = buffer.readInt();
        
        UserInfo info = new UserInfo(id, name, age);
        out.add(info); // (4)
    }
}

(1)
まずはimportするクラスの変更です。パッケージを変更するだけでなく、以下のようにクラス名も変更になっています。

Netty3 Netty4
org.jboss.netty.buffer.ChannelBuffer io.netty.buffer.ByteBuf
org.jboss.netty.channel.ChannelHandlerContext io.netty.channel.ChannelHandlerContext
org.jboss.netty.handler.codec.oneone.OneToOneDecoder io.netty.handler.codec.ByteToMessageDecoder

OneToOneDecoderの置き換えはちょっと注意が必要です。
Netty4ではDecoderのベースとなるクラスがByteToMessageDecoderとMessageToMessageDecoderに分かれました。この2つのクラスは以下のように使い分けてください。

クラス名 利用方法
ByteToMessageDecoder bufferからentityへの変換に利用する
MessageToMessageDecoder entityからentityへの変換に利用する

(2)
MyProtocolDecoderはbufferからentityへの変換するクラスなので、ByteToMessageDecoderを継承するようにします。

(3)
オーバーライドするメソッド名はdecodeのままですが、引数・戻り値が変わっています。
Netty3では引数のObjectをbufferクラスにキャストして使用していました。Netty4ではByteToMessageDecoderを使うことになり、引数がByteBufクラスであることが分かっているので、キャストが不要になりました。

引数からchannelがなくなっていますが、ctx.channel()でchannelを取得できます。

戻り値がvoidになっている点と、引数のListについては、(4)で説明します。

(4)
bufferにいろいろとメソッドが追加されているものの、バイトデータを扱う際のメソッドは基本的に同じものが利用できます。Netty4で利用しているByteBufは性能向上の工夫をしているようで、Netty.docs: New and noteworthyには

According to our internal performance test, converting ByteBuf from an interface to an abstract class improved the overall throughput around 5%.

と書かれています。ByteBufはabstract classですが、実装されたメソッドはないため、機能的にはinterfaceでも良い内容になっています。
Netty3では変換後のentityクラスをreturnしていましたが、Netty4ではdecodeメソッドの引数に渡されたListにaddするようになりました。

引数の型がByteBufに決まっているため、Netty3のときよりスッキリしたコードになりましたね。

Encoder

次は、EncoderをNetty4に移行しましょう。こちらも随分スッキリしたコードになります。

  • MyProtocolEncoder
package netty_sample;

// (1)
package netty_sample;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class MyProtocolEncoder extends MessageToByteEncoder<UserInfo> { // (2)
    @Override
    protected void encode(ChannelHandlerContext ctx, UserInfo info,
            ByteBuf buffer) throws Exception { // (3)
        // IDの変換
        buffer.writeLong(info.getId());

        // Nameの変換
        byte[] byteName = info.getName().getBytes("UTF-8");
        buffer.writeInt(byteName.length);
        buffer.writeBytes(byteName);

        // Ageの変換
        buffer.writeInt(info.getAge());
    }
}

(1)
まずはimportするクラスの変更です。パッケージを変更するだけでなく、以下のようにクラス名も変更になっています。

Netty3 Netty4
org.jboss.netty.buffer.ChannelBuffer io.netty.buffer.ByteBuf
org.jboss.netty.channel.ChannelHandlerContext io.netty.channel.ChannelHandlerContext
org.jboss.netty.handler.codec.oneone.OneToOneEncoder io.netty.handler.codec.MessageToByteEncoder

Decoderと同様、OneToOneEncoderの置き換えはちょっと注意が必要です。
Netty4ではEncoderのベースとなるクラスがMessageToByteEncoderとMessageToMessageEncoderに分かれました。この2つのクラスは以下のように使い分けてください。

クラス名 利用方法
MessageToByteEncoder entityからbufferへの変換に利用する
MessageToMessageEncoder entityからentityへの変換に利用する

(2)
MyProtocolEncoderはentityからbufferへの変換するクラスなので、MessageToByteEncoderを継承するようにします。ジェネリクスを利用してentityクラスを指定します。

(3)
オーバーライドするメソッド名はencodeのままですが、引数・戻り値が変わっています。
Netty3では引数のObjectをentityクラスにキャストして使用していました。Netty4ではMessageToByteEncoderを使うことになり、引数の型が分かっているので、キャストが不要になりました。

引数からchannelがなくなっていますが、ctx.channel()でchannelを取得できます。

戻り値がvoidになっている点については、(4)で説明します。

(4)
Decoderのところでも言及したように、bufferにいろいろとメソッドが追加されているものの、バイトデータを扱う際のメソッドは基本的に同じものが利用できます。bufferはencodeメソッドの引数に指定されているByteBufを利用してください。
Netty3では変換後のbufferをreturnしていましたが、Netty4では不要になりました。encodeメソッドの引数に渡されたbufferに書き込んでおけば良いです。そのため、encodeメソッドの戻り値がvoidになっています。

こちらも、引数の型がByteBufに決まっているため、Netty3のときよりスッキリしたコードになりましたね。
ここまでの段階で、Encoder/Decoderのビルドが通るようになったはずです。

Handler

次はHandlerを移行します。サーバ側から進めていますが、クライアント側から進めても良いです。

  • MyProtocolServerHandler
package netty_sample;

// (1)
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * サーバ側アプリケーションロジック
 */
public class MyProtocolServerHandler extends SimpleChannelInboundHandler<UserInfo> { // (2)
    /**
     * クライアントからメッセージを受信した際に呼び出されるメソッド
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, UserInfo msg)
            throws Exception { // (3)
        System.out.println(msg); // 受信メッセージを表示
    }
}

(1)
まずはimportするクラスの変更です。パッケージを変更しましょう。また、Netty.docs: New and noteworthyの「Revamped ChannelHandler interface」によると、Upstream/Downstreamという用語は誤解を招きやすいとのことで、Inbound/Outboundという用語に変わっています。

Netty3 Netty4
Upstream Inbound
Downstream Outbound

(2)
Netty3ではSimpleChannelHandlerを継承しましたが、イベント受信時の処理だけであれば、Netty4ではSimpleChannelInboundHandlerを継承することにより型安全に記述することができます。

(3)
messageReceivedメソッドがchannelRead0メソッドになっています。メソッド名の末尾に「0」を付けるのを忘れずに! 引数からイベントクラスがなくなった一方、メッセージの型が決まっているので、キャストが不要になりました。Netty.docs: New and noteworthyの「ChannelHandler with no event object」によると、Netty3のイベントクラスはGCによる負荷の要因となっていたようです。
また、Netty4ではメッセージ受信時にリソースの解放処理が必要ですが、SimpleChannelInboundHandlerを継承すれば、Netty側で自動的に解放してくれます。


さて、次はクライアント側です。

  • MyProtocolClientHandler
package netty_sample;

// (1)
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * クライアント側アプリケーションロジック
 */
public class MyProtocolClientHandler extends ChannelInboundHandlerAdapter { // (2)
    /**
     * サーバに接続した際に呼び出されるメソッド
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception { // (3)
        // サーバに送信する情報を生成
        UserInfo info = new UserInfo(100, "taro", 20);
        // サーバに送信
        ctx.writeAndFlush(info); // (4)
    }
}

(1)
まずはimportするクラスの変更です。パッケージを変更しましょう。Upstream/Downstreamという用語は誤解を招きやすいとのことで、Inbound/Outboundという用語に変わっています。

(2)
継承するクラス名は以下のように変更になっています。

Netty3 Netty4
org.jboss.netty.channel.SimpleChannelHandler io.netty.channel.ChannelInboundHandlerAdapter

(3)
channelConnectedメソッドがchannelActiveメソッドになっています。引数からイベントクラスもなくなりました。
Netty.docs: New and noteworthyの「Simplified channel state model」には、Channel状態の考え方の変化が記載されています。以下の図はそのページからの引用ですが、Netty3ではこうだったものが、
f:id:acro-engineer:20131017065734p:plain
Netty4ではこのようにシンプルに変わりました。
f:id:acro-engineer:20131017065750p:plain

(4)
状態がシンプルになった分、APIの使い方も変化しています。Netty3ではChannelHandlerContextから取得したChannelに対してwriteメソッドを呼び出すことで送信することができました。
Netty4ではChannelHandlerContextに対してメソッドを呼び出すようになりました。また、Netty3ではwriteメソッドで送信できましたが、Netty4ではflushしないと送信されないので注意が必要です。ここでは、writeAndFlushメソッドを使って、送信まで行っています。

さて、これでHanderクラスまでビルドが通るようになりました。

Bootstrap

残りはBootstrapクラス周りですね。サーバ側から進めていますが、クライアント側から進めても良いです。

  • MyProtocolServer
package netty_sample;

// (1)
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;

/**
 * サーバ側メインクラス
 */
public class MyProtocolServer {

    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (2)
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap(); // (3)
            bootstrap
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .localAddress(9999)
                .option(ChannelOption.SO_BACKLOG, 100)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        // Downstream(送信)
                        pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                        pipeline.addLast("myEncoder", new MyProtocolEncoder());
                        // Upstream(受信)
                        pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
                        pipeline.addLast("myDecoder", new MyProtocolDecoder());
                        // Application Logic Handler
                        pipeline.addLast("handler", new MyProtocolServerHandler()); // server
                    }
                 });

            // Start the server.
            ChannelFuture future = bootstrap.bind().sync();

            // Wait until the server socket is closed.
            future.channel().closeFuture().sync();
        } finally {
            // (4)
            // 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();
        }
    }
}

(1)
まずはimportするクラスの変更です。パッケージを変更しましょう。また、EventLoopGroupクラス等、importするクラスが増えています。

(2)
Netty3から4になるにあたって、EventLoop周りが以下のように変更になっています。

Netty3 Netty4
Java標準APIのExecutorsを利用 Nettyが用意したEventLoopGroupを利用

(3)
Netty.docs: New and noteworthyの「New bootstrap API」に書いてあるように、bootstrap周りは大幅にAPIが変わりました。Netty4では、fluent interfaceを利用するようになっています。また、option(ChannelOption.SO_BACKLOG、ChannelOption.TCP_NODELAYを指定している箇所)が型安全になっています。型安全になった一方で、「設定ファイルに文字列を書いておいて、それをセットすれば良い」という訳にはいかなくなり、自分で適切な型にキャストすることが必要になっています。
さらに、Handlerの初期化周りも、利用するクラス名が以下のように変わっています。

Netty3 Netty4
org.jboss.netty.channel.ChannelPipelineFactory io.netty.channel.ChannelInitializer

(4)
Netty3では、bootstrapに対してreleaseExternalResourcesメソッドを呼ぶことで終了させることができましたが、Netty4ではEventLoopGroupに対してメソッドを呼ぶことで終了させます

  • MyProtocolClient
package netty_sample;

// (1)
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;

/**
 * クライアント側メインクラス
 */
public class MyProtocolClient {

    public static void main(String[] args) throws Exception {
        // Configure the client.
        EventLoopGroup group = new NioEventLoopGroup(); // (2)

        try {
            Bootstrap bootstrap = new Bootstrap(); // (3)
            bootstrap
                .group(group)
                .channel(NioSocketChannel.class)
                .remoteAddress("localhost", 9999)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        // Downstream(送信)
                        pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                        pipeline.addLast("myEncoder", new MyProtocolEncoder());
                        // Upstream(受信)
                        pipeline.addLast("frameDecoder",
                                new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
                        pipeline.addLast("myDecoder", new MyProtocolDecoder());
                        // Application Logic Handler
                        pipeline.addLast("handler", new MyProtocolClientHandler()); // client
                    }
                 });

            // Start the client.
            ChannelFuture future = bootstrap.connect().sync(); // 9999番ポートにconnect

            // Wait until the connection is closed.
            future.channel().closeFuture().sync();
        } finally {
            // (4)
            // Shut down the event loop to terminate all threads.
            group.shutdownGracefully();
        }
    }
}

(1)-(4)
このあたりの変更はサーバ側と同じように、クライアント側も大幅に変わっています。変更内容はサーバ側とほぼ同じであるため、詳細はMyProtocolServerの説明を参照ください。

最後に

さて、これですべてのクラスの変更が終わりビルドが通るようになり、Netty4に移行することができました。Encoder/Decoder周りは置き換えやすいと思いますが、EventLoop周りの変更はかなり大きいですね。プロダクトによっては、移行が大変な場合もあるかもしれません。
Nettyのページに書いてあるように、Nettyは様々なプロダクトで利用されています。もちろん、公開していないプロジェクトでも多く使われているでしょう。あちこちでNetty3をNetty4に移行する話が出ていると思いますが、その際に参考になればと思います。
 
 

Acroquest Technologyでは、キャリア採用を行っています。


  • 日頃勉強している成果を、Hadoop、Storm、NoSQL、HTML5/CSS3/JavaScriptといった最新の技術を使ったプロジェクトで発揮したい。
  • 社会貢献性の高いプロジェクトに提案からリリースまで携わりたい。
  • 書籍・雑誌等の執筆や対外的な勉強会の開催を通した技術の発信や、社内勉強会での技術情報共有により、技術的に成長したい。
  • OSSの開発に携わりたい。

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
 キャリア採用ページ