kagamihogeの日記

kagamihogeの日記です。

JBoss AS 7でWebSocket(Atmosphere)動かす準備するところまで

2014/06/19 追記 このエントリに書いてある内容は古くなっておりhttp://www.wildfly.org/使えば下記のソースからビルドなどの手順は不要です。

環境

やったこと

WebSocketをJBossで動かしてみよう、と考えたのは単なる趣味です。

で、まずはその方法はおろかWebSocketがどういうものなのかも良く知らないのでとりあえずぐぐってみる。Websocket support in AS7 | Communityというページがあり、WebSocketを既に利用してるコンポーネントだとか、WebSocketの実装一覧だとかがある。ここでようやく、WebSocketは具体的な実装を選ばないといけないことを知る。んで、「JBoss WebSocket」とかでぐぐるAtmosphere/atmosphere · GitHubが上の方に出てくるのでこれを使うことにした。

JBoss AS 7.1.2以降のビルド

Installing JBoss WebSocket Support · Atmosphere/atmosphere Wiki · GitHubによると「Because jboss-websockets works with AS 7.1.2+ only, you need to build it also like this」とあり、JBoss AS 7.1.2以降を用意しなければならない。2013/03/11時点では、JBoss Application Server Downloads - JBoss Communityには7.1.1.Finalまでしか無い。というわけで、ソースから自前でビルドしたモジュールを用意しなければならない。なぜ7.1.2以降でしかダメなのかは、どうやらjboss-websocketsが、7.2以降にしか含まれていないApache Portable Runtime connector (APR)とかいうのに依存してるかららしい。

ちなみに7.1.1だとこんな実行時例外が発生します。

20:25:33,250 ERROR [org.apache.catalina.core.ContainerBase.[jboss.web].[default-host].[/chat].[org.atmosphere.samples.chat.MyWebSocketServlet]] (http-localhost-127.0.0.1-8080-1) サーブレット org.atmosphere.samples.chat.MyWebSocketServlet のServlet.service()が例外を投げました: java.lang.ClassNotFoundException: org.jboss.servlet.http.UpgradableHttpServletResponse from [Module "deployment.chat.war:main" from Service Module Loader]

他の実装を選ぶとかすれば、自前でビルドとかやらなくても良い気がするけど、今回は折角なので自分でビルドを試してみることにした。フツウは中々ソースからビルドとかやらないんで、良い機会かと思いまして。

というわけで7.2.0.Finalをビルドする。Installing JBoss WebSocket Support · Atmosphere/atmosphere Wiki · GitHubからほぼコピペだけど、gitのコマンドいくつかとビルドスクリプト実行するだけ。取得バージョンは7.2.0.Finalに変えています。windowsでやったので、batファイルを実行します。

git clone https://github.com/jbossas/jboss-as.git
cd jboss-as/
git checkout 7.2.0.Final
build.bat

それとbuild.batは下の方をこんな感じに変更。テストはスキップしてええやろってことで、SMOKE_TESTSてのをコメントアウト。あと、mavenのオプションに-Dmaven.test.skip=trueを追加。ビルドがOutOfMemoryErrorで落ちたのでOutOfMemoryError - Apache Maven - Apache Software Foundationに従ってMAVEN_OPTSを追加。

REM set SMOKE_TESTS=-Dintegration.module -Dsmoke.integration.tests

set MAVEN_OPTS=-Xmx512m -XX:MaxPermSize=128m
call %1 %GOAL% %SMOKE_TESTS% %3 %4 %5 %6 %7 %8 -Dmaven.test.skip=true

これでビルドできた。

サンプルコードの作成

mikebrock/jboss-websockets · GitHubを参考にコードを書く。

まず、%JBOSS_ROOT%\standalone\configuration\standalone.xmlを下記のように書き換える。native="false"になってるのをnative="true"に書き換える。7.2だと、web:1.1からweb:1.4に変わってます。

<subsystem xmlns="urn:jboss:domain:web:1.4" default-virtual-server="default-host" native="true">

Installing JBoss WebSocket Support · Atmosphere/atmosphere Wiki · GitHubを参考にpom.xmlを書く。atmosphere-runtimejboss-as-websocketsのバージョンは、その時々で最適なものを使って下さいねと。あと、サーブレットが無いとビルドが通らないのでJBossのヤツを入れてます。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>as7websocket</groupId>
	<artifactId>websocketsample</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	<name>websocketsample Maven Webapp</name>
	<url>http://maven.apache.org</url>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>3.8.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.jboss.spec.javax.servlet</groupId>
			<artifactId>jboss-servlet-api_3.0_spec</artifactId>
			<version>1.0.2.Final</version>
		</dependency>
		<dependency>
			<groupId>org.atmosphere</groupId>
			<artifactId>atmosphere-runtime</artifactId>
			<version>1.0.11</version>
		</dependency>
		<dependency>
			<groupId>org.atmosphere.jboss.as</groupId>
			<artifactId>jboss-as-websockets</artifactId>
			<version>0.3</version>
		</dependency>
	</dependencies>
	<build>
		<finalName>websocketsample</finalName>
	</build>
</project>

コピペしてきたサンプルのソースコード

package atmosphere.samples;

import java.io.IOException;

import javax.servlet.annotation.WebServlet;

import org.atmosphere.jboss.as.websockets.WebSocket;
import org.atmosphere.jboss.as.websockets.servlet.WebSocketServlet;
import org.atmosphere.jboss.websockets.Frame;
import org.atmosphere.jboss.websockets.frame.TextFrame;

@WebServlet("/websocket/")
public class MyWebSocketServlet extends WebSocketServlet {

    @Override
    protected void onSocketOpened(WebSocket socket) throws IOException {
        System.out.println("Websocket opened :)");
    }

    @Override
    protected void onSocketClosed(WebSocket socket) throws IOException {
        System.out.println("Websocket closed :(");
    }

    @Override
    protected void onReceivedFrame(WebSocket socket) throws IOException {
        final Frame frame = socket.readFrame();
        if (frame instanceof TextFrame) {
            final String text = ((TextFrame) frame).getText();
            if ("Hello".equals(text)) {
                socket.writeFrame(TextFrame.from("Hey, there!"));
            }
        }
    }
}

これをデプロイして http://localhost:8080/websocketsample/websocket/ にアクセスするとコンソールログに「Websocket opened :)」と表示される。

クライアント側をGetting started with the atmosphere framework and websocket · Atmosphere/atmosphere Wiki · GitHubとかを参考にしながら作る。

まずindex.html サンプルをモロにぱくったもの。

<!DOCTYPE html>
<!-- Atmosphere -->
<script type="text/javascript" src="jquery/jquery-1.9.0.js"></script>
<script type="text/javascript" src="jquery/jquery.atmosphere.js"></script>
<!-- Application -->
<script type="text/javascript" src="jquery/application.js"></script>


<html>

<head>
    <meta charset="utf-8">
    <title>Atmosphere Chat</title>

    <style>
        * {
            font-family: tahoma;
            font-size: 12px;
            padding: 0px;
            margin: 0px;
        }

        p {
            line-height: 18px;
        }

        div {
            width: 500px;
            margin-left: auto;
            margin-right: auto;
        }

        #content {
            padding: 5px;
            background: #ddd;
            border-radius: 5px;
            border: 1px solid #CCC;
            margin-top: 10px;
        }

        #header {
            padding: 5px;
            background: #f5deb3;
            border-radius: 5px;
            border: 1px solid #CCC;
            margin-top: 10px;
        }

        #input {
            border-radius: 2px;
            border: 1px solid #ccc;
            margin-top: 10px;
            padding: 5px;
            width: 400px;
        }

        #status {
            width: 88px;
            display: block;
            float: left;
            margin-top: 15px;
        }
    </style>
</head>
<body>
<div id="header"><h3>Atmosphere Chat. Default transport is WebSocket, fallback is long-polling</h3></div>
<div id="content"></div>
<div>
    <span id="status">Connecting...</span>
    <input type="text" id="input" disabled="disabled"/>
</div>

</body>
</html>

javascriptの準備。jquery-1.9.0.jsとjquery.atmosphere.jsはここから持ってくる。

jquery/application.js

 $(function () {
    "use strict";

    var header = $('#header');
    var content = $('#content');
    var input = $('#input');
    var status = $('#status');
    var myName = false;
    var author = null;
    var logged = false;
    var socket = $.atmosphere;
    var subSocket;
    var transport = 'websocket';

    // We are now ready to cut the request
    var request = { url: document.location.toString() + 'websocket/',
        contentType : "application/json",
        logLevel : 'debug',
        transport : 'websocket' ,
        fallbackTransport: 'long-polling'};


    request.onOpen = function(response) {
        content.html($('<p>', { text: 'Atmosphere connected using ' + response.transport }));
        input.removeAttr('disabled').focus();
        status.text('Choose name:');
        transport = response.transport;
    };

    request.onTransportFailure = function(errorMsg, request) {
        jQuery.atmosphere.info(errorMsg);
        if (window.EventSource) {
            request.fallbackTransport = "sse";
        }
        header.html($('<h3>', { text: 'Atmosphere Chat. Default transport is WebSocket, fallback is ' + request.fallbackTransport }));
    };

    request.onMessage = function (response) {
        content.append('<p>' + response.responseBody +'</p>');
    };

    request.onClose = function(response) {
        subSocket.push(jQuery.stringifyJSON({ author: author, message: 'disconnecting' }));
        logged = false;
    };

    request.onError = function(response) {
        content.html($('<p>', { text: 'Sorry, but there\'s some problem with your '
            + 'socket or the server is down' }));
    };

    subSocket = socket.subscribe(request);

    input.keydown(function(e) {
        if (e.keyCode === 13) {
        	subSocket.push('Hello');
        }
    });
});

requestオブジェクトのurlパラメータは'websocket/'と、最後にスラッシュを付けないと404になる。サーブレットマッピングだからだろうか?
input.keydownは、'Hello'っていう固定文字列を送るだけで、サーバ側はその文字列が来たときだけ"Hey, there!"という文字列を送り返す。で、リスナーに当たるrequest.onMessageが"Hey, there!"を画面に表示する。