JavaEE7をはじめよう(22) - WebSocket クライアント API
前回の記事までは、WebSocket のサーバー側の実装例を中心に紹介し、クライアント側の実装は JavaScript を利用してきた。
しかし、Java EE の WebSocket API はクライアント用の API も備えている。そのため、このクライアント用 API を使用すれば、ブラウザでなくとも、また JavaScript でなくとも、WebSocket によるメッセージ送受信を行うアプリケーションを作ることができる。
今回は、前回作成したチャットアプリケーションと接続して、メッセージを受信する Java アプリケーションを紹介する。
実行時のスクリーンショット
最初にチャットアプリケーションの実行イメージを示しておこう。
このアプリケーションは、JavaScript によるブラウザ版とタスクトレイに常駐してスタンドアロンで動作する Java アプリケーションの2つがある。
- タスクトレイで左クリックで ping を選択すると、pong メッセージが表示される。
- ブラウザからチャットメッセージを送信すると、受信したメッセージがブラウザとタスクトレイの両方に表示される。
- サーバーから現在時刻の通知を受信すると、ブラウザとタスクトレイの両方に時刻が表示される。
- ブラウザから画像ファイルを送信すると、ファイルはユーザーフォルダに保存され、タスクトレイにメッセージが表示される。
準備作業
さて、ソースコードの説明に入ろう。
今回作成したアプリケーションのソースコード全体は、github にある。
Java EE の WebSocket API は独立性が高く、WebSocket に関する機能のみであれば、Java SE 環境でも使用することが可能だ。 (ただし、CDI や JPA、EJB など他の機能を使用しない場合に限る。)
以下のライブラリに関する 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の記事を参考にさせて頂いた。
[前多 賢太郎]