tomcat7のWebSocketでcanvasの描画を共有する

2013.03.04

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。tomcat7でWebSocketを使ってみました。
今回はcanvasに書いた情報を共有するといことをやりたいと思います。
使用する環境はpleiades3.7.2、Java7になります。またEclipseのtomcatホームの設定はしてあるものとして進めます。

Eclipseでプロジェクトを作成します

「ファイル」>「新規」>「その他」>「Java」からTomcatプロジェクトを選択し、プロジェクトを作成します。
(今回はサブディレクトリにWebContentsを設定して作成しました)
srcフォルダ配下にws.appパッケージを作成します。ここにサーバ側のソースを格納します。
クライアント側のソースはWebContents直下にhtmlファイルを作成します。
以下フォルダ構成になります。
プロジェクト構成

ビルドパスを追加します

tomcatでwebsocketを実装するのにはWebSocketServletを継承してサーブレットを実装する必要があります。
それにはcatalina.jarとtomcat-coyote.jarが必要なのでビルドバスに追加をします。
プロジェクトを右クリックして「プロパティ」から「Javaのビルド・バス」を選択します。ライブラリタブから「変数の追加」ボタンをクリックし、「TOMCAT_HOME」を選択します。次に拡張ボタンをクリックし、「lib」フォルダからcatalina.jarを選択します。同様にtomcat-coyote.jarも追加します。

ビルドバス

 

サーブレットを作成します

ws.app配下にDrawServletクラスを作成します。

package ws.app;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.List;
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(urlPatterns={"/DrawServlet"})
public class DrawServlet extends WebSocketServlet{
    private static final long serialVersionUID = 1L;
    private static List<DrawMessageInbound> messageList = new ArrayList<DrawMessageInbound>();

    private class DrawMessageInbound extends MessageInbound{
        WsOutbound drawOutbound;

        // 接続時の処理
        @Override
        public void onOpen(WsOutbound outbound){
            System.out.println("open");
            this.drawOutbound = outbound;
            messageList.add(this);
        }

        // 接続解除時の処理
        @Override
        public void onClose(int status){
            System.out.println("close");
            messageList.remove(this);
        }

        // メッセージ受信時の処理
        @Override
        public void onTextMessage(CharBuffer message) throws IOException{
            System.out.println("message"+ message);
            for(DrawMessageInbound in: messageList){
                CharBuffer buffer = CharBuffer.wrap(message);
                in.drawOutbound.writeTextMessage(buffer);
                in.drawOutbound.flush();
            }
        }

        // メッセージ受信時の処理
        @Override
        public void onBinaryMessage(ByteBuffer bb) throws IOException{
        }
    }

    @Override
    public StreamInbound createWebSocketInbound(String arg0, HttpServletRequest arg1) {
        return new DrawMessageInbound();
    }
}

ソースについて簡単に説明します。
冒頭にも書きましたが、WebSocketServletを継承する必要があります。アノテーションはURLマッピングのためのものになります。Servlet3.0ではHttpServletクラスを継承していれば、@WebServletアノテーションを書くだけでweb.xmlを記載しなくてもサーブレットを呼び出すことができます。WebSocketServletクラスはHttpServletを継承してているクラスなので記述しておけばweb.xmlがなくても呼び出されます。

@WebServlet(urlPatterns={"/DrawServlet"})
public class DrawServlet extends WebSocketServlet{
}

WebSocketServletクラスを継承するとcreateWebSocketInboundを実装する必要があります。クライアントからの要求があるとこのメソッドが呼び出されます。またWebSoket通信はStreamInboundクラスを返却する必要があります。ここではMessageInboundクラス(StreamInboundを継承したクラス)を継承したDrawMessageInboundクラスを生成して返却しています。

    @Override
    public StreamInbound createWebSocketInbound(String arg0, HttpServletRequest arg1) {
        return new DrawMessageInbound();
    }

接続時の処理を記述します。ここでは自身の内部クラスをリストに追加しています。

        // 接続時の処理
        @Override
        public void onOpen(WsOutbound outbound){
            System.out.println("open");
            this.drawOutbound = outbound;
            messageList.add(this);
        }

接続解除時の処理を記述します。接続時に追加した自身をリストから削除します。

        @Override
        public void onClose(int status){
            System.out.println("close");
            messageList.remove(this);
        }

メッセージ受信時の処理を記述します。ここではクライアントから送信されたメッセージ(マウス座標の文字列)を接続された全てに対してメッセージを送信しています。

        // メッセージ受信時の処理
        @Override
        public void onTextMessage(CharBuffer message) throws IOException{
            System.out.println("message"+ message);
            for(DrawMessageInbound in: messageList){
                CharBuffer buffer = CharBuffer.wrap(message);
                in.drawOutbound.writeTextMessage(buffer);
                in.drawOutbound.flush();
            }
        }

バイナリメッセージを受信した時の処理を記述します。今回は文字列のみを取り扱うので何も実装しません。

        // メッセージ受信時の処理
        @Override
        public void onBinaryMessage(ByteBuffer bb) throws IOException{
        }

HTMLファイルの作成

WebContentsフォルダ直下にindex.htmlを作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>canvas</title>
<script>
    var ws = new WebSocket("ws://localhost:8080/websocket/DrawServlet");
    ws.onopen = function(){
        // 接続時の処理
    };
    var isMouseDown = false;
    var canvas;
    var context;
    window.onload = function() {
        canvas = document.getElementById("canvas1");
        context = canvas.getContext('2d');
        canvas.addEventListener("mousemove", function(event){
            mouseX = event.pageX;
            mouseY = event.pageY;
            if(isMouseDown){
                draw(mouseX, mouseY);
                ws.send(mouseX + "," + mouseY);
            }
        });
        canvas.addEventListener("mousedown", function(){
            isMouseDown = true;
        });
        canvas.addEventListener("mouseup", function(){
            isMouseDown = false;
        });
    }

    // 描画処理
    function draw(x, y){
        context.beginPath();
        context.fillStyle = "#0099ff";
        context.arc(x, y, 10, 0, Math.PI*2, false);
        context.fill();
    }

    ws.onmessage = function(message){
        // 受信時の処理
        var value = message.data.split(",");
        draw(value[0], value[1]);

    };

    // 接続解除
    function closeConnect(){
        ws.close();
    }

</script>
</head>
<body>
    <div>
        <canvas id="canvas1" width="500" height="400" style="background-color:#999999;">
        </canvas>
    </div>
    <input type="button" value="接続解除" onclick="closeConnect()">

</body>
</html>

クライアント側のソースについて説明をします。
サーバに対して接続を行っています。

var ws = new WebSocket("ws://localhost:8080/websocket/DrawServlet");

ページを開いた時の初期処理を記述しています。ここではcanvasを取得してmousemove、mousedown、mouseupのイベントを登録しています。

    window.onload = function() {
        canvas = document.getElementById("canvas1");
        context = canvas.getContext('2d');
        canvas.addEventListener("mousemove", function(event){
            mouseX = event.pageX;
            mouseY = event.pageY;
            if(isMouseDown){
                draw(mouseX, mouseY);
                ws.send(mouseX + "," + mouseY);
            }
        });
        canvas.addEventListener("mousedown", function(){
            isMouseDown = true;
        });
        canvas.addEventListener("mouseup", function(){
            isMouseDown = false;
        });
    }

mousemoveイベント内ではマウスがクリックされた状態で、マウスが動いた場合にdrawメソッドを呼び出し、線を引きサーバに対してカンマ区切りでマウスのX、Y座標を送信しています。

            if(isMouseDown){
                draw(mouseX, mouseY);
                ws.send(mouseX + "," + mouseY);
            }

サーバからの受信時に呼び出されます。受信メッセージをカンマで分割しX、Y座標を取得し描画メソッドを呼び出しています。

    ws.onmessage = function(message){
        // 受信時の処理
        var value = message.data.split(",");
        draw(value[0], value[1]);

    };

サーバとの接続を解除します。

    function closeConnect(){
        ws.close();
    }

動かしてみましょう

http://localhost:8080/websocketにブラウザからアクセスしてみます。
ブラウザ複数起動させます。canvasに線を引くと別ブラウザにも線が引かれるのが分かります。ローカル環境だとスムーズに描画されるかと思います。 接続解除を押すとサーバとの接続が解除されるので描画が共有されなくなります。

最後に

tomcatを使って簡単にWebSocket通信が出来たかと思います。ただしWebSocketの仕様については仕様が確定していませんので、今後変わる可能性があるみたいです。
Java7EEではWebSocket APIが含まれる予定なので、そうすれば 仕様も確定するのかなと思います。