snuffkinの遊び場

IT関係、スポーツ、数学等に関することを、気が向いたときに書いてます。

イベントドリブンなネットワークプログラミングができるフレームワークNetty

このところ、JBoss関係のプロダクトに触ることが多いのですが、その中で特に気に入ったプロダクトがNettyです(NettyはJBossから独立していますが)。優れたプロダクトだと思うのですが、それほどドキュメントもないため、Nettyについて使ったり調べたことをまとめて行こうと思います。

Nettyとは?

Nettyは、イベントドリブンな非同期通信を行うアプリケーションを開発するためのフレームワークです。ソケット周りを直接触る処理はNettyが行ってくれて、イベントドリブンで接続・切断時の処理を記述したり、電文受信時の処理を記述することができ、簡潔で見通しの良いネットワークプログラミングができます。また、Nettyには電文処理を行うアプリケーションを開発するときに必要なライブラリが用意されているため、オリジナルプロトコルを使う場合にもNettyを使えば開発しやすいです。
Netty: Home」がプロジェクトのサイトです。解説ドキュメントはあまり豊富ではありませんが、Javadocは充実しています。今のところ、Nettyを知る一番のドキュメントはJavadocではないかと思います。また、stackoverflow.comのNettyページでは、活発にQ&Aがやりとりされているため、こちらも参考になると思います。
ライセンスはApache License 2.0です。また、NettyはJavaで書かれています。

私がNettyを使いたくなった動機

RPCやらHTTPやら、異なる言語のプロセスと簡単に通信できる仕組みがいろいろと考えられてきたため、自力でネットワークプログラミングする機会は随分減りました。ですが、既存システムに合わせることが必須のプロジェクトもあり、TCP上のオリジナルプロトコルも健在です。「MessagePack-RPCとか使おうよ!」と言いたいところですが、どうにもなりません。

ネットワークプログラミングは、結構繊細なお決まりの処理をしなきゃいけないため、正直面倒です。JDBC周りでお決まりのclose処理を書くより遥かに面倒です。私はアプリケーションロジックを開発したいのに!
これが一体どれだけ面倒なのか、ちょっと見てみましょう。

まずは、NIO以前からある、昔ながらのSocketクラスを使ったプログラミング。エラー処理とか考えると、「Javaネットワークプログラミング講座」にあるサンプルのような処理が必要です(コードへのリンクがあるので、SocketConnector.java、MessageReciever.java、MessageSender.javaあたりを参照してください)。考えないといけないことが多く、結構大変です。

NIOの導入で便利になったものの、サーバ側だけでも「Java In The Box」にあるサンプルのような処理を書く必要があります。アプリケーションロジックを開発したいのに、依然として、このあたりの処理は面倒です。

とまあ、自分で書くのが面倒なので、他のサイトを参照させて頂きました。さて、ここまで読んだ人で、これらのサイトのコードをしっかり追った方はどのくらいいるでしょうか。正直、呪文のようなコードに挫折した人も多いのでは。サイトのコードが悪いのではなく、面倒な処理が必要なAPIになっているのです。
こういう実装は嫌だったので、いろいろ探してたどり着いたのがNettyでした。Nettyを使うと、ネットワークプログラミングをもっと見通し良く記述することができます。

Nettyを使った具体例

それでは、サンプルとしてクライアントから"Hello, World!"という文字列を受信してそのまま返すエコーサーバを紹介します。
「データの先頭に電文長があり、その後に実際の電文内容が続く」という電文プロトコルに、文字列を詰めて送信することにします。最初の4byteに電文長を格納し、残りは電文内容(文字列をbyteにしたもの)だとしましょう。

まずはサーバ側です。EchoServerがメインクラスです。具体的に受信した電文を処理しているアプリケーションロジックがEchoServerHandlerです。細かな説明は今回は省略し、別の機会に書きます。

  • EchoServer
package jp.gr.java_conf.snuffkin.sandbox.netty.echo;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.frame.LengthFieldBasedFrameDecoder;
import org.jboss.netty.handler.codec.frame.LengthFieldPrepender;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;

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

    public static void main(String[] args) {
        ChannelFactory factory = 
            new NioServerSocketChannelFactory( // server
                    Executors.newCachedThreadPool(),
                    Executors.newCachedThreadPool()
                    );
        
        ServerBootstrap bootstrap = new ServerBootstrap(factory);
        bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
            public ChannelPipeline getPipeline() {
                ChannelPipeline pipeline = Channels.pipeline();
                // Downstream(送信)
                pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                pipeline.addLast("stringEncoder", new StringEncoder());
                // Upstream(受信)
                pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
                pipeline.addLast("stringDecoder", new StringDecoder());
                // Application Logic Handler
                pipeline.addLast("handler", new EchoServerHandler()); // server
                
                return pipeline;
            }
        });
        
        bootstrap.bind(new InetSocketAddress(9999)); // 9999番ポートでlisten
    }
}
  • EchoServerHandler
package jp.gr.java_conf.snuffkin.sandbox.netty.echo;

import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

/**
 * サーバ側アプリケーションロジック
 */
public class EchoServerHandler extends SimpleChannelHandler {
    /**
     * クライアントから電文を受信した際に呼び出されるメソッド
     */
    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) {
        String msg = (String) event.getMessage(); // 受信電文を取りだす
        ctx.getChannel().write(msg); // クライアントに送信
    }
}

次にクライアント側です。EchoClientがメインクラスです。アプリケーションロジックを実装しているのが、EchoClientHandlerです。こちらも、細かな説明は今回は省略し、別の機会に書きます。

  • EchoClient
package jp.gr.java_conf.snuffkin.sandbox.netty.echo;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.frame.LengthFieldBasedFrameDecoder;
import org.jboss.netty.handler.codec.frame.LengthFieldPrepender;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;

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

    public static void main(String[] args) {
        ChannelFactory factory =
            new NioClientSocketChannelFactory( // client
                    Executors.newCachedThreadPool(),
                    Executors.newCachedThreadPool()
                    );
        
        ClientBootstrap bootstrap = new ClientBootstrap(factory);
        bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
            public ChannelPipeline getPipeline() {
                ChannelPipeline pipeline = Channels.pipeline();
                // Downstream(送信)
                pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                pipeline.addLast("stringEncoder", new StringEncoder());
                // Upstream(受信)
                pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
                pipeline.addLast("stringDecoder", new StringDecoder());
                // Application Logic Handler
                pipeline.addLast("handler", new EchoClientHandler()); // client

                return pipeline;
            }
        });
        
        ChannelFuture future = bootstrap.connect(new InetSocketAddress("localhost", 9999)); // 9999番ポートにconnect
        future.getChannel().getCloseFuture().awaitUninterruptibly();
        bootstrap.releaseExternalResources();
    }
}
  • EchoClientHandler
package jp.gr.java_conf.snuffkin.sandbox.netty.echo;

import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

/**
 * クライアント側アプリケーションロジック
 */
public class EchoClientHandler extends SimpleChannelHandler {
    /**
     * サーバに接続した際に呼び出されるメソッド
     */
    @Override
    public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent event) {
        ctx.getChannel().write("Hello, World!");
    }
    
    /**
     * サーバから電文を受信した際に呼び出されるメソッド
     */
    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) {
        String msg = (String) event.getMessage();
        System.out.println(msg);
    }
}

動作の流れを解説すると、以下の通りです。

  1. EchoServerを起動してください。9999番ポートでクライアントからの接続を待ちます。
  2. サーバが起動したら、EchoClientを起動してください。EchoClientは9999番ポートに接続します。
  3. クライアントがサーバに接続すると、EchoClientHandler#channelConnectedがNettyから呼び出されます。この中で、サーバに対して文字列"Hello, World!"を送信します。
  4. サーバがクライアントからの電文を受信すると、EchoServerHandler#messageReceivedがNettyから呼び出されます。クライアントから受信した文字列を、クライアントに送信します。
  5. クライアントがサーバからの電文を受信すると、EchoClientHandler#messageReceivedがNettyから呼び出されます。サーバから受信した文字列を標準出力します。
Hello, World!

import文を除いたとしても、よくあるRPCのサンプルと比べるとコード量は多いです。ですが、この例はオリジナルプロトコルを実装したものであり、RPCライブラリに乗っかって通信したものより、多くの処理を自分で作ったことになります。なので、単純には比較できないですね。

アプリケーションロジックを書いているEchoServerHandlerやEchoClientHandlerを見てください。このくらい簡単にオリジナルプロトコルで通信することができます。このサンプルではエラー処理を省略したところがありますが、ネットワークプログラミングをイベントドリブンで記述できるようになると、こんなにも見通しが良くなります。Nettyを使わないサンプルと違って、コードを読む気になりますよね?

この例を振り返ってみると、byteの変数がまったく登場しません。このくらい簡単な例なら、byteの変数すら使わずに書けるのが、ネットワークアプリケーションを開発するためのフレームワークであるNettyの良さだと思います。

さて、Nettyの印象はどうだったでしょうか。Nettyの気持ち良さの一端が伝われば幸いです。今回書いたコードの具体的な解説は、また今度書きたいと思います。