[WebSocket] [Java] WebSocketを試す

WebSocketとは

HTML5仕様を構成する要素の一つで一方向通信しかできなかった既存のHTML通信とは違って、
JavascriptのWebSocket APIとサーバサイドのWebSocket機能でクライアントとサーバ間の両方向通信を可能にする技術だ。

WebSocketの特徴

WebSocketは、通信ごとにHTTPコネクションを開く・閉じるのを繰り返すのではなく、
いったんHTTPコネクションを開いたら、コネクションを開いたままにしておいて、データをやりとりする。

コネクションが維持される間は、Ajaxのようにクライアントからサーバへの通信だけでなく、
サーバからクライアントに直接データを送ることもできる。

以下は、ChromeでWebSocketの公式サイトにあるデモを試したキャプチャである。
"Rock it with HTML5 WebSocket"という文字列を何回もSendしているのにコネクションは一つ(?encoding=text)しか発生していない。
通信の内容はFramesタブをクリックすれば、分かる。
f:id:jeongman7:20140222213222j:plain

WebSocketのAPIについて

サーバAPI

昔はサーブレットコンテナごとに違うWebSocketのAPIがあったが、
今は、JSR 356: JavaTM API for WebSocketという標準ができて、
各ベンダが自分たちのサーブレットコンテナに実装している。
標準APIがあるので、ベンダ依存のAPIは使わないこと。

WebSocketサーバプログラムはWeb(サーブレットJSP)でもStandalone(mainメソッド)でも作れる。

JavaでWebSocketサーバプログラムを実装するために以下のアノテーションとクラスを使用する。

  • @ServerEndPoint
  • @OnOpen
  • @OnMessage
  • @OnClose
  • @PathParam
  • Encoder
  • Decoder
  • 他にも色々。

クライアントAPI

WebSocketのクライアントプログラムはどの言語でも作れるが、ここではJavascriptを取り上げる。
Javascriptでは以下のように非常に単純で分かりやすいAPIが存在する。

  • WebSocket(url)
  • WebSocket.send
  • WebSocket.onopen
  • WebSocket.onmessage
  • WebSocket.onerror
  • WebSocket.onclose

サンプル作成

必要なもの

  • WebSocketをサポートするサブレットコンテナ(Tomcat8, GlassFish、Jetty等)
  • サーバプログラム(JavaJSP)
  • クライアントプログラム(Javascript)

サンプル作成スタート

時間を節約するため、サンプルは早く作りたい。
今回はNetBeansを利用してサンプル作ろう。
Netbeansをダウンロード・インストールし、立ち上げて新規プロジェクトを作成する。
カテゴリでJava Web→Webアプリケーションを選択し、次へ、プロジェクト名を入力し、次へ、
サーバーと設定画面で終了ボタンを押すと、プロジェクトが作成される。

さあ、コーディングをしてみよう。

サーバプログラム

送ったメッセージをそのまま返すだけである。

package dukelab.websocket.demo;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

/**
 * WebSocketデモ。
 *
 * @author DukeLab
 */
// Webソケットのサーバ側クラスであること表すアノテーション。
// 引数(wsdemo)はクライアントから接続時、使われるURIを表す。
@ServerEndpoint(value = "/wsdemo")
public class WebSocketDemo {

    @OnMessage
    public String onMessage(String text) {
        return text;
    }
    
}

WebSocketのサーバプログラムは@ServerEndpointアノテーションをつけることで始まる。
@ServerEndpointの引数valueにはクライアントから接続時、使われるURIを指定する。
URIは/で始まらなければならない
例えば、ウェブアプリケーションがlocalhost:8080で動いていてコンテキストパスがtestなら、
上記のサーバプログラムへのURLはws://localhost:8080/test/wsdemoになる。

ここでは指定していないが、@ServerEndpointアノテーションへの引数に、
メッセージ変換のためのencoder、decoderも指定できる。
本格的なアプリケーションを作る時に、encoderとdecoderが必要になりそうだ

それから、@OnMessageアノテーションをメッセージを受信するメソッドに指定する。
メソッドの引数textがクライアントから受信したデータということだ。
onMessageメソッド内で色々処理を行って、その応答をreturnすればいい。

@OnMessageアノテーション以外に@OnOpen、@OnCloseもある。
@Onxxxアノテーションがつくメソッド(例 : onMessage(String Text))の引数や応答を返す方法は上記以外にもある。
リクエストが来た時だけでなく、サーバから自発的に送ることもできる。
それらは実装の状況に応じてドキュメントを参照すればいいだろう。

ここでは至極簡単なサンプルサーバプログラムを作った。
上記のクラスだけだ。
web.xmlとかサーブレットは要らない。
本当に便利だ。

クライアントプログラム

サンプルを実行したブラウザはChrome 33.0.1750.117である。

<!DOCTYPE html>
<html>
    <head>
        <title>WebSocketDemo Client</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <script type="text/javascript">
            WebSocketDemo = {};
            (function(d) {
                function $(query) {
                    return document.querySelector(query);
                }
                function printMessage(msg) {
                    $("#msgbox").innerHTML += "<div>" + msg + "</div>";
                }
                d.connect = function() {
                    var ws = new WebSocket("ws://localhost:8080/WebApplication3/wsdemo");
                    ws.onmessage = function(event) {
                        printMessage("Server : " + event.data);
                    };
                    d.webSocket = ws;
                    $("#connect").disabled = true;
                    $("#send").disabled = false;
                };
                d.send = function() {
                    var msg = $("#msg").value;
                    d.webSocket.send(msg);
                    printMessage("Client : " + msg);
                };
            }) (WebSocketDemo);
        </script>
        <form action="javascript:void(0);">
            <input type="text" id="msg" size="20"> 
            <input type="button" id="connect" value="Connect" onclick="WebSocketDemo.connect();">
            <input type="button" id="send" value="Send" onclick="WebSocketDemo.send();" disabled>
        </form>
        <div id="msgbox" style="border-style: solid;width: 500px;height: 400px"></div>
    </body>
</html>

Connectボタンを押すと、WebSocketオブジェクトを生成する。
WebSocketのコンストラクタにはスキームがws://又はwss://(SSLの場合)で始まるURLを指定する。
後、サーバからメッセージを受信すると、onmessageハンドラーが呼ばれる。
引数のオブジェクトのdataプロパティからメッセージの取得ができる。

Sendボタンを押すと、WebSocketオブジェクトのsendメソッドを呼び出してメッセージをサーバに送信する。
サーバより実装が簡単だ。

注意点は、IEの場合、WebSocketから使えるのはIE10からだということだ。
IE9まではWebSocketが使えない。

実行結果

NetBeansで実行ボタンを押せばいい。NetBeans、結構使えるじゃん!
f:id:jeongman7:20140223212045j:plain

さらに進化したサンプル

上記までは、内部的にやり取りされるのが変わっただけで、既存のAjaxでも同じ機能のアプリケーションは作ることができる。
ここで、サーバから自発的にクライアントにメッセージを送る機能をつけてみよう。いわゆるPUSH型通信。
これはAjaxではできないのだ。
setIntervalで周期的にサーバに見に行くしかないのだ。いわゆるPULL型通信。
Ajax以外にCometというのもあるが、これはコネクション確立後、ただ、Pendingして待つだけだ。
実際の通信が送られるまで待つので、通信量を減らすことはできるが、サーバから応答が返ってくると、
また次のHTTPコネクションを確立しなければならない。
ずっと通信を確立しているWebSocketはAjaxとCometとは歴然と違うのだ。

このさらに進化したサンプルでは、制御用JSPを一個作って、そのJSPで何かを入力すると、
サーバから全てのクライアントにメッセージが送信される機能をつける。

以下は上記のWebSocketDemoクラスに機能を追加したものだ。

package dukelab.websocket.demo;

import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

/**
 * WebSocketデモ。
 *
 * @author dukelab
 */
@ServerEndpoint(value = "/wsdemo")
public class WebSocketDemo {

    private static Set<Session> ses = new CopyOnWriteArraySet<>();

    @OnOpen
    public void onOpen(Session session) {
        System.out.println("onOpen : " + session);
        ses.add(session);
    }
   
    @OnMessage
    public String onMessage(String text) {
        return "echo => " + text;
    }
    
    @OnClose
    public void onClose(Session session) {
        System.out.println("onClose : " + session);
        ses.remove(session);
    }
    
    public static void sendMessage(String msg) {
        for (Session ses : ses) {
            ses.getAsyncRemote().sendText(msg);
        }
    }
    
}

要は、接続時(onOpen)にセッションをスレッドセーフなコレクションに入れておいて、
そのセッションに対して操作をすればいい。
切断時(onClose)にセッションをスレッドセーフなコレクションから削除する処理は忘れないように

以下は制御用JSP(control.jsp)である。

<%@page import="dukelab.websocket.demo.WebSocketDemo"%>
<%@page contentType="text/html" pageEncoding="utf-8"%>
<!DOCTYPE html>
<%
    boolean isPOST = request.getMethod().toLowerCase().equals("post");
    if (isPOST) {
        WebSocketDemo.sendMessage(request.getParameter("msg"));
    }
%>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>WebSocketDemo Control</title>
    </head>
    <body>
        <%= isPOST ? "Message was sent." : "" %>
        <form action="" method="post">
            <input type="text" name="msg">
            <input type="submit" name="submit" value="Send">
        </form>
    </body>
</html>

以下の画面で、左が制御用JSPで、右がエコデモである。
制御用JSPでメッセージを入力し、Sendボタンを押すと、全てのクライアントにメッセージが送信される
これは、なかなか面白い。
f:id:jeongman7:20140223212109j:plain

まとめ

WebSocketに関する概念やAPI、そしてサンプルを作ってみた。
APIが分かりやすいので、サーバプログラム・クライアントプログラムの作成も簡単にできる。
JavaやCで使ってきたTCP/IPソケットに比べると、遊び水準だ。
IEのサポートが乏しいのが残念なところだが、IE10以上が広がっていくと、問題ないだろう。
いまだIE6のユーザがあって、それがいつになるか分からないが...
これから色々なアイデアのアプリケーションの実現に役立ちそうだ。