Vert.x で WebSocket

Vert.x 1.3.1 Final で単純な WebSocket 処理を実装してみました。

単純な JSON データを送受信するチャットサーバーを Vert.x で、動作確認のためのクライアントを HTML5 で実装する事にします。

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20130114/

クライアントの作成 (HTML5

まずはクライアントです。

以下のような JSON データを送受信する WebSocket の処理を CoffeeScript で実装しました。送信する画像データは Web ブラウザへ画像ファイルをドラッグアンドドロップする事で切り替えられるようにしています。

{"message": <メッセージ>, "image": <Data URL 形式の画像データ>}
index.coffee
$( -> 
    # ドラッグアンドドロップ処理
    addEventListener 'dragover', (ev) ->
        ev.preventDefault()
    ,false

    # ドロップした画像で img の src を置き換える処理
    addEventListener 'drop', (ev) ->
        ev.preventDefault()

        file = ev.dataTransfer.files[0]

        if file.type.indexOf('image/') is 0
            r = new FileReader()
            r.onload = (ev) -> $('#image').attr 'src', ev.target.result
            r.readAsDataURL file

    # WebSocket
    ws = new WebSocket 'ws://localhost:8080/connect'

    # JSONデータ受信時の処理
    ws.onmessage = (event) ->
        obj = JSON.parse event.data
        $('#list').prepend "<div><img class=\"chatimg\" src=\"#{obj.image}\" /><p class=\"msg\">#{obj.message}</p></div>"

    # 接続時の処理
    ws.onopen = (event) -> console.log "open : #{event}"
    # 切断時の処理
    ws.onclose = (event) -> console.log "close : code = #{event.code}, reason = #{event.reason}"

    # JSONデータ送信処理
    $('#sendMessage').click ->
        msgEl = $('#message')

        params =
            message: msgEl.val()
            image: $('#image').attr('src')

        ws.send JSON.stringify(params)
        msgEl.val('')

    # ページ終了時の処理
    $(window).bind 'beforeunload', ->
        ws.close()
)

上記ファイルを JavaScript へ変換しておきます。(下記コマンドで index.js ファイルが生成されます)

JavaScript への変換
> coffee -c -b index.coffee

HTML ファイルの内容は以下の通りです。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>image chat sample</title>
    <link rel="stylesheet" href="index.css" />
    <script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
    <script src="index.js"></script>
</head>
<body>
    <div>
        <img id="image" src="data:image/png;base64,iVBORw・・・Jggg==" class="chatimg" />
        <textarea id="message" cols="50" rows="2" ></textarea>
        <br />
        <button id="sendMessage">send</button>
    </div>

    <hr />

    <div id="list"></div>
</body>
</html>

サーバーの作成 (Vert.x)

それでは、本題のサーバー部分を Groovy で実装してみます。

Vert.x の Groovy では、HttpServer の websocketHandler へクロージャを渡して WebSocket の処理を実装します。

このクロージャは新規クライアントからの接続が発生する度に実行され、パラメータとして WebSocket クラスのインスタンスが渡ってきます。

WebSocket クラスには複数のハンドラーメソッド(xxxHandler)が用意されており、任意のハンドラーにクロージャを渡して各処理を実装します。

今回は以下のハンドラーを使いました。

  • dataHandler でデータ受信時の処理を実装 (全クライアントへデータ送信)
  • closedHandler でクライアントとの接続が切れた際の処理を実装 (wsMap から接続が切れたものを削除)

下記サンプルでは、ConcurrentHashMap で WebSocket のインスタンスを管理しておき、JSON 受信時に全 WebSocket インスタンスへ JSON をそのまま送信するよう実装しています。

server.groovy
def wsMap = [:] as java.util.concurrent.ConcurrentHashMap

vertx.createHttpServer().websocketHandler { ws ->
    ws.dataHandler { data ->
        // 全クライアントへデータ送信
        wsMap.each { k, v ->
            v.writeTextFrame(data.toString())
        }
    }

    ws.closedHandler {
        // 接続切断されたものを wsMap から削除
        wsMap.remove(ws.textHandlerID)
        println "closed : ${ws.textHandlerID}"
    }

    println "connect : ${ws.textHandlerID}"
    wsMap.put(ws.textHandlerID, ws)

}.listen 8080

println "server started ..."

下記のように vertx run でサーバーを起動した後、index.html ファイルを複数の Web ブラウザで開いて *1 send ボタンを押下すると JSON の送受信を確認できると思います。

ただし、一度 JSON データを送信した後でないと受信できないようなので注意。(原因はよく分かってません)

実行例
> vertx run server.groovy
server started ...
connect : 8680eeb8-4666-4ec0-9496-98d8269f8099
connect : 1dd620bb-f9b3-4bed-90c9-aa2bb2243786
closed : 8680eeb8-4666-4ec0-9496-98d8269f8099
・・・
画面例

*1:今回はローカルファイルを直接開きました