いまさらHTML5 （WebSocket編） 

## はじめに

HTML5のWebSocketを使ってみます。

## チャット

WebSocketオブジェクト生成時に接続に行きます。
パラメータは、`ws://`から始まる、URLです。

WebSocketオブジェクト生成後、イベントハンドラを登録します。

|名称|説明|
|:--|:--|
|onopen|接続イベント|
|onclose|切断イベント|
|onmessage|メッセージ受信イベント|
|onerror|エラーイベント|

WebSocketオブジェクトのメソッド

|メソッド|説明|
|:--|:--|
|send|メッセージ送信|
|close|切断|

```html:websocket01.html
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>WebSocket Sample</title>
    <script type="text/javascript" src="./jquery.js"></script>
    <script type="text/javascript">
        // 接続先URI
        var uri = "ws://" + location.host + "/sample/chat";

        // WebSocketオブジェクト
        var webSocket = null;

        // 初期処理
        function init() {
            // ボタン押下イベント設定
            $("[data-name='message']").keypress(press);
            // 接続
            open();
        }

        // 接続
        function open() {
            if (webSocket == null) {
                // WebSocket の初期化
                webSocket = new WebSocket(uri);
                // イベントハンドラの設定
                webSocket.onopen = onOpen;
                webSocket.onmessage = onMessage;
                webSocket.onclose = onClose;
                webSocket.onerror = onError;
            }
        }

        // 接続イベント
        function onOpen(event) {
            chat("接続しました。");
        }

        // メッセージ受信イベント
        function onMessage(event) {
            if (event && event.data) {
                chat(event.data);
            }
        }

        // エラーイベント
        function onError(event) {
            //chat("エラーが発生しました。");
        }

        // 切断イベント
        function onClose(event) {
            chat("切断しました。3秒後に再接続します。(" + event.code + ")");
            webSocket = null;
            setTimeout("open()", 3000);
        }

        // キー押下時
        function press(event) {
            // キーがEnterか判定
            if (event && event.which == 13) {
                // メッセージ取得
                var message = $("[data-name='message']").val();
                // 存在チェック
                if (message && webSocket) {
                    // メッセージ送信
                    webSocket.send("" + message);
                    // メッセージ初期化
                    $("[data-name='message']").val("");
                }
            }
        }

        // チャットに表示
        function chat(message) {
            // 100件まで残す
            var chats = $("[data-name='chat']").find("div");
            while (chats.length >= 100) {
                chats = chats.last().remove();
            }
            // メッセージ表示
            var msgtag = $("<div>").text(message);
            $("[data-name='chat']").prepend(msgtag);
        }

        // 初期処理登録
        $(init);
    </script>
</head>
<body>
    <input type="text" data-name="message" size="100" />
    <hr />
    <div data-name="chat"></div>
</body>
</html>
```

サーバ側は、受信したメッセージをオンラインのWebSocketに送信するだけの簡単なものです。

```java:WebSocketChat.java
package sample;

import java.util.Set;

import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/chat")
public class WebSocketChat {

	@OnOpen
	public void onOpen(Session session, EndpointConfig config) {
		String id = session.getId();
		echo("(" + id + ")が入室しました。", session);
	}

	@OnMessage
	public void onMessage(String message, Session session) {
		String id = session.getId();
		echo("(" + id + ") " + message, session);
	}

	@OnClose
	public void onClose(Session session, CloseReason reason) {
		String id = session.getId();
		echo("(" + id + ")が退出しました。", session);
	}

	@OnError
	public void onError(Session session, Throwable throwable) {
		throwable.printStackTrace();
	}
	
	/**
	 * メッセージエコー
	 * 
	 * @param message メッセージ
	 * @param session セッション
	 */
	private void echo(String message, Session session) {
		System.out.println(message);
		// 開いているセッション取得
		Set<Session> sessions = session.getOpenSessions();
		// セッション数分ループ
		for (Session tmp : sessions) {
			try {
				// メッセージ送信
				tmp.getBasicRemote().sendText(message);
			} catch (Exception e) {
				// 例外の場合
				e.printStackTrace();
			}
		}
	}
}
```

## JSONデータ

テキスト送信出来るので、そのままJsonデータを送受信してみます。
`jquery-ui`の`draggable`を利用して、ボールを動かすようなサンプルです。
１ブラウザだけでやってると意味ありませんが、複数ブラウザでやると全部同じ動きをします。

```html:websocket02.html
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>WebSocket Sample</title>
    <script type="text/javascript" src="./jquery.js"></script>
    <script type="text/javascript" src="./jquery-ui.js"></script>
    <script type="text/javascript">
        // 接続先URI
        var uri = "ws://" + location.host + "/sample/json";

        // WebSocketオブジェクト
        var webSocket = null;

        // ボール
        var balls = null;

        // ボールの場所
        var ballpos = { t: 0, l: 0 };

        // 初期処理
        function init() {
            // ボール生成
            ball = $("<div>").width(20).height(20)
                .css("background-color", "#f00").css("border-radius", "10px");
            // ボールの可動範囲設定
            ball.draggable({ containment: "[data-name='square']" });
            // ボールのイベント設定
            ball.on("drag", dragball);
            // ボール描写
            $("[data-name='square']").append(ball);
            // ボール移動（早さ０）
            move(0);
            // 接続
            open();
        }

        // ボール移動
        function move(spped) {
            // 各ボール移動
            ball.stop();
            ball.animate({ top: ballpos.t, left: ballpos.l }, spped);
        }

        // ボール移動イベント
        function dragball(event, ui) {
            // ボール位置設定
            ballpos.t = ui.position.top.toFixed(0);
            ballpos.l = ui.position.left.toFixed(0);
            // ボール位置設定
            var msg = JSON.stringify(ballpos);
            if (webSocket) {
                webSocket.send(msg);
            }
            $("[data-name='status']").text(msg);
        }

        // 接続
        function open() {
            if (webSocket == null) {
                // WebSocket の初期化
                webSocket = new WebSocket(uri);
                // イベントハンドラの設定
                webSocket.onopen = onOpen;
                webSocket.onmessage = onMessage;
                webSocket.onclose = onClose;
                webSocket.onerror = onError;
            }
        }

        // 接続イベント
        function onOpen(event) {
            $("[data-name='status']").text("OPEN");
        }

        // メッセージ受信イベント
        function onMessage(event) {
            if (event && event.data) {
                var pos = JSON.parse(event.data);
                ballpos = pos;
                move(5);
                $("[data-name='status']").text(event.data);
            }
        }

        // エラーイベント
        function onError(event) {
            $("[data-name='status']").text("ERROR");
        }

        // 切断イベント
        function onClose(event) {
            $("[data-name='status']").text("CLOSE");
            webSocket = null;
            setTimeout("open()", 3000);
        }

        // 初期処理登録
        $(init);
    </script>
</head>
<body>
    <div data-name="status">&nbsp;</div>
    <div data-name="square" style="width: 400px; height: 300px; background-color: gray"></div>
</body>
</html>
```

サーバ側は、`static`で保持しているJSONデータを接続時`onOpen`には、接続した相手へ、
データ受信時`onMessage`は、データ送信者以外に配布しているだけです。
（データ形式が複雑になると、JSONデータを解析しなければならないと思います。例えば、ボールが４つになった時など）

```java:WebSocketJson.java
package sample;

import java.util.Set;

import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/json")
public class WebSocketJson {
	
	private static String json = "";

	@OnOpen
	public void onOpen(Session session, EndpointConfig config) {
		if (json.length() > 0) {
			try {
				session.getAsyncRemote().sendText(json);
			} catch (Exception e) {
				// 例外の場合
				System.out.println("" + e.getMessage());
			}
		}
	}

	@OnMessage
	public void onMessage(String message, Session session) {
		String id = session.getId();
		synchronized (json) {
			json = message;
			// 開いているセッション取得
			Set<Session> sessions = session.getOpenSessions();
			// セッション数分ループ
			for (Session tmp : sessions) {
				if (!tmp.getId().equals(id)) {
					try {
						// メッセージ送信
						tmp.getAsyncRemote().sendText(json);
					} catch (Exception e) {
						// 例外の場合
						System.out.println("" + e.getMessage());
					}
				}
			}
		}
	}

	@OnClose
	public void onClose(Session session, CloseReason reason) {
		// 何もしません。
	}

	@OnError
	public void onError(Session session, Throwable throwable) {
		// 何もしません。
	}
}
```

## ファイル

WebSocketオブジェクトは、`send`メソッドでBlobオブジェクトを送信できます。
というわけで、ファイルをWebSocketで送信してみます。

`input`タグの`value`属性が変更になったイベントを契機に、Fileオブジェクトを取得します。
WebSocketを接続し、Fileオブジェクトを送信します。

```html:websocket03.html
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>WebSocket Sample</title>
    <script type="text/javascript" src="./jquery.js"></script>
    <script type="text/javascript">
        // 接続先URI
        var uri = "ws://" + location.host + "/sample/blob";

        // WebSocketオブジェクト
        var webSocket = null;

        // ファイル
        var file = null;

        // 初期処理
        function init() {
            // ファイル選択時
            $("[data-name='sendfile']").change(filechange);
        }

        // ファイル選択時
        function filechange() {
            // ファイル存在チェック
            if (file == null) {
                // ファイル取得
                var files = $(this).get(0).files;
                if (files.length > 0) {
                    file = files[0];
                    // 接続
                    open();
                }
            }
        }

        // 接続
        function open() {
            if (webSocket == null) {
                // WebSocket の初期化
                webSocket = new WebSocket(uri);
                // イベントハンドラの設定
                webSocket.onopen = onOpen;
                webSocket.onmessage = onMessage;
                webSocket.onclose = onClose;
                webSocket.onerror = onError;
            }
        }

        // 接続イベント
        function onOpen(event) {
            $("[data-name='status']").text("OPEN");
            try {
                // WebSoket存在判定
                if (webSocket != null) {
                    // ファイル存在判定
                    if (file != null) {
                        // ファイル送信
                        webSocket.send(file);
                        file = null;
                    }
                    // 切断
                    webSocket.close();
                    webSocket = null;
                }
            } catch (e) {
                $("[data-name='status']").text("Error:" + e.message);

            }
        }

        // メッセージ受信イベント
        function onMessage(event) {
            if (event && event.data) {
                $("[data-name='status']").text("MESSAGE:" + event.data);
            }
        }

        // エラーイベント
        function onError(event) {
            $("[data-name='status']").text("ERROR");
        }

        // 切断イベント
        function onClose(event) {
            $("[data-name='status']").text("CLOSE");
            webSocket = null;
        }

        // 初期処理登録
        $(init);
    </script>
</head>
<body>
    <div data-name="status">&nbsp;</div>
    <input type="file" data-name="sendfile" />
</body>
</html>
```

サーバ側は、`onMessage(byte[] bs, boolean last, Session session)`メソッドでデータを受信します。
ブラウザ側でデータを分割送信している為、第二引数の`last`変数が`true`になるまで、データを溜め込みます。
溜め込んだデータはとりあえず、文字列として出力します。

```java:WebSocketBlob.java
package sample;

import java.io.ByteArrayOutputStream;

import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/blob")
public class WebSocketBlob {
	
	// バッファ
	private ByteArrayOutputStream baos = null;

	@OnOpen
	public void onOpen(Session session, EndpointConfig config) {
		// 何もしません。
	}

	@OnMessage
	public void onMessage(byte[] bs, boolean last, Session session) {
		try {
			System.out.println(session.getId() + "/" + bs.length + "/" + last);
			// バッファ存在チェック
			if (baos == null) {
				// バッファ生成
				baos = new ByteArrayOutputStream();
			}
			// 書き込み
			baos.write(bs);
			baos.flush();
			// 最後であるか判定
			if (last) {
				// 最後なので、byte配列化
				byte[] data = baos.toByteArray();
				// とりあえず、文字列にして出力
				System.out.println(new String(data));
				// バッファ後処理
				baos.close();
				baos = null;
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@OnClose
	public void onClose(Session session, CloseReason reason) {
		// 何もしません。
	}

	@OnError
	public void onError(Session session, Throwable throwable) {
		// 何もしません。
	}
}
```

## 感想

サーバ側からプッシュ出来る仕組みは素晴らしいと思いつつも、
使う場所は限定されるんだろうと思いました。