読者です 読者をやめる 読者になる 読者になる

エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JavaEE7をはじめよう(22) - WebSocket クライアント API

JavaEE

前回の記事までは、WebSocket のサーバー側の実装例を中心に紹介し、クライアント側の実装は JavaScript を利用してきた。

しかし、Java EE の WebSocket API はクライアント用の API も備えている。そのため、このクライアント用 API を使用すれば、ブラウザでなくとも、また JavaScript でなくとも、WebSocket によるメッセージ送受信を行うアプリケーションを作ることができる。

今回は、前回作成したチャットアプリケーションと接続して、メッセージを受信する Java アプリケーションを紹介する。

実行時のスクリーンショット

最初にチャットアプリケーションの実行イメージを示しておこう。

このアプリケーションは、JavaScript によるブラウザ版とタスクトレイに常駐してスタンドアロンで動作する Java アプリケーションの2つがある。

  • タスクトレイで左クリックで ping を選択すると、pong メッセージが表示される。

f:id:enterprisegeeks:20150213180838p:plain

  • ブラウザからチャットメッセージを送信すると、受信したメッセージがブラウザとタスクトレイの両方に表示される。

f:id:enterprisegeeks:20150213180845p:plain

  • サーバーから現在時刻の通知を受信すると、ブラウザとタスクトレイの両方に時刻が表示される。

f:id:enterprisegeeks:20150213180846p:plain

  • ブラウザから画像ファイルを送信すると、ファイルはユーザーフォルダに保存され、タスクトレイにメッセージが表示される。

f:id:enterprisegeeks:20150213180848p:plain

準備作業

さて、ソースコードの説明に入ろう。
今回作成したアプリケーションのソースコード全体は、github にある。

Java EE の WebSocket API は独立性が高く、WebSocket に関する機能のみであれば、Java SE 環境でも使用することが可能だ。 (ただし、CDIJPAEJB など他の機能を使用しない場合に限る。)

以下のライブラリに関する jar ファイルをクラスパスに設定する。

  • WebSocket API - WebSocket API の jar ファイル。
  • tyrus - WebSocket 仕様の参照実装。glassfish でも使用している。
  • grizzly - 非同期IOを使用したHTTPサーバー。同じく glassfish でも使用している。

なお、上記3つのライブラリでは、クライアント用、サーバ用、両方の3種類のモジュールを提供しているが、今回はそのうちクライアント用のモジュールを使用している。詳しい内容は、github 上のプロジェクトの pom.xml を参照いただきたい。

WebSocketクライアントのコード

WebSocket のクライアント側の実装は、クライアントエンドポイントのクラスを作ることで行う。

JavaScript での実装や、サーバーエンドポイントの実装とあまり変わるところはない。

クラス定義にはアノテーションを指定する

まず、WebSocket クライアントのクラス定義には@ClientEndpointアノテーションを指定する。このアノテーションは、クライアントエンドポイントとして定義するクラスには必須で指定する必要がある。また、クライアント側でもデコーダ、エンコーダを利用できる。

/**
 * WebSocket クライアント
 */
@ClientEndpoint(
  decoders = {Decoders.MessageDecoder.class, 
               Decoders.FileAttrDecoder.class})
public class WSclient  {
コンストラクタは自前で呼び出す

コンストラクタは次の通りである。 サーバーエンドポイントとは異なり、クライアントエンドポイントのコンストラクタは自前で呼び出すので、任意のコンストラクタを定義できる。 ここでは、タスクトレイにメッセージを出力するため、TrayIconインスタンスを渡している。

public class WSclient  {

    /** タスクトレイ */
    final private TrayIcon tray;

    /**
     * コンストラクタ
     * @param tray サーバーからのイベント受信にて、メッセージ表示を行う
     */
    public WSclient(TrayIcon tray) {
        this.tray = tray;
    }
サーバとの接続や切断時のコールバックメソッド

次に、サーバーからの通信があったときに呼び出されるコールバックメソッドについて解説する。 サーバーエンドポイントと同じアノテーションである@OnOpen@OnMessageなどが使用でき、メソッドの引数のルールなども同様となる。

まずは、サーバとの接続時やエラー時、サーバからの切断時のメソッドである。

@OnOpenは、クライアントからサーバへの接続時に、サーバから接続要求が受け付けられたときに呼び出される(接続に関するコードは後述する)。引数のSessionはサーバとの接続を示すオブジェクトであり、クライアントから通信を行う場合にも用いるためフィールドに保持している。

@OnErrorはなんらかのエラー時に、@OnCloseはサーバからの切断時にそれぞれ呼び出されるメソッドである。ここでは、後処理として、タスクトレイへの表示やログ出力を行っている。

    /** WebSocketセッション */
    private Session mySession;

    /** サーバー接続時の処理 */
    @OnOpen
    public void open(Session session) throws IOException{
        System.out.println(session.getId() + " was opened.");
        mySession = session;
        // クライアントからメッセージを1度でも
    // 送っておかないと受信できない場合がある。
        session.getBasicRemote().sendPing(null);
    }

    /** エラー時の処理 */
    @OnError
    public void error(Session session, Throwable e) {
        System.out.println(session.getId() + " was error.");
        e.printStackTrace();
        // trayにメッセージを表示。
        tray.displayMessage("エラー", e.getMessage(),
             TrayIcon.MessageType.ERROR);
        if (session.isOpen()) {
            try {
                session.close();
            } catch (IOException ex) {
            }
        }
    }
    
    /** サーバーからの切断時の処理 */
    @OnClose
    public void close(Session session) {
        System.out.println(session.getId() + " was closed.");
    }
メッセージ受信時のコールバックメソッド

メッセージ受信時のコールバックメソッドには、@OnMessageを指定する。サーバと同様に、テキスト用とバイナリ用の2つを定義できる。

テキスト用のonMessageメソッドでは、受け取ったメッセージをタスクトレイに表示するようにしている。このメソッドが呼び出される前には、デコーダが入力テキストを対応するオブジェクトに変換している。(デコーダの仕組みについては以前の記事を、ソースコードについては github を参照のこと。)

バリナリ用のメソッドでは、受け取ったファイルをローカルファイルに保存し、そのファイルパスをタスクトレイに表示するようにしている。

    /** 受信ファイル */
    private FileAttr file;

    /** サーバーからのメッセージ受信時の処理 */
    @OnMessage
    public void onMessage(TextBase text, Session ses)  {
        if (text instanceof Message) {
            onMessage((Message)text, ses);
        } else {
            this.file = (FileAttr)text;
        }
    }
    
    public void onMessage(Message message, Session ses)  {
        System.out.println("recieved:" + message.message);
        // trayにメッセージを表示。
        tray.displayMessage("From [" + message.name +"]",
                message.message, TrayIcon.MessageType.INFO);
    }

    /** サーバーからバイナリ受信時の処理 */
    @OnMessage
    public void onBinary(ByteBuffer buf, Session ses)  {
        System.out.println("recieved:binary");
        // trayにメッセージを表示。
        String home = System.getProperty("user.home");
        File output = new File(home, file.fileName);
        try(FileOutputStream os = new FileOutputStream(output);
                FileChannel oc = os.getChannel()){
            oc.write(buf);
        } catch(IOException e){
            throw new RuntimeException(e);
        }
        tray.displayMessage(
                "画像ファイルを受信 from[" + file.name + "]",
                output.getAbsolutePath(),
                TrayIcon.MessageType.INFO);
    }
    
クライアントからサーバへの通信を行うメソッド

続いて、クライアントからサーバへのメッセージ送信、および切断を行うメソッドを示す。これらのメソッドは、タスクトレイから送信メニューを選択した際に実行される。通信には、@OnOpenメソッドで取得したSessionオブジェクトを使用する。

    /** メッセージ送信
     * @param message メッセージ
     */
    public void sendMessage(String message) {
        try {
            mySession.getBasicRemote().sendText(message);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
    
    /** クライアントからの切断 */
    public void close() {
        try {
            mySession.close();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

タスクトレイアプリケーション

最後に、タスクトレイに常駐するアプリケーションのコードを示す。

/**
 * WebSocketからの通知を受け取るタスクトレイ
 */
public class WebSocketNotifierTray {

    /**
     * コンストラクタ 各種設定を行う
     * @param url WebSocketサーバーのURL
     */
    public WebSocketNotifierTray(String url) throws IOException,
            AWTException, DeploymentException, URISyntaxException {
        
        // アイコン
        Image icon = ImageIO.read(getClass()
                       .getResourceAsStream("/icon.png"));
        // 1. タスクトレイインスタンスの生成
        final TrayIcon tray = new TrayIcon(icon);
        
        // 2. WebSocketクライアント
        final WSclient client = new WSclient(tray);
        // サーバーへ接続
        WebSocketContainer container 
                = ContainerProvider.getWebSocketContainer();
        container.connectToServer(client, new URI(url));
        
        // 3. タスクトレイに設定するメニューの設定。
        
        // Pingメニュー:クライアントから、"ping"メッセージ送信
        MenuItem ping = new MenuItem("ping");
        ping.addActionListener(e -> client.sendMessage("ping"));
        
        // 終了メニュー
        MenuItem exit = new MenuItem("exit");
        exit.addActionListener(e -> {
            client.close();
            System.exit(0);
        });
        
        // ポップアップメニュー追加
        PopupMenu menu = new PopupMenu();
        menu.add(ping);
        menu.add(exit);
        
        tray.setPopupMenu(menu);
        
        // タスクトレイ格納
        SystemTray.getSystemTray().add(tray);
    }

1 では、java.awt.TrayIconを使ってタスクトレイに常駐するプログラムのインスタンスを作成している。ちなみにTrayIconは JDK6 から追加された API である。

2 では、クライアントエンドポイントのインスタンスを作成し、サーバーと接続を行っている。前述したとおり、クライアントのコールバックメソッドで、タスクトレイにメッセージを表示するため、TrayIconインスタンスを渡している。

その後、WebSocketContainer#connectToServerメソッドを呼び出すことで、WebSocket サーバーとの接続が開始される。 接続が完了したら、後はサーバーからのメッセージ受信を待機し続ける。

3 以降では、TrayIconへのポップアップメニュー(タスクトレイのアイコンを左クリックで表示されるメニュー)を設定している。このアプリケーションは基本的にはサーバーからの受信を受け付けるものだが、生存確認のための Ping 送信だけはこちらから送信可能とした。 また、アプリケーション終了のための Exit メニューも設定した。

少々余談になるが、これらのメニューを選択したときの処理は Java8 から提供されたラムダ式で記述している。

このクラスには、アプリケーションを起動するためのmainメソッドも定義してある。

    /** 
     * 起動 WebSocket接続URLはプログラム引数で与える
     * 
     * @param args 0は接続先のURL。無い場合デフォルト設定
     */
    public static void main(String[] args) throws Exception {
        String defaultUrl = 
           "ws://java-ee-example.herokuapp.com/" +
           "java_ee_example/websocket_sample";
      
        String url = args.length == 0 ? defaultUrl : args[0];
        
        new WebSocketNotifierTray(url);
    }
}

まとめ

今回は、WebSocket のクライアント用 API の使用例を紹介した。

WebSocket は HTTP と独立したプロトコルであり、Java による実装が提供されたことで、ブラウザ以外の用途にも利用できる。

参考文献

今回、タスクトレイのプログラムを作成するにあたっては、ITProの記事を参考にさせて頂いた。

[前多 賢太郎]