Jettyで始めるWebSocket超入門

第6回アプリケーションの作成と配布物の生成

連載の最後となる今回は、WebSocketのトピックをいくつか取り上げたあと、WebSocketChatのアプリケーション化と配布物の生成を行ないます。

WebSocketのセキュリティ

WebSocketは、リビジョン76で接続処理に手を加え、堅牢性が増しています。また、HTTPと同様なOriginを基にしたセキュリティモデルを採用しています。Ajaxと違い、現行のWebSocket対応ブラウザ側には、same originポリシーによる制限はないようです。

Jetty7はそのままでは、cross originが可能です。制限が必要な場合は、CrossOriginFilter※1を使用します。

最新の仕様

第3回「データ送信の詳細」の項で、フレームタイプについて説明しました。最新の仕様では、フレームタイプの扱いが大きく変更になりそうです。ドラフト版のリビジョン76の段階では、フレームタイプがテキストフレームとバイナリフレームに大別されていましたが、執筆段階の最新のドラフト版ではフレームの仕様が大きく変わり、テキストやバイナリに関わらず、⁠TLV(type-length-value⁠⁠」形式になったようです。しかし、ドラフト版のリビジョン番号もまだ割り当てられていないため、今後どのように変更されるのかはわかりません。

この変更された仕様をJettyが実装した場合、Jettyのフレームタイプの定数[2]の扱いも変更されると思いますが、今回サンプルとして作成しているチャットアプリケーションは、受信したデータのフレームタイプを変更せずに送信しているため、メソッドが変更される等の大きな変更がない限り影響はないでしょう。

第3回「WebSocketプロトコル」の項でも言及しましたが、サーバやクライアントがどのリビジョンに対応しているのかを考慮し、ライブラリのバージョンを変更してください。

アイコンの準備

アプリケーション化を行なう前に、3種類のアイコンを準備しましょう[3]⁠。1つは、WindowsのタスクトレイやMacのステータスメニューに表示するためのアイコンです。2つ目はWindows用のアプリケーションアイコン、3つ目はMac OS X用のアプリケーションアイコンです[4]⁠。

「src/main/resources」内に、⁠images」という名前の新しいディレクトリを作成し、これらのアイコンを入れてください。

メニューの作成

ここまでに作成してきたWebSocketChatは、GUIが定義されていません。このままでは、起動していることもわかりませんし[5]⁠、終了することもできません。最低限のGUIとして、終了メニューを作成します。

そこで、第4回で作成したWebSocketChatクラスを以下のように変更しましょう。

リスト1 WebSocketChatクラス
package webSocketChat;

import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URL;

import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

public class WebSocketChat {

  public static void main(String[] args) throws Exception {
    new WebSocketChat();
  }

  public WebSocketChat() throws Exception {

    MenuItem quitMenuItem = new MenuItem("Quit");
    quitMenuItem.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        System.exit(0);
      }
    });

    PopupMenu popupMenu = new PopupMenu();
    popupMenu.add(quitMenuItem);

    URL imageUrl = this.getClass().getClassLoader().getResource("images/icon.png");
    TrayIcon trayIcon = new TrayIcon(Toolkit.getDefaultToolkit().createImage(imageUrl));
    trayIcon.setImageAutoSize(true);
    trayIcon.setToolTip("WebSocketChat");
    trayIcon.setPopupMenu(popupMenu);

    SystemTray systemTray = java.awt.SystemTray.getSystemTray();
    systemTray.add(trayIcon);

    Server server = new Server(8040);

    ResourceHandler rh = new ResourceHandler();
    rh.setResourceBase(this.getClass().getClassLoader().getResource("html").toExternalForm());

    MyWebSocketServlet wss = new MyWebSocketServlet();
    ServletHolder sh = new ServletHolder(wss);
    ServletContextHandler sch = new ServletContextHandler();
    sch.addServlet(sh, "/ws/*");

    HandlerList hl = new HandlerList();
    hl.setHandlers(new Handler[] {rh, sch});
    server.setHandler(hl);

    server.start();

  }

}

修正内容を順に解説します。

    MenuItem quitMenuItem = new MenuItem("Quit");
    quitMenuItem.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        System.exit(0);
      }
    });

「Quit」というラベルをつけてメニュー項目を作成しています。そして、無名クラス内でアプリケーション終了の処理を定義し、メニュー項目が選択された時の動作に追加します。

    PopupMenu popupMenu = new PopupMenu();
    popupMenu.add(quitMenuItem);

メニューのアイコンをクリックした時に表示するポップアップメニューを作成し、メニュー項目を追加します。

    URL imageUrl = this.getClass().getClassLoader().getResource("images/icon.png");
    TrayIcon trayIcon = new TrayIcon(Toolkit.getDefaultToolkit().createImage(imageUrl));
    trayIcon.setImageAutoSize(true);
    trayIcon.setToolTip("WebSocketChat");
    trayIcon.setPopupMenu(popupMenu);

メニューアイコンの画像を、トレイアイコンに指定します。環境によってメニューの高さが違う可能性を考慮し、大きめの画像を準備して、⁠setImageAutoSize」で表示サイズを自動的に変更されるようにしました。そして、利用者がWebSocketChatとわかるようにツールチップも指定し、ポップアップメニューを登録しています。

    SystemTray systemTray = java.awt.SystemTray.getSystemTray();
    systemTray.add(trayIcon);

システムトレイは、Windowsではタスクトレイ、Macではステータスメニュー、Linuxではノーティフィケーションエリアやシステムトレイ等を指します[6]⁠。このシステムトレイにトレイアイコンを追加します。

それでは、EclipseからWebSocketChatを起動してみてください。

図1 メニューが表示された様子(Windowsの場合)
図1 メニューが表示された様子(Windowsの場合)
図2 メニューが表示された様子(Mac OS Xの場合)
図2 メニューが表示された様子(Mac OS Xの場合)
図3 メニューが表示された様子(Ubuntuの場合)
図3 メニューが表示された様子(Ubuntuの場合)

それぞれのOSで、メニューが表示するされることを確認できました。

アプリケーション化

次に、WebSocketChatのアプリケーション化を行ないます。pom.xmlを修正したのちに、パッケージの設定のために「アセンブリ記述子」が記述されたファイルを作成します。

まず、pom.xmlを修正します。

リスト2 pom.xml
<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>WebSocketChat</groupId>
  <artifactId>WebSocketChat</artifactId>
  <name>WebSocketChat</name>
  <version>0.0.1-SNAPSHOT</version>
  <properties>
    <package.package-name>webSocketChat</package.package-name>
    <package.main-class>${package.package-name}.${name}</package.main-class>
    <package.base-name>${name}-${version}</package.base-name>
    <package.jar-name>${package.base-name}.jar</package.jar-name>
    <package.exe-name>${package.base-name}.exe</package.exe-name>
    <package.app-name>${package.base-name}.app</package.app-name>
  </properties>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
          <encoding>UTF-8</encoding>
          <debug>true</debug>
          <optimize>false</optimize>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <mainClass>${package.main-class}</mainClass>
              <packageName>${package.package-name}</packageName>
              <addClasspath>true</addClasspath>
              <classpathPrefix>lib</classpathPrefix>
            </manifest>
            <manifestEntries>
              <Built-By></Built-By>
            </manifestEntries>
          </archive>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>osxappbundle-maven-plugin</artifactId>
        <version>1.0-alpha-2</version>
        <configuration>
          <mainClass>${package.main-class}</mainClass>
          <jvmVersion>1.6+</jvmVersion>
          <iconFile>target/classes/images/mac.icns</iconFile>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>bundle</goal></goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.bluestemsoftware.open.maven.plugin</groupId>
        <artifactId>launch4j-plugin</artifactId>
        <version>1.5.0.0</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>launch4j</goal></goals>
            <configuration>
              <headerType>GUI</headerType>
              <jar>target/${package.jar-name}</jar>
              <outfile>target/${package.exe-name}</outfile>
              <customProcName>${name}</customProcName>
              <errTitle>${name}</errTitle>
              <icon>target/classes/images/win.ico</icon>
              <jre>
                <minVersion>1.6.0</minVersion>
              </jre>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>attached</goal></goals>
            <configuration>
              <descriptors>
                <descriptor>src/main/assembly/assembly.xml</descriptor>
              </descriptors>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-webapp</artifactId>
      <version>7.1.4.v20100610</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-websocket</artifactId>
      <version>7.1.4.v20100610</version>
    </dependency>
  </dependencies>
</project>

それでは、修正内容を見ていきましょう。

  <name>WebSocketChat</name>

「name」要素は、Mavenプロジェクト情報のひとつです。WebSocketChatは他のプロジェクトから利用されることは想定していませんが、⁠name」要素の値をプロパティの設定で利用するために定義しています。

  <properties>
    <package.package-name>webSocketChat</package.package-name>
    <package.main-class>${package.package-name}.${name}</package.main-class>
    <package.base-name>${name}-${version}</package.base-name>
    <package.jar-name>${package.base-name}.jar</package.jar-name>
    <package.exe-name>${package.base-name}.exe</package.exe-name>
    <package.app-name>${package.base-name}.app</package.app-name>
  </properties>

プロパティの設定です。要素名がプロパティ名になります。プロパティの値を参照するには「${プロバティ名}」と記述します。例えば「package.main-class」の値は、⁠package.package-name」の値「webSocketChat」と、⁠name⁠⁠ の値「WebSocketChat」を参照し、⁠webSocketChat.WebSocketChat」となります。

      <plugin>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <mainClass>${package.main-class}</mainClass>
              <packageName>${package.package-name}</packageName>
              <addClasspath>true</addClasspath>
              <classpathPrefix>lib</classpathPrefix>
            </manifest>
            <manifestEntries>
              <Built-By></Built-By>
            </manifestEntries>
          </archive>
        </configuration>
      </plugin>

「jar」プラグインでjarファイルの設定を行なっています。ここでは、jarファイルのマニフェストファイルに記述する各種情報を定義しています。この設定で出力される「MANIFEST.MF」ファイルは以下のようになります。

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Build-Jdk: 1.6.0_20
Package: webSocketChat
Main-Class: webSocketChat.WebSocketChat
Built-By: 
Class-Path: lib/jetty-webapp-7.1.4.v20100610.jar lib/jetty-xml-7.1.4.v
 20100610.jar lib/jetty-util-7.1.4.v20100610.jar lib/jetty-servlet-7.1
 .4.v20100610.jar lib/jetty-security-7.1.4.v20100610.jar lib/jetty-ser
 ver-7.1.4.v20100610.jar lib/servlet-api-2.5.jar lib/jetty-continuatio
 n-7.1.4.v20100610.jar lib/jetty-http-7.1.4.v20100610.jar lib/jetty-io
 -7.1.4.v20100610.jar lib/jetty-websocket-7.1.4.v20100610.jar

マニフェストファイルの内容をみれば、pom.xmlで何を設定していたのかよく理解できると思います。

唯一わかりにくいのが「Built-By」要素の指定ですが、これは個人情報の漏洩を考えての処置です。⁠Built-By」要素の値を指定しなかった場合、パソコンのログインユーザIDが使用されます。ログインに本名を使っている人で本名を明かしたくない方は、⁠<Built-By></Built-By>」のようにするか、ハンドルネームを指定しておけばよいでしょう。

      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>osxappbundle-maven-plugin</artifactId>
        <version>1.0-alpha-2</version>
        <configuration>
          <mainClass>${package.main-class}</mainClass>
          <jvmVersion>1.6+</jvmVersion>
          <iconFile>target/classes/images/mac.icns</iconFile>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>bundle</goal></goals>
          </execution>
        </executions>
      </plugin>

Mac OS X用にアプリケーションを出力するための設定です。⁠jar」プラグインはMavenに付随しているためバージョンを指定していませんでしたが、⁠osxappbundle-maven-plugin」OS X Application Bundle Pluginというサードパーティ製のプラグインのため、⁠アルファ版ではありますが)執筆時の安定したバージョンを指定しています。バージョン番号は、必要に応じて変更してください。

「src/main/resources」配下のファイルは、パッケージ作成時に「target/classes」に複製されます。アイコンファイルの指定は、このディレクトリ配下に対して行なっています。

その他に、このアプリケーションを実行するために使用するJVMのバージョンの指定、Mavenのフェーズ[7]⁠、使用するプラグインのゴール[8]を行なっています。

      <plugin>
        <groupId>org.bluestemsoftware.open.maven.plugin</groupId>
        <artifactId>launch4j-plugin</artifactId>
        <version>1.5.0.0</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>launch4j</goal></goals>
            <configuration>
              <headerType>GUI</headerType>
              <jar>target/${package.jar-name}</jar>
              <outfile>target/${package.exe-name}</outfile>
              <customProcName>${name}</customProcName>
              <errTitle>${name}</errTitle>
              <icon>target/classes/images/win.ico</icon>
              <jre>
                <minVersion>1.6.0</minVersion>
              </jre>
            </configuration>
          </execution>
        </executions>
      </plugin>

Windows用にアプリケーションを出力するための設定です。jarファイルをWindowsアプリケーションにするLaunch4jを、Mavenから利用するためのプラグインLaunch4j Maven Pluginを使用します。⁠OS X Application Bundle Plugin」同様、バージョンを指定しています。

設定項目は「Launch4j Maven Plugin」の作者のページを、項目の内容は「Launch4j」公式ページのドキュメントをご覧ください。⁠Launch4j」は日本語の情報も豊富にあるため、ここでは詳細は割愛します。

      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>attached</goal></goals>
            <configuration>
              <descriptors>
                <descriptor>src/main/assembly/assembly.xml</descriptor>
              </descriptors>
            </configuration>
          </execution>
        </executions>
      </plugin>

「assembly」プラグインはアーカイブファイルを作成するためのものです。アーカイブの設定は、別ファイルに「アセンブリ記述子」で行ないます。

配布物の作成

どのようなアーカイブファイルを作成するかを指定するための「アセンブリ記述子」の実体は、xmlファイルです。⁠src/main/assembly」ディレクトリを作成し、以下の内容の「assembly.xml」を配下に配置してください。

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
  <id>bin</id>
  <formats>
    <format>zip</format>
  </formats>
  <fileSets>
    <fileSet>
      <directory>target</directory>
      <outputDirectory>/</outputDirectory>
      <includes>
        <include>*.jar</include>
        <include>*.exe</include>
        <include>*.dmg</include>
      </includes>
    </fileSet>
  </fileSets>
  <dependencySets>
    <dependencySet>
      <outputDirectory>lib</outputDirectory>
      <unpack>false</unpack>
      <scope>runtime</scope>
    </dependencySet>
  </dependencySets>
</assembly>

「id」要素には作成されるアーカイブファイル名を識別するための文字列を、⁠format」要素にはアーカイブファイルの形式を指定します。上記のアセンブリ識別子の場合、アーカイブファイル名は「[POMファイルで指定したartifactId]-[POMファイルで指定したversion]-bin.zip」となります。

「fileSets」要素はアーカイブファイルに含めるファイルの設定です。⁠fileSet」要素毎に複数指定できます。Mavenが生成するファイルはすべて「target」ディレクトリ配下に出力されるため、⁠directory」要素は「target」になっています。⁠outputDirectory」要素には、アーカイブするファイルの、アーカイブファイル内の位置を指定します。

使用しているMavenのライブラリを添付する設定は、⁠dependencySets」要素配下で行ないます。実行時に使用するライブラリのみを含めるように、⁠scoope」要素には「runtime」を指定します。他には、出力先のパス、ライブラリのjarファイルを解いてclassファイルを含めるかどうかを設定しています。

最後にコンパイルとパッケージ化を一度に行ないます。⁠プロジェクト・エクスプローラー」内の「WebSocketChat」を右クリックし、⁠実行⁠⁠→⁠Maven package」を選択してください。⁠target」ディレクトリ内に「WebSocketChat-0.0.1-SNAPSHOT-bin.zip」という名前のアーカイブファイルが出力されます。

このアーカイブファイルには、WebSocketChatのjarファイル、Windows用のexeファイル、Mac OS X用のdmgファイルが、⁠lib」ディレクトリ配下には使用するライブラリは含まれます。Linuxで使用する時は、ターミナルで「java -jar WebSocketChat-0.0.1-SNAPSHOT.jar」を実行してください。

おわりに

当初、全4回の連載の予定でしたが、できるだけ丁寧に解説しようとした結果、ここまで延びてしまいました。連載で作成したアプリケーションを基に、機能を拡張するために必要なキーワードは可能な限り散りばめたつもりです。

連載中も何度も言及しましたが、WebSocketは未だ正式な仕様がない未成熟な技術です。アプリケーションが使用しているライブラリを最新のバージョンにあげたり、新しいブラウザがリリースされた結果、互換性がなくなり動作しなくなる可能性もあります。しかし、それでも追いかける価値のあるポテンシャルを非常に秘めた技術であると、筆者は考えています。正式な仕様のリリースを、みなさんと一緒に首を長くして待っています。

今回解説したサンプルファイルがダウンロードできます。

「src/test/java」「src/test/resources」が空のディレクトリとなっていますが、これはMavenプロジェクトを生成時に自動的にできるものですので、環境の再現のためにあえて入れています。

おすすめ記事

記事・ニュース一覧