takeda_san’s blog

JavaFXと機械学習を頑張る方向。

QwiklabsでAWSをやっていく

思えばいままで、サーバーサイドをやってこなかったな。
というわけでAWSやろう。

なぜQwiklabs

AWSとかGCPとか、やったことある友達がいないんですよね…
いや、普通の友もいないですけど。
だから、独学で!というわけになるんですけども、従量課金制だからチョット怖いんです。

そこでQwiklabs。
AWSの講座を受けるための費用にAWSの利用料(でいいのかな)が入っているので、
私のような特に目的もないけどいじってみたい。という人にピッタリ。

https://qwiklabs.com/

アカウント作成からクレジット購入

アカウントとサクッと作って、講座を受けるために必要なクレジットを購入します。
クレカですぐに買えました。
(途中、微妙に表示がおかしかったり、部分的に中国語で表示されたりしましたが)

講座受け放題みたいなのもあるみたいですが、ひとまずお試しの8クレジット分。
ちなみに、一度にクレジットをたくさん買うとちょっとお安くなるみたいです。
このシステム、ゲームの石を買うやつをどうしても連想してしまう。

f:id:takeda_san:20171016210925p:plain

講座をうけてみる

さっそく講座をうけてみる。
いくつかの講座を順番に受けるクエストというものもあるらしいが、とりあえず単品で
『Introduction to Amazon Elastic Compute Cloud (EC2)』
というのをやってみる。
この講座は日本語版もあるみたいで、右の地球マークから言語を選べる。
f:id:takeda_san:20171016211624p:plain

開始してみると、なにやらカウントダウンが開始される。
どうやら制限時間があるらしい。
f:id:takeda_san:20171016213932p:plain

はめられたッ。なんだか映画のSAWをおもいだす演出である。
しかしながら手順を淡々と進めていくと、16分ほど余った。
なんだ、焦る必要はなかった。

というわけでものの、15分でサーバが起動してしまった。
これが、文明… f:id:takeda_san:20171016215654p:plain

残りの時間は講座を終了しない限り、わりと自由に別のインスタンスを作成したり、立ち上げたサーバをいじれたりしました。
これで1クレジット(1ドルぐらい)。
個人的にはお安いと思う。

まとめ

  • 良かった点
    登録が簡単!
    講座の画面からコンソールにボタン一つでログインできて準備いらず!
    1講座20分ぐらいで気軽にできる!
    日本語訳が完璧!
    (AWSの初心者向け講座はだいたい翻訳されているみたいですが、GCPとか応用編みたいなのは、英語のみみたいです)

  • うーんな点
    用語の解説がほぼなく、次々と新たな用語が出てくる
    (講座のテキストを下読みしたほうが、もっと効果的に勉強できたかも…)
    解説に画像が全くないので、どこを操作すればよいのか戸惑うところがあった
    (レイアウトが良く変りそうだからしょうがないとは思うが…)
    制限時間があるので、そちらに気がとられがち

まだ、クレジットあるのでGCPもやってみようかなと思います。

EclipseでJigsawしてみる 完結編

ぽやぽやしていたらJava9正式リリースされてました。
そういえば、EclipseのJava9対応もそろそろでしたっけ。

Java9対応のOxygen a1が10/11リリースとの情報を手に入れた我々は、RC版を手に入れるべくEclipseホームページへと急いだ。
というわけで、このシリーズも完結編です。

takeda-san.hatenablog.com

takeda-san.hatenablog.com

設定編

というわけで、OxygenのRC版をダウンロードとインストール。
(10/11以降は普通にリリース版をダウンロードしてね)

あと、肝心なjdkをダウンロードして、インストールします。
Java9 GAってかいてあるけど何だろうね。GAって。
General Availabilityの略で、一般向けってことなのかね。

ワークスペースjreを設定します。

f:id:takeda_san:20171007110115p:plain

おっ、ちゃんと設定できてる。

んで、前に作ったJigsaw実験用のプロジェクトを確認してみる。
コードとjarの作り方はこの記事をみてね。

takeda-san.hatenablog.com

AppのjarとLibというjarがあって、AppクラスからLibクラスにはアクセス許可したいけど、
そのLibクラス内部で使ってるLibUtilクラスのpublicメソッドにはアクセスさせたくないって感じです。

f:id:takeda_san:20171007111702p:plain

f:id:takeda_san:20171007110740p:plain

オッ、すばらしい。
ちゃんと外部からは使えませんね。
ありがとうEclipse

JavaFXで白い画面がでるので…

白い画面現象

JavaFXの画面の描画が止まって、真っ白なウィンドウが表示される。
という現象が、何回か起きてます。

原因としては次のどちらかでした。

  1. ヒープ領域がいっぱいだった
    コントローラーとか、UI部品などの大きなオブジェクトの中に参照が残っていて画面を閉じても
    GCされずにのこってしまう。
    リスナーをaddしたらちゃんとremoveしようねッ。という話。
    これが起きたらヒープダンプをとってvisualVMかなんかで参照を持ってるオブジェクトを確認すると良い。

  2. 描画スレッドで重たい処理をしていた、またはデッドロックしてた
    JavaFXはどうやら「JavaFX Application Thread」で描画しているようなのだけど
    このスレッドでサーバーの応答を待つとか、巨大ファイルを読み込んだりをするとその間、描画が止まります。
    ちゃんと、描画と関係ない重い処理は、別のスレッドで実行しようねッ。という話。

あと、同じくこのスレッドでデッドロックすると当然描画が止まる。
これが起きたらスレッドダンプをとって、デッドロックしてないか見てみると良い。

第三の敵

そして襲来した第三の敵。
ヒープ領域も余裕があるし、スレッドダンプを確認しても描画用のスレッドは動いている。
頭を悩ませていた時に、天から『もしかしたら、ハード依存かもよ・・・かもよ・・・』とのお告げが。

[JDK-8154847] Windows content is blank when using StageStyle.UNIFIED - Java Bug System

そして、こいつである。
原因としては、

  • Windows10とintelの特定のチップセット(Intel(R) HD Graphics 5xxと思われる)の組み合わせで
  • 「StageStyle.UNIFIED」をステージに設定

すると発生するとのこと。
類似のバグ報告を確認するとIntel(R) HD Graphics 520とIntel(R) HD Graphics 530で起きることは確認されているようだ。
で、グラフィックドライバのバグだからしょうがないネ。的な感じなのかな。

もう仕方ないので、ハードでの描画をあきらめてソフト側で描画処理をすることにしました。
このバグのコメント欄にもあるんだけれども、VMオプションで『-Dprism.order=sw』をしていするとソフト側で描画してくれるっポイ。
無事解決なんだけど、ちょっとモヤモヤ。

テーブル内のチェックボックスをクリックしたときに行を選択したことにしたい

おのれ、チェックボックス

こんな画面、ギョームアプリケーションを作ったことがある方なら一度はあると思います。
チェックボックスを選択して、その行に対して操作を行うみたいなやつ。

f:id:takeda_san:20170725225412p:plain

JavaFXでは、CheckBoxTableCellというチェックボックス入りのテーブルセルクラスが標準であるのでそれを使います。
標準であるのはありがたいんですが、このクラス、選択時にテーブルのOnMouseClickedが動かないし、行を選択したことにもならないのよね。

普通ならテーブルの行をクリックすると、こんな感じで青く色が反転して選択状態になるんだけど。

f:id:takeda_san:20170725225943p:plain

チェックボックスをクリックすると

f:id:takeda_san:20170725230044p:plain

行は選択状態にならんのかい… ちなみに、テーブル側でOnMouseClickedを設定しておいても動きません。

チェックボックスを押したときでも、行を選択状態にしたい!!!
というわけで、(ムリヤリ)挙動を変えて使いやすくしましょう。

プログラム

今回もみんな大好きOracleのサンプルプログラムをすこし改変して使います。

docs.oracle.com

また、プログラム全文のせちゃいます。

メインクラス

package application;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class Main extends Application {
    private TableView<Person> table = new TableView<>();
    private ObservableList<Person> data = FXCollections.observableArrayList(
            new Person(false, "Jacob", "Smith", "jacob.smith@example.com"),
            new Person(false, "Isabella", "Johnson", "isabella.johnson@example.com"),
            new Person(false, "Ethan", "Williams", "ethan.williams@example.com"),
            new Person(false, "Emma", "Jones", "emma.jones@example.com"),
            new Person(false, "Michael", "Brown", "michael.brown@example.com"));

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        Scene scene = new Scene(new Group());
        stage.setTitle("Table View Sample");
        stage.setWidth(515);
        stage.setHeight(500);

        final Label label = new Label("Address Book");
        label.setFont(new Font("Arial", 20));

        CheckBoxColumn checkBoxCol = new CheckBoxColumn();
        checkBoxCol.setMinWidth(50);

        TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
        firstNameCol.setMinWidth(100);
        firstNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));

        TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
        lastNameCol.setMinWidth(100);
        lastNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));

        TableColumn<Person, String> emailCol = new TableColumn<>("Email");
        emailCol.setMinWidth(200);
        emailCol.setCellValueFactory(new PropertyValueFactory<>("email"));

        // なぜかテーブル全体をeditableにしないとチェックボックスが押せない…
        table.setEditable(true);

        // マウスクリック時に動作する
        table.setOnMouseClicked(event -> {
            System.out.println("行が選択されたぜ!!");
        });

        table.setItems(data);
        table.getColumns().add(checkBoxCol);
        table.getColumns().add(firstNameCol);
        table.getColumns().add(lastNameCol);
        table.getColumns().add(emailCol);

        final VBox vbox = new VBox();
        vbox.setSpacing(5);
        vbox.setPadding(new Insets(10, 0, 0, 10));
        vbox.getChildren().addAll(label, table);

        ((Group) scene.getRoot()).getChildren().addAll(vbox);

        stage.setScene(scene);
        stage.show();
    }

}

チェックボックスカラムのクラス

package application;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.Event;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;

public class CheckBoxColumn extends TableColumn<Person, Boolean> {

    public CheckBoxColumn() {
        // Personのcheckedプロパティと紐づける
        this.setCellValueFactory(new PropertyValueFactory<>("checked"));

        this.setCellFactory(column -> {

            // CheckBoxTableCellの挙動を定義する
            CheckBoxTableCell<Person, Boolean> cell = new CheckBoxTableCell<Person, Boolean>(index -> {
                BooleanProperty selected = new SimpleBooleanProperty(
                        this.getTableView().getItems().get(index).isChecked());

                selected.addListener((ov, o, n) -> {
                    // チェックボックスの状態が変わったらPersonのデータも更新する
                    this.getTableView().getItems().get(index).setChecked(n);

                    // チェックボックスが押されたので、行を選択したことにする
                    this.getTableView().getSelectionModel().select(index);

                    // テーブルにクリックイベントを飛ばす
                    Event.fireEvent(column.getTableView(), new MouseEvent(MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0,
                            MouseButton.PRIMARY, 1, true, true, true, true, true, true, true, true, true, true, null));
                });

                return selected;
            });

            return cell;
        });
    }
}

テーブルに表示するモデルのクラス

package application;

import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;

public class Person {

    private SimpleBooleanProperty checked;
    private SimpleStringProperty firstName;
    private SimpleStringProperty lastName;
    private SimpleStringProperty email;

    public Person(boolean checked, String fName, String lName, String email) {
        this.checked = new SimpleBooleanProperty(checked);
        this.firstName = new SimpleStringProperty(fName);
        this.lastName = new SimpleStringProperty(lName);
        this.email = new SimpleStringProperty(email);
    }

    public SimpleBooleanProperty checkedProperty() {
        return checked;
    }

    public boolean isChecked() {
        return checked.get();
    }

    public void setChecked(boolean checked) {
        this.checked.set(checked);
    }

    public String getFirstName() {
        return firstName.get();
    }

    public void setFirstName(String fName) {
        firstName.set(fName);
    }

    public String getLastName() {
        return lastName.get();
    }

    public void setLastName(String fName) {
        lastName.set(fName);
    }

    public String getEmail() {
        return email.get();
    }

    public void setEmail(String fName) {
        email.set(fName);
    }
}

長々とプログラムを載せましたが、大事なのはチェックボックスカラムクラスのここだけ。

// チェックボックスが押されたので、行を選択したことにする
this.getTableView().getSelectionModel().select(index);

// テーブルにクリックイベントを飛ばす
Event.fireEvent(column.getTableView(), new MouseEvent(MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0,
        MouseButton.PRIMARY, 1, true, true, true, true, true, true, true, true, true, true, null));

まずは、上の一行のほうから。
テーブルではSelectionModelというクラスを使って、任意に行の選択状態を変更したり、選択されている行のモデルデータを取得することができます。
これが、地味に便利。
今回は、チェックボックスの選択状態が変更されたセルの行番号indexを使って、それと同じ行を選択したことにしています。

次に、見るからに脳みそ筋肉な次の実装。
これは、第一引数のノードで第二引数のイベントを発生させるという処理を書いています。
テーブルにイベントが飛ばないならば、自分でイベントを生成して飛ばしてしまおうというわけです。

実行結果

f:id:takeda_san:20170725232446p:plain

行が選択されたぜ!!

にしても、テーブル周りの実装が、私には高度すぎて時間がかかる…
androidとかやっている人なら、もうちょっとセンス良く書けるのかしら。

// なぜかテーブル全体をeditableにしないとチェックボックスが押せない…
table.setEditable(true);

これな…

Windowsのコマンドプロンプトの代わりにcmderをつかう

私、Windowsを家でも仕事場でも好んで使っているのですが、コマンドプロンプトの使いにくさに少々困っておりまして、
代替のものはないかなぁと、いろいろ試してみた中で個人的に使いやすいものを見つけたのでメモ。

cmderというもの。
何よりハーフライフ的なロゴが気に入りました。

cmder.net

画面はこんな感じ。フォントも読みやすくて適度に色がついてます。

f:id:takeda_san:20170713224724p:plain

初めから使いやすいようにされてて素敵。
エディタでも何でもあまりカスタマイズして使うという習慣がないので、デフォルトで使いやすいのが良いです。
(世の皆様から、怒られそう)

タブで複数切り替え可能。
gitコマンドもsshも使えます。

というので、ひと記事終わるのも寂しいのでPythonインタプリタを使えるまで、やったこともメモしておきます。

Pythonをやっていく気持ち

今読んでる本だと、Anacondaディストリビューションが実際便利らしいのでインストール。 バージョン4.4.0をデフォルトでインストールしました。

Download Anaconda Now! | Continuum

cmderを立ち上げて「python –version」っと。

λ python  --version
'python' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

はいはい、環境変数ね。
インストール時に環境変数を設定するのはやめて、スタートメニューから選べるAnaconda Promptから起動しよう。な!
と、言われていた気がするので言われたとおりにしてみる。

f:id:takeda_san:20170713230922p:plain

うん、できたね。
でもね、コマンドプロンプトだと非常に見づらいのね。

この、Anaconda Promptが実行時に何を指定しているのか見てみる。

f:id:takeda_san:20170713231216p:plain

%windir%\system32\cmd.exe "/K" C:\Users\takeda\Anaconda3\Scripts\activate.bat C:\Users\takeda\Anaconda3

これをcmderに入れてやればよいのだろうか。
おもむろにcmderでWinキー + Alt + Tを押し、起動時の設定を入れる。

左下の「+」ボタンで新規追加して適当に名前を付ける。
(今回はcmd::Python)

んで、デフォルト設定っぽい、「cmd::Cmder」の設定をコピペしてくる。

f:id:takeda_san:20170713233150p:plain

んで一番下の段が、コマンドプロンプト実行時の各オプション指定。

cmd /k "%ConEmuDir%\..\init.bat & C:\Users\takeda\Anaconda3\Scripts\activate.bat"  -new_console:d:%USERPROFILE% C:\Users\takeda\Anaconda3

すでに、/kで一つバッチファイルが指定されているので、&で挟んでAnaconda Promptで指定されていたバッチファイルパス。 んで、末尾にファイルパスを指定する。(これなんだろうか…、ユーザ指定?)
最後に左下のSave settingsを押して設定完了。

右下の緑色の「+」ボタンから、先ほどの設定を使ってコマンドプロンプトを起動してみる。
そして、気合をいれて「python –version」!!

f:id:takeda_san:20170713233811p:plain

やりましたよ。奥様。

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に組み込んで、任意の文字列を送受信できるようにします。

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

ちょっとNettyを使う用事があったので、忘れないうちにメモ。
せっかくなのでJavaFXでソケット通信アプリケーションを作ってみる。

今回使っているバージョンは、「4.1.12-Final」です。

まずは、JavaFXを使わないで、サンプルプログラムを動かして動作を確認してみる。
[サーバ] → [クライアント] のような通信をするプログラムです。

ここの「Writing a Time Server」と「Writing a Time Client」からコピペです。

Netty.docs: User guide for 4.x

クライアントの接続を待つ、サーバ側のプログラム

package application.socket;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

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 {
                            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;

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

public class ServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) {
        final ByteBuf time = ctx.alloc().buffer(4);
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));

        final ChannelFuture f = ctx.writeAndFlush(time);
        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;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

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 {
                    ch.pipeline().addLast(new ClientHandler());
                }
            });

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

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

最後にサーバから送信されてきたバイト列を元の状態に変換する、クライアントハンドラ。

package application.socket;

import java.util.Date;

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

public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        } finally {
            m.release();
        }
    }

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

んで、サーバ→クライアントの順番で起動する。
(クライアントの実行時引数にサーバ側の待ち受けホスト名:localhost、ポート:8080を指定するのをお忘れなく)

実行結果(クライアント側)

Wed Jul 05 22:24:06 JST 2017

うむ、ちゃんと動いた。

ハンドラって何さ

送信時、受信時のデータを変換するための、クラスっぽい。
パイプラインという処理の流れでデータを変換していて、その一つの処理の塊がハンドラらしい。
公式のこの図が素晴らしくわかりやすい。

ChannelPipeline (Netty API Reference (4.1.12.Final))

                                                 I/O Request
                                            via Channel or
                                        ChannelHandlerContext
                                                      |
  +---------------------------------------------------+---------------+
  |                           ChannelPipeline         |               |
  |                                                  \|/              |
  |    +---------------------+            +-----------+----------+    |
  |    | Inbound Handler  N  |            | Outbound Handler  1  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  |               |                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler N-1 |            | Outbound Handler  2  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  .               |
  |               .                                   .               |
  | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
  |        [ method call]                       [method call]         |
  |               .                                   .               |
  |               .                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler  2  |            | Outbound Handler M-1 |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  |               |                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler  1  |            | Outbound Handler  M  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  +---------------+-----------------------------------+---------------+
                  |                                  \|/
  +---------------+-----------------------------------+---------------+
  |               |                                   |               |
  |       [ Socket.read() ]                    [ Socket.write() ]     |
  |                                                                   |
  |  Netty Internal I/O Threads (Transport Implementation)            |
  +-------------------------------------------------------------------+
 

自分のアプリケーション→別のアプリケーションへの送信時のハンドラをアウトバウンド。
別のアプリケーション→自分のアプリケーションの受信時はインバウンド。

つまり、今回の例だと次のような流れでデータがやり取りされていたということだろう。

(時刻情報)→ [ServerHandler] →(バイト列)→ [ClientHandler] →(時刻情報)
       ↑ここで変換          ↑ここで復元

動作のシーケンスを教えなさいよ

  1. サーバ起動 8080ポートで接続を待つ
  2. クライアント起動 ホスト名:localhost ポート:8080のサーバに接続しに行く
  3. サーバ、接続時に動作する「channelActive」メソッドを実行
    3.1. 時刻情報を取得する
    3.2. 時刻情報をint型としてバイト列に変換する
    3.3. バイト列を送信する
  4. サーバ、送信終了後に接続を切断する
  5. クライアント、データ受信時に動作する「channelRead」メソッドを実行
    5.1. 受信したバイト列をint型として読み込む
    5.2. 元の時刻情報(long型)に復元する
    5.3. コンソールに時刻情報(Date型として)を表示する

ざっくりとこんな感じだろうか。
次回はちょっとコードに手を入れて、任意の文字数のメッセージをやり取りできるようにします。