Pocket

皆様こんにちは。t.i.でございます。

タイトルのWebSocketとは、HTML5の目玉である最近考えられた通信規格の一つです。
(厳密言うと、今はもうHTML5から切り離された一つの規格として検討が進んでいます)
今回の記事では、最初に既存の通信規格HTTPとWebSocketの違いを簡単に説明した後、JavaScriptとJava(Tomcat)によるサンプルプログラムをご紹介致します。
(サンプルプログラムは、Eclipseプロジェクト一式をzipにまとめたものが用意してありますので、直ぐに自分の環境で試すことができます)

その1 HTTPとWebSocketの違い

HTTPとWebSocketの大きな違いは『アプリケーション的に繋ぎっぱなし(クライアントがサーバからの情報を即座に受け取れる)』か否かと、双方向データ通信が可能か否かです。
この『繋ぎっぱなし』についてですが、ネットワーク的・アプリケーション的なものがありますので、正しく理解するために簡単にご説明します。

ネットワーク的に繋ぎっぱなし

HTTPでは「通信路作成 → 要求(ファイルください) → 応答(どうぞ) → 通信路削除」を何度も繰り返していました。
ただし通信路作成はコストの高い作業なので、後に一つの通信路を使いまわす方法が出て来ました。

図1. 『ネットワーク的に繋ぎっぱなし』の比較
図1. 『ネットワーク的に繋ぎっぱなし』の比較

右が『ネットワーク的に繋ぎっぱなし』です。
具体的には「HTTP Keep-Alive」を使うことで実現していて、ほぼ全てのブラウザ・サーバが対応しています。
メリットは、小さい要求/応答が何度も発生する場合の処理が早くなることです。

アプリケーション的に繋ぎっぱなし

こちらが今回のメインです。
わかりやすくするために、チャットアプリケーション(複数人が同時におしゃべり)を例として考えてみましょう。
ここで言う『アプリケーション的に繋ぎっぱなし』は、『他の人の発言がリアルタイムで分かる』をイメージしています。

図2. 『アプリケーション的に繋ぎっぱなし』の比較
図2. 『アプリケーション的に繋ぎっぱなし』の比較

右が『アプリケーション的に繋ぎっぱなし』です。
一般的に広く使われているAjaxでは、左のように定期的に発言があったかを確認します。
実際にはこの確認間隔を非常に短くすることで、ほぼリアルタイムで発言有無を知ることができます。
Cometと呼ばれる右の手法では、必要な時まで応答を返さずにだんまりを決め込みます。
発言があった時に初めて応答を返しますので、クライアントは発言があったことをリアルタイムで知ることができます。

そして、今回紹介するWebSocketは以下のような形になります。

図3. WebSocketの例
図3. WebSocketの例

応答を返した後は、「いつでも即座に発言(データ)を投げられるし受け取れる」という状態になります。
即座に変更(発言があったこと)を知ることができるので『アプリケーション的に繋ぎっぱなし』ということができます。
このWebSocketが前述のCometと特に異なっているのは以下2点です。

  • 応答を返した後は、要求/応答とは全く異なる形でデータをやり取りする
  • クライアントからサーバへ、サーバからクライアントへという双方向のやり取りが可能

WebSocketは全てのブラウザが対応しているわけではありませんが、主要な最新ブラウザであればほぼ対応しています(対応状況はWebSocketのWikiが詳しいです)。

その2 JavaScriptとJava(Tomcat)によるサンプル

それではサンプルアプリケーションを見てみましょう。
よく見かける多人数チャットを取り上げてみました。
ただしサーバ側は以下2つの理由よりNode.jsではなくJava(Tomcat)を使用しています。
■理由1. サーバアプリはJavaScriptよりJavaで作ることが多い
サーバサイドJavaScriptというものも存在してはいますが、正直Node.js以外は未経験という人が多いのではないでしょうか。
■理由2. Node.jsのプロセス管理方法が確立していない
「やったサンプルが動いた動いた」レベルであれば、コマンドラインから
% node foo.js
でも問題ありませんが、業務利用なども視野に入れるとTomcat管理の方が望ましいと思います(WebLogicやJBossの方がより良いかもしれませんがサンプルということで……)。

それではサンプルを見てみましょう。

クライアント側(JavaScript)

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>WebSocket Sample</title>
<script src="http://code.jquery.com/jquery-latest.min.js"></script>
<script>
$(function(){
	// WebSocket作成
	var ws = new WebSocket("ws://localhost:8080/WebSocketSample/echo");

	// WebSocket open時のイベントハンドラ登録
	ws.onopen = function(){
		$("#log").prepend("&lt;onopen&gt; " + "<br/>");
	}

	// WebSocket message受信時のイベントハンドラ登録
	ws.onmessage = function(message){
		$("#log").prepend(message.data + "<br/>");
	}

	// WebSocket error時のイベントハンドラ登録
	ws.onerror = function(){
		$("#log").prepend("&lt;onerror&gt; " + "<br/>");
	}

	// WebSocket close時のイベントハンドラ登録
	ws.onclose = function(){
		$("#log").prepend("&lt;onclose&gt; " + "<br/>");
	}

	// Windowが閉じられた(例:ブラウザを閉じた)時のイベントを設定
	$(window).unload(function() {
		ws.onclose(); // WebSocket close
	})

	// キー入力時のイベントを設定
	$("#message").keyup(function(e){
		ws.send($("#message").val()); // WebSocketを使いサーバにメッセージを送信
	});

})
</script>
</head>
<body><input type="text" id="message" /><div id="log" /></body>
</html>

9行目のnew WebSocket(“エンドポイント”)でWebSocketを作成します。
エンドポイントはws(httpの代わり)またはwss(httpsの代わり)で始めます。
そしてWebSocketにonopen, onmessage, onerror, oncloseのイベントハンドラをセットします。
それぞれ直感的にわかるイベント名だと思います。
詳細はThe WebSocket APIを御覧ください(日本語訳をしてくださっている方のページ)
一般的な流れは open → message → message → … → message → close となります。
メッセージ送信の際は、38行目のようにWebSocketに対しsend(“送信メッセージ”)をします。

サーバ側(Java/Tomcat)

それではサーバ側です。
サーバ側は、Java(Tomcat7.0.39)で作成しています(その他のバージョンだとメソッドが異なり動かない可能性があります)。

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.catalina.websocket.WsOutbound;

@WebServlet(value = { "/echo" })
public class EchoServlet extends WebSocketServlet {

	/**
	 * serialVersionUID
	 */
	private static final long serialVersionUID = 6946416208261279049L;

	/**
	 * スレッドセーフなIDカウンタ
	 */
	private static final AtomicInteger idCounter = new AtomicInteger(1);

	/**
	 * スレッドセーフなEchoInboundのSet
	 */
	private static final Set echoInboundSet = new CopyOnWriteArraySet();

	@Override
	protected StreamInbound createWebSocketInbound(String subProtocol,
			HttpServletRequest request) {
		return new EchoInbound(idCounter.getAndIncrement());
	}

	/**
	 * 全体にEchoするWebSocket
	 */
	private class EchoInbound extends MessageInbound {

		private String id;

		public EchoInbound(int id) {
			this.id = Integer.toString(id);
		}

		@Override
		protected void onOpen(WsOutbound outbound) {
			System.out.println("onOpen");
			echoInboundSet.add(this);

			try {
				outbound.writeTextMessage(CharBuffer.wrap("YOU ARE " + id));
				talk("JOIN " + id);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		@Override
		protected void onClose(int status) {
			System.out.println("onClose(status = " + status + ")");
			echoInboundSet.remove(this);
			try {
				talk("PART " + id);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		@Override
		protected void onBinaryMessage(ByteBuffer message) throws IOException {
			System.out.println("onBinaryMessage");
		}

		@Override
		protected void onTextMessage(CharBuffer message) throws IOException {
			talk(id + "> " + message);
		}

		private void talk(String message) throws IOException{
			CharBuffer charBuffer = CharBuffer.wrap(message);
			for (MessageInbound echoInbound : echoInboundSet) {
				echoInbound.getWsOutbound().writeTextMessage(charBuffer);
				charBuffer.position(0);
			}
		}
	}
}

ポイントは以下5つです。

  • 16行目:@WebServlet(value = { “/echo” }) を書く(web.xmlの記述が不要)
  • 17行目:WebSocketServletを継承したサーブレットを作成する
  • 43行目:MessageInbound(StreamInboundでも可)を継承したクラスを作成する(メインとなるクラスです)
  • 81行目:MessageInbound#onTextMessageに、クライアントからテキストを受け取った際の処理を書く
  • 57行目、88行目:クライアントにテキストを渡す時は、outbound(#getWsOutboundで取得可能)に対し#writeTextMessageを実行する

今回は多人数チャットで接続管理が必要なため#onOpenや#onClose、echoInboundSetがソースにありますが、接続管理が不要であればこれらは不要ですので更に簡単になります。
プログラムの@Override部分の詳細はTomcat公式ドキュメントを御覧下さい。
※Tomcat公式ドキュメントの通り、TomcatのWebSocketはまだ開発段階ですのでご注意下さい

実行結果

Firefox20で実行した結果です。
相手が一文字入力する度に文字が表示されます。
図4. Chromeの実行結果図4. Firefoxでの実行結果

おわりに

今回はWebSocketについて、既存の通信規格HTTPとの違いとJavaサンプルプログラムをご紹介しました。
紹介したサンプルプログラムはEclipseでインポート可能な形式でUPしておきまので、是非ともWebSocketをお試し下さい。
WebSocketSample.zip
※Eclipseでウィンドウ → 設定 → サーバ → ランタイム環境 でTomcat7.0.39を追加し、WebSocketSampleプロジェクト → プロパティ → Javaのビルド・パス → ライブラリー → ライブラリーの追加 → サーバ・ランタイム で先に追加したTomcat7.0.39を設定

WebSocketについて細かいところまで知りたいという方は、本家本元の RFC 6455 The WebSocket Protocolを読むといいと思います(日本語訳をしてくださっている方のページ)。

それでは次回をお楽しみに。