The Java EE 7 Tutorialの18 Java API for WebSocketのセクションを読んで訳した。
18 Java API for WebSocket
この章はWebSocketアプリケーションの開発をサポートするJava API for WebSocket(JSR 356)の解説をします。WebSocketはTCPプロトコルを介して二つのピア間で全二重通信を行うアプリケーションプロトコルです。
HTTPで使用される伝統的なリクエスト-レスポンスモデルでは、クライアントはリソースをリクエストし、それからサーバはレスポンスを返します。やり取りは常にクライアントから開始され、サーバはクライアントがまずリクエストをしないことには一切のデータ送信は出来ません。ごく稀に変更されるドキュメントにクライアントが時々リクエストを作成する場合にはこのモデルはWorld Wide Webで上手くいきましたが、コンテンツがすぐに更新されてユーザがWebによりインタラクティブな体験を期待するようになるとこのアプローチの限界が徐々に明らかになりつつあります。WebSocketプロトコルはクライアント・サーバ間で全二重通信チャネルを提供することでこの限界に取り組みます。JavaScriptとHTML5などの他のクライアント技術と組み合わせて、WebSocketはユーザ体験をより豊かに出来ます。
以下のトピックをこの章では扱います。
- Introduction to WebSocket
- Creating WebSocket Applications in the Java EE Platform
- Programmatic Endpoints
- Annotated Endpoints
- Sending and Receiving Messages
- Maintaining Client State
- Using Encoders and Decoders
- Path Parameters
- Handling Errors
- Specifying an Endpoint Configurator Class
- The dukeetf2 Example Application
- The websocketbot Example Application
- Further Information about WebSocket
18.1 Introduction to WebSocket
WebSocketアプリケーションでは、サーバはWebSocketエンドポイント(endpoint)にパブリッシュし、クライアントはサーバとの接続にエンドポイントのURIを使用します。WebSocketプロトコルはコネクション確立後は対称構造になり、クライアントとサーバはコネクションが開かれている間は何時でも互いにメッセージ送信が可能で、何時でもどちらかがコネクションをクローズできます。クライアントは通常一つのサーバとのみ接続し、サーバは複数クライアントからのコネクションを受け入れます。
WebSocketプロトコルは二つの構造からなり、それはハンドシェイク(handshake)とデータ転送(data transfer)です。クライアントはWebSocketエンドポイントにURLでリクエストを送信することでハンドシェイクを初期化します。ハンドシェイクは既存のHTTPベースのインフラと互換性があります。WebサーバーはHTTPコネクションのアップグレードリクエスト(HTTP connection upgrade request)として解釈します。クライアントからのハンドシェイクの例は以下のようになります。
GET /path/to/websocket/endpoint HTTP/1.1 Host: localhost Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg== Origin: http://localhost Sec-WebSocket-Version: 13
サーバがクライアントに返すレスポンスのハンドシェイクの例は以下のようになります。
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=
サーバはSec-WebSocket-Accept
ヘッダーの値を生成するためにSec-WebSocket-Key
に演算を適用します。クライアントはSec-WebSocket-Key
ヘッダーの値に同じ演算を適用し、もしサーバから受け取った値と演算結果が一致する場合、コネクションの確立が成功します。クライアントとサーバはハンドシェイク成功後は互いにメッセージ送信が可能になります。
WebSocketはテキストメッセージ(UTF-8でエンコードされたもの)とバイナリメッセージをサポートします。WebSocketのコントロールフレーム(control frames)は、close, ping, pong(pingフレームのレスポンス)です。pingとpongフレームはアプリケーションデータを含むこともあります。
WebSocketエンドポイントは以下の形式を持つURIによって表現されます。
ws://host:port/path?query wss://host:port/path?query
ws
スキーマは暗号化されないWebSocketの接続を表現し、wss
スキーマは暗号化された接続を表現します。port
はオプションで、デフォルトポート番号は暗号化されない接続では80で暗号化された接続では443です。path
はサーバ内のエンドポイントの位置を示します。query
はオプションです。
モダンなwebブラウザはWebSocketプロトコルを実装し、JavaScript APIを提供し、このAPIはエンドポイントへの接続・メッセージ送信・WebSocketイベント(コネクションの開始、メッセージ受信、切断など)にコールバックメソッドの割り当て、をすることが出来ます。
18.2 Creating WebSocket Applications in the Java EE Platform
Java EEプラットフォームにはJava API for WebSocket (JSR 356)が含まれており、アプリケーションにWebSocketエンドポイントを作成・設定・デプロイすることが出来ます。また、JSR 356で定義されるWebSocketクライアントAPIは任意のJavaアプリケーションからリモートのWebSocketエンドポイントにアクセスできます。
Java API for WebSocketは以下のパッケージで構成されています。
javax.websocket.server
パッケージには、サーバーエンドポイントの設定と作成をするためのアノテーション・クラス・インタフェースが含まれています。javax.websocket
パッケージには、クライアントとサーバーエンドポイントに共通のアノテーション・クラス・インタフェース・例外が含まれています。
WebSocketエンドポイントはjavax.websocket.Endpoint
クラスのインスタンスです。Java API for WebSocketには二種類のエンドポイント作成が可能で、programmaticとannotatedの二つです。programmatic endpointを作成するには、Endpoint
クラスを拡張してライフサイクルメソッドをオーバーライドします。annotated endpointを作成するには、上記リストに書かれているパッケージで提供されるアノテーションをJavaクラスとメソッドに付与します。エンドポイント作成後、リモートクライアントが接続できるようにアプリケーションを特定のURIでデプロイします。
Note:
大抵のケースにおいて、programmatic endpointよりもannotated endpointで作成とデプロイをする方が簡単です。このチャプターではprogrammatic endpointの単純な例を示しますが、annotated endpointsに焦点を当てていきます。
WebSocketエンドポイントの作成とデプロイ手順は以下のようになります。
この手順はprogrammaticとannotatedエンドポイントで多少異なりますが、以降のセクションで詳細を説明します。
Note:
servletと比較すると、WebSocketエンドポイントは複数回インスタンス化されます。コンテナはデプロイメントURIへの接続ごとにエンドポイントのインスタンスを生成します。各インスタンスは単一の接続とだけ関連付けられます。エンドポイントインスタンスのコードを実行するスレッドは常に一つだけなので、各コネクションにユーザ状態を保持し続けたりデプロイすることが容易になります。
18.3 Programmatic Endpoints
以下の例はEndpoint
クラスを拡張してエンドポイントを作成する方法です。
public class EchoEndpoint extends Endpoint { @Override public void onOpen(final Session session, EndpointConfig config) { session.addMessageHandler(new MessageHandler.Whole<String>() { @Override public void onMessage(String msg) { try { session.getBasicRemote().sendText(msg); } catch (IOException e) { ... } } }); } }
このエンドポイントは受信したメッセージをエコーバックします。Endpoint
クラスは三つのライフサイクルメソッド、onOpen
, onClose
, onError
を定義しています。EchoEndpoint
クラスはEndpoint
クラスで唯一の抽象メソッドonOpen
メソッドを実装しています。
Session
引数はこことリモートのエンドポイント間の対話(conversation)を表現します。addMessageHandler
メソッドはメッセージハンドラーを登録し、getBasicRemote
メソッドはリモートのエンドポイントを表現するオブジェクトを返します。Session
インタフェースはこのチャプター内で詳細を説明します。
メッセージハンドラーは無名内部クラスとして実装されています。メソッドハンドラーのonMessage
メソッドはエンドポイントがテキストメッセージを受信するときに呼び出されます。
このprogrammaticエンドポイントをデプロイするには、Java EEアプリケーションで以下のコードを使用します。
ServerEndpointConfig.Builder.create(EchoEndpoint.class, "/echo").build();
アプリケーションをデプロイすると、エンドポイントはws://<host>:<port>/<application>/echo
で利用可能になり、たとえばws://localhost:8080/echoapp/echo
などで使用します。
18.4 Annotated Endpoints
以下の例はアノテーションを使用してProgrammatic Endpointsの例と同様なエンドポイントを作成しています。
@ServerEndpoint("/echo") public class EchoEndpoint { @OnMessage public void onMessage(Session session, String msg) { try { session.getBasicRemote().sendText(msg); } catch (IOException e) { ... } } }
annotated endpointはprogrammatic endpointと比べるとシンプルで、ServerEndpoint
アノテーションで定義されるパスでアプリケーションへ自動的にデプロイされます。メッセージハンドラー用に追加でクラスを作る代わりに、この例ではメッセージ処理を実行するメソッドを指定するOnMessage
アノテーションを使用しています。
Table 18-1はライサイクルメソッドを処理するメソッドを指定するためにjavax.websocket
パッケージで利用可能なアノテーションの一覧表です。この表の例の列はそのメソッドで良く使う引数です。各ケースで使用可能な引数の組み合わせの詳細についてはAPIリファレンスを参照してください。
Table 18-1 WebSocket Endpoint Lifecycle Annotations
http://docs.oracle.com/javaee/7/tutorial/doc/websocket004.htm#BABDGEJH を参照。*1
18.5 Sending and Receiving Messages
WebSocketエンドポイントはテキストとバイナリメッセージを送受信可能です。また、ping/pongフレームの送受信も可能です。このセクションでは、接続されたピアにメッセージを送るためのSession
とRemoteEndpoint
インタフェースと、メッセージを受信するためのOnMessage
アノテーションの使い方について説明します。
18.5.1 Sending Messages
エンドポイントにメッセージ送信するステップは以下のステップを踏みます。
- コネクションから
Session
オブジェクトを取得する。
Session
オブジェクトは、Table18-1に示したように、エンドポイントでアノテーション付与されたライフサイクルメソッドの引数として利用可能です。ピアからのメッセージに応答する場合、メッセージを受信したメソッド(@OnMessage
アノテーションを付与されたメソッド)内でSession
オブジェクトが利用可能です。もしレスポンスではなくメッセージ送信をする場合、エンドポイントクラスのインスタンス変数としてSession
オブジェクトを@OnOpen
アノテーションのメソッドで保存し、他のメソッドでこのオブジェクトにアクセスすることが出来ます。 RemoteEndpoint
オブジェクトを取得するためにSession
オブジェクトを使用する。
Session.getBasicRemote
とSession.getAsyncRemote
メソッドは、それぞれRemoteEndpoint.Basic
とRemoteEndpoint.Async
を返します。RemoteEndpoint.Basic
インタフェースは送信メッセージのメソッドをブロックしますが、RemoteEndpoint.Async
はノンブロッキングです。- ピアにメッセージを送信するための
RemoteEndpoint
オブジェクトを使用する。
以下にピアへのメッセージ送信するメソッドの使用例を示します。void RemoteEndpoint.Basic.sendText(String text)
ピアにテキストメッセージを送信します。このメソッドはメッセージがすべて送信されるまでブロックします。void RemoteEndpoint.Basic.sendBinary(ByteBuffer data)
ピアにバイナリメッセージを送信します。このメソッドはメッセージがすべて送信されるまでブロックします。void RemoteEndpoint.sendPing(ByteBuffer appData)
ピアにpingフレームを送信します。void RemoteEndpoint.sendPong(ByteBuffer appData)
ピアにpongフレームを送信します。
到着するテキストメッセージに応答する方法についてはAnnotated Endpointsで説明しています。
18.5.1.1 Sending Messages to All Peers Connected to an Endpoint
エンドポイントの各インスタンスは一つ以上のコネクションとピアに関連付けられます。場合によっては、エンドポイントインスタンスが接続している全ピアにメッセージ送信する必要があることもあります。例としてはチャットやネットオークションなどです。Session
インタフェースはこの目的のためにgetOpenSessions
メソッドを提供しています。以下の例は接続している全ピアに到着したテキストメッセージをフォワードするためのこのメソッドを使用しています。
@ServerEndpoint("/echoall") public class EchoAllEndpoint { @OnMessage public void onMessage(Session session, String msg) { try { for (Session sess : session.getOpenSessions()) { if (sess.isOpen()) sess.getBasicRemote().sendText(msg); } } catch (IOException e) { ... } } }
18.5.2 Receiving Messages
OnMessage
アノテーションは到着するメッセージを処理するメソッドを指定するものです。エンドポイントには最大三つの@OnMessage
アノテーションを付与するメソッドを作成でき、メッセージタイプがテキストかバイナリのものと、pongです。以下にメッセージのタイプ別に三つのメソッドを作成する方法を示します。
@ServerEndpoint("/receive") public class ReceiveEndpoint { @OnMessage public void textMessage(Session session, String msg) { System.out.println("Text message: " + msg); } @OnMessage public void binaryMessage(Session session, ByteBuffer msg) { System.out.println("Binary message: " + msg.toString()); } @OnMessage public void pongMessage(Session session, PongMessage msg) { System.out.println("Pong message: " + msg.getApplicationData().toString()); } }
18.6 Maintaining Client State
コンテナはコネクションごとにエンドポイントクラスのインスタンスを生成するので、クライアント状態の情報を格納するためにインスタンス変数を使用可能です。また、Session.getUserProperties
メソッドがユーザープロパティを格納するための更新可能なmapを提供します。たとえば、以下のエンドポイントは到着したテキストメッセージに対して、各クライアントからの直前のメッセージ内容を返します。
@ServerEndpoint("/delayedecho") public class DelayedEchoEndpoint { @OnOpen public void open(Session session) { session.getUserProperties().put("previousMsg", " "); } @OnMessage public void message(Session session, String msg) { String prev = (String) session.getUserProperties() .get("previousMsg"); session.getUserProperties().put("previousMsg", msg); try { session.getBasicRemote().sendText(prev); } catch (IOException e) { ... } } }
すべての接続クライアントに共通な情報を格納するには、クラスのstatic変数が使用可能です。ただし、プログラマにはそれらの変数へのアクセスがスレッドセーフなことを保証する責任があります。
18.7 Using Encoders and Decoders
Java API for WebSocketは、WebSocketメッセージとカスタムJavaクラス間での変換を行うエンコーダ・デコーダをサポートしています。エンコーダはJavaオブジェクトを受け取ってWebSocketメッセージとして送信可能な表現を生成します。たとえば、エンコーダは、一般的にはJSON,XMLあるいはバイナリ表現を生成します。デコーダはその逆をするもので、WebSocketメッセージを読み込んでJavaオブジェクトを生成します。
このメカニズムはWebSocketアプリケーションをシンプルにするもので、その理由はオブジェクトのシリアライズ・デシリアライズからビジネスロジックを切り離すためです。
18.7.1 Implementing Encoders to Convert Java Objects into WebSocket Messages
以下にエンドポイントでエンコーダを使用して実装する手順を示します。
- 以下のインタフェースの一つを実装します。
ServerEndpoint
のオプションパラメータencoders
にエンコーダ実装を追加します。- メッセージとしてオブジェクトを送信するためにインタフェース
RemoteEndpoint.Basic
もしくはRemoteEndpoint.Async
のメソッドsendObject(Object data)
を使用します。コンテナは型が一致するエンコーダを検索し、オブジェクトをWebSocketメッセージへ変換するためにそのクラスを使用します。
たとえば、いま二つのJavaクラス(MessageA
とMessageB
)をテキストメッセージとして送信したいとすると、以下のようにEncoder.Text<MessageA>
とEncoder.Text<MessageB>
インタフェースを実装します。
public class MessageATextEncoder implements Encoder.Text<MessageA> { @Override public void init(EndpointConfig ec) { } @Override public void destroy() { } @Override public String encode(MessageA msgA) throws EncodeException { // msgAのプロパティにアクセスしてJSONテキストに変換する。。。 return msgAJsonString; } }
Encoder.Text<MessageB>
も同じように実装します。それから、以下のようにServerEndpoint
アノテーションにencoders
パラメータを追加します。
@ServerEndpoint( value = "/myendpoint", encoders = { MessageATextEncoder.class, MessageBTextEncoder.class } ) public class EncEndpoint { ... }
WebSocketメッセージとしてMessageA
とMessageB
を送信するにはsendObject
メソッドを以下のように使用します。
MessageA msgA = new MessageA(...); MessageB msgB = new MessageB(...); session.getBasicRemote.sendObject(msgA); session.getBasicRemote.sendObject(msgB);
この例のように、テキスト・バイナリメッセージそれぞれのエンコーダを複数指定可能です。エンドポイント同様、エンコーダは一つ以上のWebSocketコネクションとピアに関連付けられ、エンコーダインスタンスのコードを実行するスレッドは常時一つのみです。
18.7.2 Implementing Decoders to Convert WebSocket Messages into Java Objects
以下にエンドポイントでデコーダを使用して実装する手順を示します。
- 以下のインタフェースの一つを実装します。
ServerEndpoint
のオプションパラメータdecoders
にデコーダ実装を追加します。- エンドポイントで引数としてカスタムJavaクラスを取るメソッドに
OnMessage
アノテーションを使用します。指定したデコーダでデコード可能なメッセージをエンドポイントが受信するとき、コンテナは引数としてカスタムJavaクラスを取る@OnMessage
アノテーションを付与したメソッドが存在すればこれを呼び出します。
いま、テキストメッセージとして送受信したい二つのJavaクラス(MessageA
とMessageB
)があり、共通クラス(Message
)を拡張して定義している、とします。テキストメッセージのデコーダは一つだけ定義可能なので、Message
クラスのデコーダは以下のように実装します。
public class MessageTextDecoder implements Decoder.Text<Message> { @Override public void init(EndpointConfig ec) { } @Override public void destroy() { } @Override public Message decode(String string) throws DecodeException { // Read message... if ( /* message is an A message */ ) return new MessageA(...); else if ( /* message is a B message */ ) return new MessageB(...); } @Override public boolean willDecode(String string) { // メッセージがMessageAかMessageBのどちらかに変換可能か // 判定します。 return canDecode; } }
以下のようにServerEndpoint
アノテーションのパラメータにdecoder
を追加します。
@ServerEndpoint( value = "/myendpoint", encoders = { MessageATextEncoder.class, MessageBTextEncoder.class }, decoders = { MessageTextDecoder.class } ) public class EncDecEndpoint { ... }
そして、MessageA
とMessageB
を受信するエンドポイントのメソッドを以下のように定義します。
@OnMessage public void message(Session session, Message msg) { if (msg instanceof MessageA) { // We received a MessageA object... } else if (msg instanceof MessageB) { // We received a MessageB object... } }
エンドポイント同様に、デコーダのインスタンスは一つのWebSocketコネクションとピア にだけ関連付けられるため、ある時間にデコーダインスタンスのコードを実行するスレッドは一つだけです。
18.8 Path Parameters
ServerEndpoint
アノテーションはアプリケーションのパラメータとしてエンドポイントデプロイメントURIの一部をURIテンプレートを使用して定義可能です。
@ServerEndpoint("/chatrooms/{room-name}") public class ChatEndpoint { ... }
いま、ローカルのJava EEサーバにポート8080でchatapp
というwebアプリケーションでエンドポイントをデプロイしているとすると、クライアントは以下のようなURIを使用してエンドポイントに接続可能です。
http://localhost:8080/chatapp/chatrooms/currentnews http://localhost:8080/chatapp/chatrooms/music http://localhost:8080/chatapp/chatrooms/cars http://localhost:8080/chatapp/chatrooms/technology
annotated endpointは@OnOpen
, @OnMessage
, @OnClose
アノテーションを付与されたメソッドの引数としてパスパラメータを受け取ることができます。たとえば、クライアントが入室したいチャットルームを決定するために@OnOpen
メソッドでパラメータを使用できます。
@ServerEndpoint("/chatrooms/{room-name}") public class ChatEndpoint { @OnOpen public void open(Session session, EndpointConfig c, @PathParam("room-name") String roomName) { // 選択したチャットルームにクライアントを追加する。 } }
メソッドの引数として使用可能なパスパラメータは、文字列・プリミティブ型とそのラッパー型のいずれかです。
18.9 Handling Errors
アノテーションが付与されているWebSocketエンドポイントでエラー処理をするメソッドを定義するには、@OnError
を付与します。
@ServerEndpoint("/testendpoint") public class TestEndpoint { ... @OnError public void error(Session session, Throwable t) { t.printStackTrace(); ... } }
このメソッドが呼び出されるのは、コネクションに問題が発生・メッセージ処理中のランタイムエラー・メッセージデコード時の変換エラー、の場合です。
18.10 Specifying an Endpoint Configurator Class
Java API for WebSocketでは、コンテナがサーバーエンドポイントのインスタンスをどのように生成するかを設定できます。以下のようなロジックのエンドポイント設定を作成可能です。
- WebSocketコネクション用の初回HTTPリクエストの詳細へのアクセス。
Origin
HTTPヘッダーのカスタムチェックの実行。- WebSocketハンドシェークレスポンスの修正。
- クライアントからリクエストされるWebSocketサブプロトコルの選択。
- エンドポイントインスタンスの初期化とインスタンス化の制御。
カスタムのエンドポイント設定ロジックを構築するには、ServerEndpointConfig.Configurator
クラスを拡張してそのメソッドをオーバーライドします。エンドポイントクラスでは、ServerEndpoint
アノテーションのconfigurator
パラメータを使用してconfiguratorクラスを指定します。
例として、以下のconfiguratorクラスはエンドポイントインスタンスでハンドシェイクリクエストオブジェクトを使用可能にしています。
public class CustomConfigurator extends ServerEndpointConfig.Configurator { @Override public void modifyHandshake(ServerEndpointConfig conf, HandshakeRequest req, HandshakeResponse resp) { conf.getUserProperties().put("handshakereq", req); } }
以下のエンドポイントクラスは、ハンドシェイクリクエストオブジェクトにアクセス可能なカスタムconfiguratorクラスを、エンドポイントインスタンスに設定しています。
@ServerEndpoint( value = "/myendpoint", configurator = CustomConfigurator.class ) public class MyEndpoint { @OnOpen public void open(Session s, EndpointConfig conf) { HandshakeRequest req = (HandshakeRequest) conf.getUserProperties() .get("handshakereq"); Map<String,List<String>> headers = req.getHeaders(); ... } }
エンドポイントクラスは、HttpSession
オブジェクトやヘッダーのような、HTTPリクエストの詳細にアクセスするためのハンドシェイクリクエストオブジェクトを使用可能です。
エンドポイント設定の詳細な情報については、ServerEndpointConfig.Configurator
のAPIリファレンスを参照してください。
18.11 The dukeetf2 Example Application
dukeetf2
サンプルアプリケーションは、tut-install/examples/web/websocket/dukeetf2/
ディレクトリ*2にアップされており、webクライアントへデータアップデートをするためのWebSocketエンドポイントの使用方法のサンプルとなっています。このサンプルは電子証券取引における定期的な出来高と価格の更新を提供するサービスのようなものです。
18.11.1 Architecture of the dukeetf2 Sample Application
dukeetf2
サンプルアプリケーションは、WebSocketエンドポイント・EJB・HTMLページ、から構成されます。
- エンドポイントはクライアントからの接続を受け付け、出来高と価格の新しいデータが使用可能ならその更新情報を送信します。
- EJBは毎秒出来高と価格を更新します。
- HTMLページはWebSocketエンドポイントに接続するためにJavaScriptを使用し、受信メッセージをパースし、ページをリロードせずに出来高と価格の情報を更新します。
18.11.1.1 The Endpoint
WebSocketエンドポイントはETFEndpoint
クラスで実装されており、キューにすべての接続セッションを格納し、送信する新情報があるときにEJBが呼び出すメソッドを定義しています。
@ServerEndpoint("/dukeetf") public class ETFEndpoint { private static final Logger logger = Logger.getLogger("ETFEndpoint"); /* Queue for all open WebSocket sessions */ static Queue<Session> queue = new ConcurrentLinkedQueue<>(); /* PriceVolumeBean calls this method to send updates */ public static void send(double price, int volume) { String msg = String.format("%.2f, %d", price, volume); try { /* Send updates to all open WebSocket sessions */ for (Session session : queue) { session.getBasicRemote().sendText(msg); logger.log(Level.INFO, "Sent: {0}", msg); } } catch (IOException e) { logger.log(Level.INFO, e.toString()); } } ... }
エンドポイントのライフサイクルメソッドはキューにセッションの追加と削除を行います。
@ServerEndpoint("/dukeetf") public class ETFEndpoint { ... @OnOpen public void openConnection(Session session) { /* Register this connection in the queue */ queue.add(session); logger.log(Level.INFO, "Connection opened."); } @OnClose public void closedConnection(Session session) { /* Remove this connection from the queue */ queue.remove(session); logger.log(Level.INFO, "Connection closed."); } @OnError public void error(Session session, Throwable t) { /* Remove this connection from the queue */ queue.remove(session); logger.log(Level.INFO, t.toString()); logger.log(Level.INFO, "Connection error."); } }
18.11.1.2 The Enterprise Bean
EJBは毎秒出来高と価格の情報を生成するためにタイマーサービスを使用します。
@Startup @Singleton public class PriceVolumeBean { /* Use the container's timer service */ @Resource TimerService tservice; private Random random; private volatile double price = 100.0; private volatile int volume = 300000; private static final Logger logger = Logger.getLogger("PriceVolumeBean"); @PostConstruct public void init() { /* Initialize the EJB and create a timer */ logger.log(Level.INFO, "Initializing EJB."); random = new Random(); tservice.createIntervalTimer(1000, 1000, new TimerConfig()); } @Timeout public void timeout() { /* Adjust price and volume and send updates */ price += 1.0*(random.nextInt(100)-50)/100.0; volume += random.nextInt(5000) - 2500; ETFEndpoint.send(price, volume); } }
EJBはタイムアウトメソッドでETFEndpoint
クラスのsend
メソッドを呼び出します。タイマーサービスの詳細な情報についてはChapter 34, "Running the Enterprise Bean Examples"のUsing the Timer Serviceを参照してください。
18.11.1.3 The HTML Page
HTMLページはテーブルとJavaScriptのコードで構成されます。テーブルはJavaScriptから参照する二つのフィールドを持っています。
<!DOCTYPE html> <html> <head>...</head> <body> ... <table> ... <td id="price">--.--</td> ... <td id="volume">--</td> ... </table> </body> </html>
JavaScriptはサーバエンドポイントへの接続と受信メッセージ用のコールバックメソッドを指定するためにWebSocket APIを使用します。コールバックメソッドは新しい情報でページを更新します。
var wsocket; function connect() { wsocket = new WebSocket("ws://localhost:8080/dukeetf2/dukeetf"); wsocket.onmessage = onMessage; } function onMessage(evt) { var arraypv = evt.data.split(","); document.getElementById("price").innerHTML = arraypv[0]; document.getElementById("volume").innerHTML = arraypv[1]; } window.addEventListener("load", connect, false);
WebSocket APIは多くのモダンなブラウザでサポートされており、HTML5 webクライアントの開発に広く使用されています。
18.11.2 Running the dukeetf2 Example Application
このセクションではNetBeans IDEとコマンドラインでdukeetf2
サンプルアプリケーションを実行する方法について説明します。
18.11.2.1 To Run the dukeetf2 Example Application Using NetBeans IDE
- GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
- FileメニューからOpen Projectを選択
- Open Projectダイアログボックスから以下のディレクトリに移動。
tut-install/examples/web/websocket
dukeetf2
フォルダを選択。- Open Projectをクリック。
- Projectsタブで
dukeetf2
プロジェクトを右クリックしてRunを選択。
このコマンドはアプリケーションをビルドしてWARファイル(dukeetf2.war
)にパッケージしてtarget
ディレクトリに配置し、サーバにデプロイし、以下のURLでwebブラウザーを開きます。
http://localhost:8080/dukeetf2/
- 同じURLを別のwebブラウザーのタブかウィンドウで開き、両方のページが出来高と値を同時に更新するのを確認します。
18.11.2.2 To Run the dukeetf2 Example Application Using Maven
- GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
- ターミナルで以下に移動:
tut-install/examples/web/websocket/dukeetf2/
- アプリケーションをデプロイするために以下のコマンドを実行。
mvn install
- webブラウザを開いて以下のアドレスを入力。
http://localhost:8080/dukeetf2/
- 同じURLを別のwebブラウザーのタブかウィンドウで開き、両方のページが出来高と値を同時に更新するのを確認します。
18.12 The websocketbot Example Application
websocketbot
サンプルアプリケーションは、tut-install/examples/web/websocket/websocketbot/
ディレクトリ*3にアップされており、チャットの実装にWebSocketエンドポイントを使用する方法のサンプルとなっています。このサンプルは多数のユーザが入室して会話できるチャットルームの例です。ユーザはチャットルームで常に使用可能なbotエージェントに簡単な質問を投げることができます。
18.12.1 Architecture of the websocketbot Example Application
websocketbot
サンプルアプリケーションは以下の要素で構成されています。
- メッセージに応答するbotエージェントのロジックを持つCDIビーン(
BotBean
) - チャットルームを実装するWebSocketエンドポイント(
BotEndpoint
) - アプリケーションのメッセージを表現するクラス(
Message
,ChatMessage
,InfoMessage
,JoinMessage
,UsersMessage
) - アプリケーションのメッセージをJSONデータ形式のWebSocketテキストメッセージにエンコードするクラス(
ChatMessageEncoder
,InfoMessageEncoder
,JoinMessageEncoder
,UsersMessageEncoder
) - JSONデータ形式のWebSocketテキストメッセージをパースして
JoinMessage
やChatMessage
にデコードするクラス(MessageDecoder
) - チャットルームのクライアントを実装するJavaScriptコードを含むHTMLページ(
index.html
)
18.12.1.1 The CDI Bean
CDIビーンはrespond
メソッドを持つJavaクラスです。このメソッドは事前設定された質問とチャットメッセージを比較してレスポンスを返します。
@Named public class BotBean { public String respond(String msg) { ... } }
18.12.1.2 The WebSocket Endpoint
WebSocketエンドポイント(BotEndpoint
)はannotated endpointで以下の機能を実行します。
- クライアントからのメッセージ受信
- クライアントへのメッセージの転送
- 接続クライアントリストのメンテナンス
- botエージェント機能の呼び出し
エンドポイントはServerEndpoint
アノテーションを使用してデプロイメントURI・エンコーダ・デコーダを定義します。エンドポイントは、DI経由でBotBean
クラスとmanaged executor service resourceのインスタンスを取得します。
@ServerEndpoint( value = "/websocketbot", decoders = { MessageDecoder.class }, encoders = { JoinMessageEncoder.class, ChatMessageEncoder.class, InfoMessageEncoder.class, UsersMessageEncoder.class } ) /* There is a BotEndpoint instance per connection */ public class BotEndpoint { private static final Logger logger = Logger.getLogger("BotEndpoint"); /* Bot functionality bean */ @Inject private BotBean botbean; /* Executor service for asynchronous processing */ @Resource(name="comp/DefaultManagedExecutorService") private ManagedExecutorService mes; @OnOpen public void openConnection(Session session) { logger.log(Level.INFO, "Connection opened."); } ... }
message
メソッドはクライアントから送られるメッセージを処理します。デコーダは到着するテキストメッセージを、Message
クラスを拡張するJoinMessage
かChatMessage
に変換します。message
メソッドは引数としてMessage
オブジェクトを受け取ります。
@OnMessage public void message(Session session, Message msg) { logger.log(Level.INFO, "Received: {0}", msg.toString()); if (msg instanceof JoinMessage) { /* Add the new user and notify everybody */ JoinMessage jmsg = (JoinMessage) msg; session.getUserProperties().put("name", jmsg.getName()); session.getUserProperties().put("active", true); logger.log(Level.INFO, "Received: {0}", jmsg.toString()); sendAll(session, new InfoMessage(jmsg.getName() + " has joined the chat")); sendAll(session, new ChatMessage("Duke", jmsg.getName(), "Hi there!!")); sendAll(session, new UsersMessage(this.getUserList(session))); } else if (msg instanceof ChatMessage) { /* Forward the message to everybody */ ChatMessage cmsg = (ChatMessage) msg; logger.log(Level.INFO, "Received: {0}", cmsg.toString()); sendAll(session, cmsg); if (cmsg.getTarget().compareTo("Duke") == 0) { /* The bot replies to the message */ mes.submit(new Runnable() { @Override public void run() { String resp = botbean.respond(cmsg.getMessage()); sendAll(session, new ChatMessage("Duke", cmsg.getName(), resp)); } }); } } }
メッセージが入室メッセージなら、エンドポイントは新規ユーザをリストに追加してすべての接続クライアントに通知をします。メッセージがチャットメッセージなら、エンドポイントはすべての接続クライアントにメッセージを転送します。
チャットメッセージがbotエージェントに対してなら、エンドポイントはBotBean
インスタンスからレスポンスを取得してすべての接続クライアントに送信します。sendAll
メソッドについてはSending Messages to All Peers Connected to an Endpointの例も参照してください。
Asynchronous Processing and Concurrency Considerations
WebSocketエンドポイントはbotのレスポンスを取得するためにBotBean.respond
メソッドを呼び出します。このサンプルでは、これはブロック操作のため、メッセージ送信をしたユーザは操作が完了するまで他のチャットメッセージの送受信が出来なくなります。この問題を避けるために、エンドポイントはコンテナからexecutor serviceを取得してConcurrency Utilities for Java EEのManagedExecutorService.submit
メソッドを使用して異なるスレッドでブロック操作を実行します。
Java EE実装を要求するJava API for WebSocket仕様はコネクションごとにエンドポイントクラスをインスタンス化します。WebSocketエンドポイントクラスのコードを実行するスレッドは常に唯一つだけだと保障されるので、WebSocketエンドポイントの構築が容易になります。この例のように、エンドポイントで新規スレッドを生成する場合、一つ以上のスレッドからアクセスされる変数とメソッドがスレッドセーフだと保障する必要があります。この例では、BotBean
のコードはスレッドセーフで、BotEndpoint.sendAll
メソッドはsynchronized
宣言がされています。
managed executor serviceとConcurrency Utilities for Java EEの詳細な情報についてはChapter 56, "Concurrency Utilities for Java EE"を参照してください。
18.12.1.3 The Application Messages
アプリケーションメッセージ(Message
, ChatMessage
, InfoMessage
, JoinMessage
, UsersMessage
)を表現するクラスは単にプロパティとgetterとsetterメソッドだけです。たとえばChatMessage
クラスは以下のようになっています。
public class ChatMessage extends Message { private String name; private String target; private String message; /* ... Constructor, getters, and setters ... */ }
18.12.1.4 The Encoder Classes
エンコーダクラスはJava API for JSON Processingを使用してアプリケーションメッセージをJSONテキストに変換します。たとえば、ChatMessageEncoder
クラスは以下のように実装されています。
/* JSONにChatMessageをエンコードします。 * たとえば * (new ChatMessage("Peter","Duke","How are you?")) * は以下のようにエンコードされます。 * {"type":"chat","target":"Duke","message":"How are you?"} */ public class ChatMessageEncoder implements Encoder.Text<ChatMessage> { @Override public void init(EndpointConfig ec) { } @Override public void destroy() { } @Override public String encode(ChatMessage chatMessage) throws EncodeException { // Access properties in chatMessage and write JSON text... } }
Java API for JSON Processingの詳細な情報についてはChapter 19, JSON Processingを参照してください。
18.12.1.5 The Message Decoder
メッセージデコーダ(MessageDecoder
)クラスはJSONテキストをパースしてWebSocketテキストメッセージをアプリケーションメッセージに変換します。これは以下のように実装されます。
/* JSONメッセージをJoinMessageかChatMessageに変換します。 * たとえば受信メッセージが以下の場合、 * {"type":"chat","name":"Peter","target":"Duke","message":"How are you?"} * 下記のようにデコードされます。 * (new ChatMessage("Peter", "Duke", "How are you?")) */ public class MessageDecoder implements Decoder.Text<Message> { /* MapでJSONメッセージのname-valueペアを格納します。 */ private Map<String,String> messageMap; @Override public void init(EndpointConfig ec) { } @Override public void destroy() { } /* メッセージがデコード可能であれば新しいメッセージオブジェクトを生成します。*/ @Override public Message decode(String string) throws DecodeException { Message msg = null; if (willDecode(string)) { switch (messageMap.get("type")) { case "join": msg = new JoinMessage(messageMap.get("name")); break; case "chat": msg = new ChatMessage(messageMap.get("name"), messageMap.get("target"), messageMap.get("message")); } } else { throw new DecodeException(string, "[Message] Can't decode."); } return msg; } /* JSONメッセージをMAPにデコードしてこの型で要求する全フィールドを * 含むかチェックします。*/ @Override public boolean willDecode(String string) { // メッセージからJSONデータをname-valueマップに変換。 // メッセージがこの型のフィールドを持つかどうかチェック。 } }
18.12.1.6 The HTML Page
HTMLページ(index.html
)にはユーザ名のフィールドがあります。ユーザが名前を入力してJoinをクリックすると、三つのテキストエリアが利用可能になります。送信メッセージの入力欄、チャットルーム、ユーザのリスト。また、JSONテキストとして送受信されるメッセージを表示するWebSocketコンソールもあります。
このページのJavaScriptコードはWebSocket APIを使用して、エンドポイントへの接続・メッセージの送信・コールバックメソッドの指定、を行います。WebSocket APIは多くのモダンなブラウザでサポートされており、HTML5 webクライアントの開発に広く使用されています。
18.12.2 Running the websocketbot Example Application
このセクションではNetBeans IDEとコマンドラインでwebsocketbot
サンプルアプリケーションを実行する方法について説明します。
18.12.2.1 To Run the websocketbot Example Application Using NetBeans IDE
- GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
- FileメニューからOpen Projectを選択
- Open Projectダイアログボックスから以下のディレクトリに移動。
tut-install/examples/web/websocket
websocketbot
フォルダを選択。- Open Projectをクリック。
- Projectsタブで
websocketbot
プロジェクトを右クリックしてRunを選択。
このコマンドはアプリケーションをビルドしてWARファイル(websocketbot.war
)にパッケージしてtarget/
ディレクトリに配置し、サーバにデプロイし、以下のURLでwebブラウザーを開きます。
http://localhost:8080/websocketbot/
テストにはTo Test the websocketbot Example Applicationを参照してください。
18.12.2.2 To Run the websocketbot Example Application Using Maven
- GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
- ターミナルで以下に移動:
tut-install/examples/web/websocket/websocketbot/
- アプリケーションをデプロイするために以下のコマンドを実行。
mvn install
- webブラウザを開いて以下のアドレスを入力。
http://localhost:8080/websocketbot/
テストにはTo Test the websocketbot Example Applicationを参照してください。
18.12.2.3 To Test the websocketbot Example Application
1. メインページで、最初のテキストフィールドに名前を入力してエンターキーを押します。
右側のテキストエリアに接続中のユーザリストが表示され、左側のテキストエリアがチャットルームです。
2. ログインボタン下のテキストエリアにメッセージを入力します。たとえば、以下のように太字部分*4のメッセージを入力してエンターを押すとこのようなレスポンスが得られます。
[--Peter has joined the chat--] Duke: @Peter Hi there!! Peter: @Duke how are you? Duke: @Peter I'm doing great, thank you! Peter: @Duke when is your birthday? Duke: @Peter My birthday is on May 23rd. Thanks for asking!
3. 別のブラウザウィンドウのアドレスバーにURIをコピペして別の名前でチャットルームに入室します。
双方のブラウザウィンドウのユーザリストに新しい名前が表示されます。どちらのウィンドウからでもメッセージ送信ができ、メッセージが別のウィンドウではどのように表示されるかを確認できます。
4.Show WebSocket Consoleをクリックします。
JSONテキストとして送受信されるメッセージをコンソールで確認できます。
18.13 Further Information about WebSocket
Java EEにおけるWebSocketの詳細な情報については、Java API for WebSocket仕様を参照してください。
http://www.jcp.org/en/jsr/detail?id=356
関連リンク
- The Java EE 7 Tutorialのテキトー翻訳まとめ - Qiita - Java EE 7 Tutorialのうち、自分がテキトー翻訳したものの一覧