takeda_san’s blog

KotlinとVRを頑張っていく方向。

JavaFXとNettyでソケット通信アプリケーションを作ってみよう その2

前回のあらすじ

takeda-san.hatenablog.com

Nettyを使った簡単なソケット通信プログラムを動かしたぞ!

今回やること

好きな文字列が送れるようにする。
前回は決まったフォーマットの固定長のデータだったので、データのエンコード、デコード部分を直接コードに書いていた。

前回記事、ClientHandlerクラスのこの辺ですね。

long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));

しかしながら、可変長のデータもあるじゃろ。
ということで、前回のサンプルプログラムを少しいじって、好きな文字列が送れるようにする。

また、今回もJavaFXは出てきません。

プログラム

今回も懲りずに全文掲載。

まずは、クライアント側から

package application.socket.part2;

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.LengthFieldPrepender;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public class Client {

    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        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<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();

                    // outbound(サーバへの送信時に実行される)
                    pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                    pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8));

                    // ハンドラ(送受信両方で実行される)
                    pipeline.addLast(new ClientHandler());
                }
            });

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

            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}
package application.socket.part2;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) {
        String message = "メッセージを送ったぞ!";

        final ChannelFuture f = ctx.writeAndFlush(message);
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

続いてサーバ側

package application.socket.part2;

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.string.StringDecoder;
import io.netty.util.CharsetUtil;

public class Server {

    private int port;

    public Server(int port) {
        this.port = port;
    }

    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<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            // inbound(サーバへの送信時に実行される)
                            pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
                            pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));

                            // ハンドラ(送受信両方で実行される)
                            ch.pipeline().addLast(new ServerHandler());
                        }
                    }).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();
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new Server(port).run();
    }
}
package application.socket.part2;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String m = (String) msg;
        System.out.println(m);
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

んで、サーバとクライアントを起動して、サーバ側のコンソールに出力されるのがこれ。

メッセージを送ったぞ!

今回、プログラムを変更したところは、ここ。

Client.java

// outbound(サーバへの送信時に実行される)
pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8));

// ハンドラ(送受信両方で実行される)
pipeline.addLast(new ClientHandler());

ClientHandler.java

@Override
public void channelActive(final ChannelHandlerContext ctx) {
    String message = "メッセージを送ったぞ!";
    final ChannelFuture f = ctx.writeAndFlush(message);
    f.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) {
            assert f == future;
            ctx.close();
        }
    });
}

Server.java

// inbound(サーバへの送信時に実行される)
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));

// ハンドラ(送受信両方で実行される)
ch.pipeline().addLast(new ServerHandler());

ServerHandler.java

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    String m = (String) msg;
    System.out.println(m);
    ctx.close();
}

で、どの順番で動いてるのよ

  1. サーバが起動する
  2. クライアントが起動する
  3. クライアントがサーバに接続する
  4. クライアントの接続時にClientHandlerクラスのchannelActiveメソッドが実行される
  5. StringのデータをUTF-8エンコード(バイト列にします)
  6. 文字列のバイト長(4バイト)を先頭にくっつける
  7. サーバへ送信する
  8. サーバのデータ受信時にバイト長を元にデータ部(Stringのデータ)を取り出す
  9. データ部をUTF-8でデコードしてStringデータに戻す
  10. コンソールに表示する

パイプライン

パイプラインとは、受け取ったメッセージに対して処理をするキューのようなもの?
データを送受信するときにパイプラインに登録した、ハンドラが順番に実行されます。

クライアントだと

  1. ClientHandler
  2. StringEncoder
  3. LengthFieldPrepender
// outbound(サーバへの送信時に実行される)
pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8));

// ハンドラ(送受信両方で実行される)
pipeline.addLast(new ClientHandler());

サーバだと

  1. LengthFieldBasedFrameDecoder
  2. StringDecoder
  3. ServerHandler
// inbound(サーバへの送信時に実行される)
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));

// ハンドラ(送受信両方で実行される)
ch.pipeline().addLast(new ServerHandler());

の順番で実行されているように見える。
outbound時には、パイプライン登録した後ろのほうから実行される。
inbound時には、パイプライン登録した前のほうから実行される。
で、良いのかな?

LengthFieldPrependerの引数に指定した数値が、データ長を示すヘッダ長になる。
StringDecoderとStringEncoderに指定した引数の文字コードでデコード、エンコードをかける。

次回はようやく、JavaFXに組み込んで、任意の文字列を送受信できるようにします。