Nettyの紹介

Nettyの概要

1. 前書き

この記事では、非同期のイベント駆動型ネットワークアプリケーションフレームワークであるNettyについて見ていきます。

Nettyの主な目的は、ネットワークコンポーネントとビジネスロジックコンポーネントの分離と疎結合を備えた、NIO(または場合によってはNIO.2)に基づいた高性能プロトコルサーバーを構築することです。 HTTPなどの広く知られているプロトコル、または独自の特定のプロトコルを実装する場合があります。

2. コアコンセプト

Nettyはノンブロッキングフレームワークです。 これにより、ブロッキングIOと比較して高いスループットが得られます。 Understanding non-blocking IO is crucial to understanding Netty’s core components and their relationships.

2.1. チャネル

ChannelはJavaNIOのベースです。 これは、読み取りや書き込みなどのIO操作が可能なオープン接続を表します。

2.2. 未来

NettyのChannelでのすべてのIO操作は非ブロッキングです。

これは、呼び出しの直後にすべての操作が返されることを意味します。 標準のJavaライブラリにはFutureインターフェースがありますが、Nettyの目的には便利ではありません。操作の完了についてFutureに問い合わせるか、操作が完了するまで現在のスレッドをブロックすることしかできません。 。

そのため、Netty has its own ChannelFuture interface.操作の完了時に呼び出されるChannelFutureにコールバックを渡すことができます。

2.3. イベントとハンドラー

Nettyはイベント駆動型のアプリケーションパラダイムを使用するため、データ処理のパイプラインはハンドラーを通過する一連のイベントです。 イベントとハンドラーは、インバウンドおよびアウトバウンドのデータフローに関連付けることができます。 受信イベントは次のとおりです。

  • チャネルのアクティブ化と非アクティブ化

  • 操作イベントの読み取り

  • 例外イベント

  • ユーザーイベント

アウトバウンドイベントはよりシンプルで、一般的に、接続のオープン/クローズとデータの書き込み/フラッシュに関連しています。

Nettyアプリケーションは、ネットワークおよびアプリケーションロジックイベントとそのハンドラーで構成されます。 チャネルイベントハンドラの基本インターフェイスは、ChannelHandlerとその祖先ChannelOutboundHandlerおよびChannelInboundHandlerです。

Nettyは、ChannelHandler.の実装の巨大な階層を提供します。空の実装であるアダプターに注目する価値があります。 ChannelInboundHandlerAdapterおよびChannelOutboundHandlerAdapter。 すべてのイベントのサブセットのみを処理する必要がある場合、これらのアダプターを拡張できます。

また、HTTPなどの特定のプロトコルには多くの実装があります。 HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator.NettyのJavadocでそれらに精通するのは良いことです。

2.4. エンコーダーとデコーダー

ネットワークプロトコルを使用する場合、データのシリアル化と逆シリアル化を実行する必要があります。 この目的のために、Nettyは、着信データをデコードできるdecodersChannelInboundHandlerの特別な拡張機能を導入しています。 ほとんどのデコーダーの基本クラスはByteToMessageDecoder.です。

発信データをエンコードするために、Nettyにはencoders.と呼ばれるChannelOutboundHandlerの拡張機能があります。MessageToByteEncoderはほとんどのエンコーダー実装のベースです.メッセージをバイトシーケンスからJavaオブジェクトに変換できます。エンコーダーとデコーダーではその逆です。

3. サーバーアプリケーションの例

リクエストを受信し、計算を実行し、レスポンスを送信する単純なプロトコルサーバーを表すプロジェクトを作成しましょう。

3.1. 依存関係

まず、pom.xmlにNetty依存関係を提供する必要があります。


    io.netty
    netty-all
    4.1.10.Final

on Maven Centralで最新バージョンを見つけることができます。

3.2. データ・モデル

要求データクラスの構造は次のとおりです。

public class RequestData {
    private int intValue;
    private String stringValue;

    // standard getters and setters
}

サーバーがリクエストを受信し、intValueに2を掛けた値を返すと仮定します。 応答には単一のint値が含まれます。

public class ResponseData {
    private int intValue;

    // standard getters and setters
}

3.3. リクエストデコーダー

ここで、プロトコルメッセージのエンコーダーとデコーダーを作成する必要があります。

Netty works with socket receive bufferはキューとしてではなく、バイトの束として表されることに注意してください。 これは、サーバーがメッセージ全体を受信して​​いないときにインバウンドハンドラーを呼び出すことができることを意味します。

We must make sure that we have received the full message before processingとそれを行うには多くの方法があります。

まず、一時的なByteBufを作成し、必要なバイト数が得られるまですべてのインバウンドバイトを追加します。

public class SimpleProcessingHandler
  extends ChannelInboundHandlerAdapter {
    private ByteBuf tmp;

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        System.out.println("Handler added");
        tmp = ctx.alloc().buffer(4);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        System.out.println("Handler removed");
        tmp.release();
        tmp = null;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        tmp.writeBytes(m);
        m.release();
        if (tmp.readableBytes() >= 4) {
            // request processing
            RequestData requestData = new RequestData();
            requestData.setIntValue(tmp.readInt());
            ResponseData responseData = new ResponseData();
            responseData.setIntValue(requestData.getIntValue() * 2);
            ChannelFuture future = ctx.writeAndFlush(responseData);
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

上記の例は少し奇妙に見えますが、Nettyの仕組みを理解するのに役立ちます。 ハンドラーのすべてのメソッドは、対応するイベントが発生すると呼び出されます。 そのため、ハンドラーが追加されたときにバッファーを初期化し、新しいバイトの受信時にデータを入力し、十分なデータを取得したら処理を開始します。

意図的にstringValueを使用しませんでした—このような方法でのデコードは不必要に複雑になります。 NettyがChannelInboundHandlerの実装である便利なデコーダークラスを提供するのはそのためです:ByteToMessageDecoderReplayingDecoder.

前述のように、Nettyを使用してチャネル処理パイプラインを作成できます。 したがって、最初のハンドラーとしてデコーダーを配置し、処理ロジックハン​​ドラーをその後に配置できます。

RequestDataのデコーダーを次に示します。

public class RequestDecoder extends ReplayingDecoder {

    private final Charset charset = Charset.forName("UTF-8");

    @Override
    protected void decode(ChannelHandlerContext ctx,
      ByteBuf in, List out) throws Exception {

        RequestData data = new RequestData();
        data.setIntValue(in.readInt());
        int strLen = in.readInt();
        data.setStringValue(
          in.readCharSequence(strLen, charset).toString());
        out.add(data);
    }
}


このデコーダのアイデアは非常に単純です。 読み取り操作に十分なデータがバッファーにない場合に例外をスローするByteBufの実装を使用します。

例外がキャッチされると、バッファは先頭に巻き戻され、デコーダはデータの新しい部分を待ちます。 decodeの実行後、outリストが空でない場合、デコードは停止します。

3.4. 応答エンコーダ

RequestDataをデコードすることに加えて、メッセージをエンコードする必要があります。 書き込み操作が発生したとき、我々は完全なメッセージデータを持っているため、この操作は簡単です。

メインハンドラーでChannelにデータを書き込むか、ロジックを分離して、ResponseDataの書き込み操作をキャッチするMessageToByteEncoderを拡張するハンドラーを作成できます。

public class ResponseDataEncoder
  extends MessageToByteEncoder {

    @Override
    protected void encode(ChannelHandlerContext ctx,
      ResponseData msg, ByteBuf out) throws Exception {
        out.writeInt(msg.getIntValue());
    }
}

3.5. リクエスト処理

別々のハンドラーでデコードとエンコードを実行したので、ProcessingHandlerを変更する必要があります。

public class ProcessingHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
      throws Exception {

        RequestData requestData = (RequestData) msg;
        ResponseData responseData = new ResponseData();
        responseData.setIntValue(requestData.getIntValue() * 2);
        ChannelFuture future = ctx.writeAndFlush(responseData);
        future.addListener(ChannelFutureListener.CLOSE);
        System.out.println(requestData);
    }
}

3.6. サーバーブートストラップ

それでは、すべてをまとめてサーバーを実行しましょう。

public class NettyServer {

    private int port;

    // constructor

    public static void main(String[] args) throws Exception {

        int port = args.length > 0
          ? Integer.parseInt(args[0]);
          : 8080;

        new NettyServer(port).run();
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
              .channel(NioServerSocketChannel.class)
              .childHandler(new ChannelInitializer() {
                @Override
                public void initChannel(SocketChannel ch)
                  throws Exception {
                    ch.pipeline().addLast(new RequestDecoder(),
                      new ResponseDataEncoder(),
                      new ProcessingHandler());
                }
            }).option(ChannelOption.SO_BACKLOG, 128)
              .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

上記のサーバーブートストラップの例で使用されているクラスの詳細は、Javadocに記載されています。 最も興味深い部分は次の行です。

ch.pipeline().addLast(
  new RequestDecoder(),
  new ResponseDataEncoder(),
  new ProcessingHandler());

ここでは、リクエストと出力を正しい順序で処理するインバウンドおよびアウトバウンドハンドラーを定義します。

4. クライアントアプリケーション

クライアントは逆エンコードとデコードを実行する必要があるため、RequestDataEncoderResponseDataDecoderが必要です。

public class RequestDataEncoder
  extends MessageToByteEncoder {

    private final Charset charset = Charset.forName("UTF-8");

    @Override
    protected void encode(ChannelHandlerContext ctx,
      RequestData msg, ByteBuf out) throws Exception {

        out.writeInt(msg.getIntValue());
        out.writeInt(msg.getStringValue().length());
        out.writeCharSequence(msg.getStringValue(), charset);
    }
}
public class ResponseDataDecoder
  extends ReplayingDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx,
      ByteBuf in, List out) throws Exception {

        ResponseData data = new ResponseData();
        data.setIntValue(in.readInt());
        out.add(data);
    }
}


また、要求を送信し、サーバーから応答を受信するClientHandlerを定義する必要があります。

public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx)
      throws Exception {

        RequestData msg = new RequestData();
        msg.setIntValue(123);
        msg.setStringValue(
          "all work and no play makes jack a dull boy");
        ChannelFuture future = ctx.writeAndFlush(msg);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
      throws Exception {
        System.out.println((ResponseData)msg);
        ctx.close();
    }
}

それでは、クライアントをブートストラップしましょう。

public class NettyClient {
    public static void main(String[] args) throws Exception {

        String host = "localhost";
        int port = 8080;
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer() {

                @Override
                public void initChannel(SocketChannel ch)
                  throws Exception {
                    ch.pipeline().addLast(new RequestDataEncoder(),
                      new ResponseDataDecoder(), new ClientHandler());
                }
            });

            ChannelFuture f = b.connect(host, port).sync();

            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

ご覧のとおり、サーバーのブートストラップには多くの共通点があります。

これで、クライアントのmainメソッドを実行して、コンソールの出力を確認できます。 予想どおり、intValueが246に等しいResponseDataを取得しました。

5. 結論

この記事では、Nettyを簡単に紹介しました。 ChannelChannelHandlerなどのコアコンポーネントを示しました。 また、シンプルなノンブロッキングプロトコルサーバーとそのクライアントを作成しました。

いつものように、すべてのコードサンプルはover on GitHubで利用できます。