Tomcat 6 と Comet の設定

ここのところ PHPばかりで Java をいじっていなかった。。。そんなわけで、ずいぶんとと新しくなった Tomcat でもいじってみようかな。

今回は、Solaris 10 x86 を使用します。必要なすべてのモジュールが /usr/local/src にダウンロードされているものとして説明をします。また、ほとんどのモジュールを /usr/local にインストールします。

自宅で利用しているのが Solaris Express Developer Editionなのでほとんどの開発環境が整っています。必要なら gcc, make, perl, m4, autoconf, libtool 等の開発環境を http://www.sunfreeware.com/ からダウンロードして Solaris にインストールしておいてください。gcc 以外は、バイナリーがない場合でも GNU のサイトからソースを入手して簡単に作ることができます。 (ソースを展開したディレクトリーで ./configure;make;make install を実行すると /usr/local/bin にインストールされます)

# はプロンプトで、スーパーユーザー(root) での作業であることを意味します。


Java (SDK)

Solaris 10 には JDKがインストールされているので、Tomcat 6 で利用可能なバージョンであればそのまま使います。

 動作チェック:
    # java -version
    java version "1.6.0"
    Java(TM) SE Runtime Environment (build 1.6.0-b105)
    Java HotSpot(TM) Server VM (build 1.6.0-b105, mixed mode)
    # 

気分的に。。。ちょっと古いので新しくする。Sun のサイトから JDK 6 Update 3 を落とし、古いバージョンを消去してからインストール。

    # cd /usr/local/src
    # gtar xzf jdk-6u3-solaris-i586.tar.Z
    # gtar xzf jdk-6u3-solaris-amd64.tar.Z
    # pkgrm SUNWj6rt SUNWj6rtx SUNWj6dev SUNWj6dvx SUNWj6dmo SUNWj6dmx SUNWj6man SUNWj6jmp SUNWj6cfg
    # pkgadd -d . SUNWj6rt SUNWj6rtx SUNWj6dev SUNWj6dvx SUNWj6dmo SUNWj6dmx SUNWj6man SUNWj6jmp SUNWj6cfg
    # java -version
    java version "1.6.0_03"
    Java(TM) SE Runtime Environment (build 1.6.0_03-b05)
    Java HotSpot(TM) Server VM (build 1.6.0_03-b05, mixed mode)
    # 

Apache 2.2 + Tomcat 6

バイナリーおよびソースを入手します。ここでは、ドキュメントを記述する時点で最新のものを利用していますが、実際に利用する場合にはセキュリティ上問題のないものを入手するようにしてください。また、提供されているソースなどが改ざんされていないことを確かめる上で PGP や MD5 による署名を確認しておくと良いでしょう。

Apache2 (HTTP Server) http://httpd.apache.org/download.cgi
Tomcat 6 (Servlet 2.5/JSP 2.1) http://tomcat.apache.org/download-60.cgi

Servlet/JSPコンテナーである Tomcat のインストール

必要なものが揃っているバイナリ版(apache-tomcat-6.0.14.tar.gz)をインストールします。Pure Java なコードなのでバイナリーがどのマシンでも動作します...ここでは説明しませんが、もしソースから作る場合、多くのアーカイブが必要になります。ソースパッケージに含まれる BUILDING.txt を参照してください(ANTを利用してコンパイル)。

    # cd /usr/local
    # gzip -dc src/apache-tomcat-6.0.14.tar.gz | tar xpf -
sh, bash 系の場合:.profile, .bashrc に登録しておくと良い
    CATALINA_HOME=/usr/local/apache-tomcat-6.0.14
    JAVA_HOME=/usr/jdk/jdk1.6.0_03
    CLASSPATH=.:$JAVA_HOME/lib/tools.jar
    export CATALINA_HOME JAVA_HOME CLASSPATH
 
csh 系の場合:.cshrc, .tcsh に登録しておくと良い
    setenv CATALINA_HOME "/usr/local/apache-tomcat-6.0.14"
    setenv JAVA_HOME "/usr/jdk/jdk1.6.0_03"
    setenv CLASSPATH ".:$JAVA_HOME/lib/tools.jar"
    # /usr/local/apache-tomcat-6.0.14/bin/catalina.sh start
    Using CATALINA_BASE:   /usr/local/apache-tomcat-6.0.14
    Using CATALINA_HOME:   /usr/local/apache-tomcat-6.0.14
    Using CATALINA_TMPDIR: /usr/local/apache-tomcat-6.0.14/temp
    Using JRE_HOME:       /usr/jdk/jdk1.6.0_03

HTTP サーバーである Apache 2.2 のインストール

DSO(Dynamic Shared Object) をサポートするように設定します。また、Tomcat連携モジュールである mod_proxy_ajp を有効にします。インストール後、-l コマンドラインオプションで mod_proxy_ajp.c が有効になっていることを確認してみます。

    # cd /usr/local/src
    # gzip -dc httpd-2.2.6.tar.gz | tar xf -
    # cd httpd-2.2.6
    #  ./configure --enable-so --enable-ssl --enable-proxy
    checking for chosen layout... Apache
    checking for working mkdir -p... yes
    ...
    # make
    # make install
    # /usr/local/apache2/bin/httpd -l
    Compiled in modules:
      core.c
      mod_authn_file.c
      mod_authn_default.c
      mod_authz_host.c
      mod_authz_groupfile.c
      mod_authz_user.c
      mod_authz_default.c
      mod_auth_basic.c
      mod_include.c
      mod_filter.c
      mod_log_config.c
      mod_env.c
      mod_setenvif.c
      mod_proxy.c
      mod_proxy_connect.c
      mod_proxy_ftp.c
      mod_proxy_http.c
      mod_proxy_ajp.c
      mod_proxy_balancer.c
      mod_ssl.c
      prefork.c
      http_core.c
      mod_mime.c
      mod_status.c
      mod_autoindex.c
      mod_asis.c
      mod_cgi.c
      mod_negotiation.c
      mod_dir.c
      mod_actions.c
      mod_userdir.c
      mod_alias.c
      mod_so.c
    #

Solaris に Apache 2.2 をサービスに追加する場合、「Solaris 10 に PHP5 と MySQL5 をインストール」を参照してください。

Tomcat と連携する設定を記述します。

# edit /usr/local/apache2/conf/httpd.conf
# Distributed authoring and versioning (WebDAV)
#Include conf/extra/httpd-dav.conf
 
# Tomcat settings
Include conf/extra/httpd-tomcat.conf
 
# Various default settings
#Include conf/extra/httpd-default.conf
# edit /usr/local/apache2/conf/extra/httpd-tomcat.conf
<Location /examples/>
  ProxyPass ajp://localhost:8009/examples/
</Location>
<Location /tomcat/>
  ProxyPass ajp://localhost:8009/
</Location>

※「http://www.example.com/examples/」にアクセスすると「Tomcatのexamplesディレクトリ」配下に転送されます。
※ 8009は、Tomcat側の連携コネクタのポート番号です。(/usr/local/apache-tomcat-6.0.14/conf/server.xml)

設定が終わったら、チェックします。

    # /usr/local/apache2/bin/apachectl configtest
    Syntax OK
    # 

実際に動かして、起動するかも確認。

    # /usr/local/apache2/bin/apachectl start
    サービスに登録してある場合
    # svcadm enable apache2

ブラウザーからアクセスしたりプロセスを確認したときに、httpd が起動されていなかった場合、/usr/local/apache2/logs/error_log の内容をチェックしてエラーの原因を解決してください。

Tomcat(8080ポート)でのアクセス Apache(80ポート)でのアクセス
tomcat apache

とりあえず、簡単なメッセージを表示する Servlet でテストします。

# mkdir /usr/local/apache-tomcat-6.0.14/webapps/ROOT/WEB-INF/classes
# edit /usr/local/apache-tomcat-6.0.14/webapps/ROOT/WEB-INF/classes/HelloWorld.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class HelloWorld extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
response.setContentType("text/html; charset=EUC-JP");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World!</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Hello World!</h1>"); out.println("<p>ようこそ Tomcat 6 の世界へ</p>");
out.println("</body>");
out.println("</html>");
}
}
# cd /usr/local/apache-tomcat-6.0.14/webapps/ROOT/WEB-INF/classes
# javac -classpath /usr/local/apache-tomcat-6.0.14/lib/servlet-api.jar HelloWorld.java
# edit /usr/local/apache-tomcat-6.0.14/webapps/ROOT/WEB-INF/web.xml
 ...
  <servlet>
      <servlet-name>HelloWorld</servlet-name>
<servlet-class>HelloWorld</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloWorld</servlet-name>
<url-pattern>/HelloWorld</url-pattern>
</servlet-mapping> </web-app>

Apache の設定で Tomcat のルートをアプリケーション名 "tomcat"と設定しておいたので、http://<サーバー名>/tomcat/HelloWorld とブラウザーでアクセスすることにより下記のように表示されます。

HelloWorld

テストが完了したらサーバーを終了。

    # /usr/local/apache2/bin/apachectl stop
    サービスに登録してある場合
    # svcadm disable apache2
 
    # /usr/local/apache-tomcat-6.0.14/bin/catalina.sh stop
    Using CATALINA_BASE:   /usr/local/apache-tomcat-6.0.14
    Using CATALINA_HOME:   /usr/local/apache-tomcat-6.0.14
    Using CATALINA_TMPDIR: /usr/local/apache-tomcat-6.0.14/temp
    Using JRE_HOME:       /usr/jdk/jdk1.6.0_03

各プログラムの起動とシステムへの登録

各ユーザーが設定しておくと便利な環境変数

sh, bash 系の場合
JAVA_HOME=/usr/jdk/jdk1.6.0_03
CATALINA_HOME=/usr/local/apache-tomcat-6.0.14
PATH=$PATH:$JAVA_HOME/bin:$CATALINA_HOME/bin
export JAVA_HOME CATALINA_HOME PATH
          
csh 系の場合
setenv JAVA_HOME /usr/jdk/jdk1.6.0_03
setenv CATALINA_HOME /usr/local/apache-tomcat-6.0.14
set path=($path $JAVA_HOME/bin $CATALINA_HOME/bin)

システム起動時に自動的に Tomcat や Apache を起動させたい場合、/etc/init.d に以下のスクリプトをコピーして /etc/rc2.d でスタートさせるようにする。もしくは、サービスとして登録する。

Tomcat
/usr/local/apache-tomcat-6.0.14/bin/catalina.sh start
/usr/local/apache-tomcat-6.0.14/bin/catalina.sh stop
Apache
/usr/local/apache2/bin/apachectl start
/usr/local/apache2/bin/apachectl stop

Tomcat Web アプリケーションマネージャーを利用するには、アカウントを登録します。

# edit /usr/local/apache-tomcat-6.0.14/conf/tomcat-users.xml
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="manager"/>
<user username="admin" password="admin" roles="manager"/>
</tomcat-users>

Tomcat を再起動ののち http://<サーバー名>/tomcat/manager/html (http://<サーバー名>:8080/manager/html)へブラウザーでアクセスすることにより設定したアカウントでログインすると下記のように表示されます。

manager


Comet

クライアントのWebブラウザから発信されるリクエストに対してサーバーがレスポンスを返すプル型の通信では、サーバ側で情報の更新があった場合リアルタイムにデータを取得できません。そのためクライアント側から定期的にリクエストを送信するなどを行わなくてはならず、無駄にトラフィックが増加してしまいます。そこで、Cometと呼ばれる、HTTPを使用して疑似的にプッシュ型の通信を実現する技術が開発されました。

Tomcatを用いてCometを実装するには、J2SE 1.4から導入されたNew I/Oを利用します(Apache Portable Runtimeを利用する方法もあります)。New I/O を利用するには、/usr/local/apache-tomcat-6.0.14/conf/server.xml の設定を変更します。

# edit /usr/local/apache-tomcat-6.0.14/conf/server.xml
<Server port="8005" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
               connectionTimeout="10000" redirectPort="8443"
               scheme="http" secure="false" />
    ...
  </Service>
</Server>
# edit /CometSampleServlet.java
/**
 * サーブレット - CometSampleServlet.java
 */
package sample;
import java.io.*;  
import java.net.*;  
import java.util.*;  
import javax.servlet.*;  
import javax.servlet.http.*;  
import org.apache.catalina.CometEvent;  
import org.apache.catalina.CometProcessor;     
 
public class CometSampleServlet extends HttpServlet implements CometProcessor {            
    protected ArrayList<HttpServletResponse> connections = new ArrayList<HttpServletResponse>();      
    protected MessageSender sender = null;
    /**
     * アクセスがあった際に呼び出されるイベントハンドラ
     */
    public void event(CometEvent event) throws IOException, ServletException {
        HttpServletRequest request = event.getHttpServletRequest();          
        HttpServletResponse response = event.getHttpServletResponse();    
 
        // コネクションが確率した際のイベント        
        if (event.getEventType() == CometEvent.EventType.BEGIN) {
            // データの送信を開始
            request.setAttribute("org.apache.tomcat.comet", Boolean.TRUE);
            PrintWriter writer = response.getWriter();
            writer.println("<!doctype html public \"-//w3c//dtd html 4.0 transitional//en\">");
            writer.println("<head><title>Comet Sample</title></head><body>");
            writer.flush();
            synchronized(connections) {
                connections.add(response);
            }
        }
 
        // I/Oエラーなど、何らかのエラーが発生した際のイベント
        else if (event.getEventType() == CometEvent.EventType.ERROR) {            
            synchronized(connections) {
                connections.remove(response);
            }
            event.close();
        }
 
        // リクエストの処理が終了した際のイベント
        else if (event.getEventType() == CometEvent.EventType.END) {
            // データの送信を終了
            synchronized(connections) {
                  connections.remove(response);
            }
            PrintWriter writer = response.getWriter();
            writer.println("</body></html>");
            event.close();
        }
 
        // データの入力があった場合のイベント
        else if (event.getEventType() == CometEvent.EventType.READ) {
        }
    }
 
    /**
     * 初期化処理
     */
    public void init() throws ServletException {
        sender = new MessageSender();
        Thread senderThread = new Thread(sender);
        senderThread.setDaemon(true);
        senderThread.start();
    }
 
    /**
     * 終了処理
     */
    public void destroy() {
        connections.clear();
        sender.stop();
        sender = null;
    }
 
    /**
     * データ送信用のスレッド
     */
    public class MessageSender implements Runnable {
        // 後述
    }
}
# edit /MessageSender.java
/**
 * サーブレット - MessageSender.java
 */

public class MessageSender implements Runnable 
{
    protected boolean running = true;
        public void stop() {
        running = false;
    }
    public void run() {
        while (running) {
            Date date = new Date();
            // 全てのクライアントにデータを送信
            for (int i = 0; i < connections.size(); i++) {
                try {
                    PrintWriter writer = connections.get(i).getWriter();
                    writer.println(date + "<br>");
                    writer.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            try {
                // 5秒休む
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
# edit /WEB-INF/web.xml
<servlet>
    <servlet-name>CometSampleServlet</servlet-name>
    <servlet-class>sample.CometSampleServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>CometSampleServlet</servlet-name>
    <url-pattern>/CometSampleServlet</url-pattern>
</servlet-mapping>

Tomcatを再起動してアプリケーションをデプロイし、ブラウザから「http://localhost:8080/Tomcat6Comet/CometServletSample」(アプリケーション名を"Tomcat6Comet"とした場合)にアクセスするすると、5秒毎に時刻が表示される。

このサンプルはリアルタイムにデータを表示させるためにコネクションを張ったままデータを送り続けるという、いささか強引なプログラムになっているため、 ブラウザの処理方法によっては正常に表示されない。スレッドの処理も簡略化してあるため、負荷が大きいので注意が必要だ。

本当の意味でリアルタイムのデータ表示を実現するならば、クライアント側でAjaxなどを利用して非同期な通信を行うのがいいだろう。そうすればサーバがレスポンスを返すまで待つ必要がなく、Cometの真価を十分に発揮できるようになる。