前回のあらすじ
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(); }
で、どの順番で動いてるのよ
- サーバが起動する
- クライアントが起動する
- クライアントがサーバに接続する
- クライアントの接続時にClientHandlerクラスのchannelActiveメソッドが実行される
- StringのデータをUTF-8でエンコード(バイト列にします)
- 文字列のバイト長(4バイト)を先頭にくっつける
- サーバへ送信する
- サーバのデータ受信時にバイト長を元にデータ部(Stringのデータ)を取り出す
- データ部をUTF-8でデコードしてStringデータに戻す
- コンソールに表示する
パイプライン
パイプラインとは、受け取ったメッセージに対して処理をするキューのようなもの?
データを送受信するときにパイプラインに登録した、ハンドラが順番に実行されます。
クライアントだと
- ClientHandler
- StringEncoder
- LengthFieldPrepender
// outbound(サーバへの送信時に実行される) pipeline.addLast("frameEncoder", new LengthFieldPrepender(4)); pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8)); // ハンドラ(送受信両方で実行される) pipeline.addLast(new ClientHandler());
サーバだと
- LengthFieldBasedFrameDecoder
- StringDecoder
- 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に組み込んで、任意の文字列を送受信できるようにします。