「ソフトウェア演習(B)Java言語」
ベーシックコース

第7回 ネットワークプログラミングとサーバサイドJava(Tomcat)

ネットワークプログラミング、パッケージを使ったプログラミング

Javaのビルトイン・ネットワーク機能では、大きく分けて2つの 通信方法をサポートしています。 ひとつは、ネットワークをストリームデータとみなす ストリームベース通信 (stream-based communications) による通信で、 もうひとつは、画像、音声、ビデオなどのデータ転送で よく用いられるパケットでの送受信に基づく パケットベース通信 (packet-based communications)です。 具体的には、これらの通信機能は、 java.netパッケージでサポートされています。 以下では、まず、ストリーム・ソケットでの通信をベースとする TCP (Transmission Control Protocol)の代表例として HTTP (Hyper Text Transfer Protocol)でのネットワーク利用例を述べます。

URI関連クラスを使ったJava言語でのネットワーク機能

Java言語が広まった1990年代半ばは、インターネットが ちょうど普及し始めた時期でもあります。 C言語でもソケットなどを利用した別のマシン資源への アクセス方法を含むライブラリが広まり始めたころです。 Java言語は、このような背景があり、当時の他の言語にはない、 URI (Uniform Resource Identifier)で遠隔資源にアクセスするための クラスや関数がビルトイン機能として、java.netパッケージに含まれています。 URIとは、 URL (Uniform Resource Locator)と URN (Uniform Resource Name)からなるものです。 URNは、名前がユニークなものならなんでもよく、 たとえば、図書を管理するISBNなども含まれます。 URIではリソースをhttp/httpsやftpのあとに コロンをつけてリソース名を記述します。

以下では、URLクラスでURLを通したHTTPでの通信を確立し、 その後、サーバ(Webサーバ)から、サーバ上の Webページ情報を取得し、そのテキスト情報をプリント するサンプルプログラムを紹介します。
GetWebContent.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*プログラム7-1 Webページのコンテンツの入手を試みる */ 
import java.net.URL;/* URLクラス */
import java.net.URLConnection;/* URLConnectionクラス */
import java.net.MalformedURLException;/* URL例外 */
import java.io.InputStream;/* 入力ストリーム */
import java.io.InputStreamReader;/* 入力ストリームリーダー */
import java.io.BufferedReader;/* バッファ入力リーダー */
import java.io.IOException;/* 入出力例外 */
import java.util.Scanner; /* Scannerクラス */

public class GetWebContent {/* URLでアクセスに、Webページのテキストにアクセスを試みる */

    public static void main(String[] args) {
        Scanner input = new Scanner( System.in );/* System.in(端末から)*/
        System.out.print( "URLを入力してください  > " ); /* プロンプト */ 
        String urlString = input.next(); /* 第一引数を文字列として読込む (next関数) */
        try {
            URL url = new URL(urlString);/* 入力URLでHTTPでアクセスを試みる */
            URLConnection con = url.openConnection();/* コネクションが設定できたらスーパークラスのオブジェクトを得る */
            InputStream in = con.getInputStream();/* 入力ストリームでアクセス */
            BufferedReader bin = /* バッファリーダーでアクセス */
                new BufferedReader(new InputStreamReader(in));
            String line;
            while ((line = bin.readLine()) != null) 
                System.out.println(line);/* 行単位でプリント */
        } 
        catch (MalformedURLException e) { e.printStackTrace();}
        catch (IOException e){ e.printStackTrace(); }
    }
}
上述のプログラムは万能ではありません。大抵のWebサイトからWebページにアクセスできますが、 できないページもあります。また、日本語の符号の違いの吸収を行う処理はしていません。 必要に応じて、22行目のInputStreamReaderの第二引数で日本語等の符号の文字列を指定することで 文字化けに対応できるかと思います。

    $ javau GetWebContent
    URLを入力してください > https://www.tut.ac.jp/
としたときの結果の一部(先頭付近)は以下のようです。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

<meta http-equiv="Content-Script-Type" content="text/javascript" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="author" content="国立大学法人豊橋技術科学大学" />

SwingからURLにアクセスし、アプリケーション窓にWebページを表示してみる

前節でURLへのアクセス方法を示しました。これを更に推し進め、 SwingのGUIを使い、シンプルなWebページを表示するプログラムの例 を紹介します。 ここで利用するのは、AWTではなく、Swingで導入されたイベントです。 具体的には、 HyperlinkEventを利用します。 これは、 HyperlinkListenerというリスナーインタフェース を実装することでイベントを取得できるようになります。 取得できるイベントは ACTIVATED, ENTERED, EXITEDの3種類で で定義されています。 また、HyperlinkEventからは、getUrl()メソッドでURLを取得することができます。 さらに、HyperlinkEventには派生クラスのイベントが幾つか定義されています。 そのひとつが HTMLFrameHyperlinkEventです。

一方、Swingクラスの JEditorPaneクラス では、そのペインで読み込まれた文書(Webページ)の 内容を getDocumentメソッド で取得することができます。これは、JEditorPaneクラスが JTextComponentクラス の派生クラスで、JTextComponentクラスにgetDocumentメソッド があるためです。 getDocumentメソッドで得られた文書は HTMLDocumentクラスにキャストできます。 その結果、HTMLFrameHyperlinkEventとして処理をすることができます。
SimpleLinkListener.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/* プログラム7-2 Java Swing (by Marc Loy他、Oreilly)より。
JEditorPaneを使ったハイパーリンクのリスナー。ハイパーリンク上にマウスが来ると
カーソルを変える。また、ハイパーリンクがクリックされると、そのURLのWebページをロードする*/

import java.io.FileNotFoundException;/* ファイルが見つからない例外 */
import javax.swing.JEditorPane;/* 編集可能なペイン */
import javax.swing.JTextField;/* テキストフィールド */
import javax.swing.JLabel;/* ラベル */
import javax.swing.event.HyperlinkEvent;/* ハイパーリンク・イベント */
import javax.swing.event.HyperlinkListener;/* ハイパーリンクのリスナー */
import javax.swing.text.html.HTMLDocument;/* HTMLをモデル化した文書クラス */
import javax.swing.text.html.HTMLFrameHyperlinkEvent;/* フレーム中でハイパーリンク・イベントをとらえる */

public class SimpleLinkListener implements HyperlinkListener {

    private JEditorPane pane; /* HTMLを表示するペイン */
    private JTextField  urlField;/* 現在表示されているURLを表示するテキストフィールド */
    private JLabel statusBar; /* リンク先の表示オプション用 */

    public SimpleLinkListener(JEditorPane jep, JTextField jtf, JLabel jl) {/* コンストラクタ */
        pane = jep;
        urlField = jtf;
        statusBar = jl;
    }
    public SimpleLinkListener(JEditorPane jep) {/* コンストラクタ:オーバーロード */
        this(jep, null, null);
    }

    public void hyperlinkUpdate(HyperlinkEvent he) {/* ハイパーリンクイベントの更新 */
        HyperlinkEvent.EventType type = he.getEventType();/*イベントタイプ */
        if (type == HyperlinkEvent.EventType.ENTERED) {
            /* ハイパーリンク上にカーソルが来た!*/
            if (statusBar != null) {/* ステータスバーにテキストを表示 */
                statusBar.setText(he.getURL().toString());/* URLをテキスト文字列に変換し表示 */
            }
        }
        else if (type == HyperlinkEvent.EventType.EXITED) {
            /* ハイパーリンクからカーソルが出た*/
            if (statusBar != null) {
                statusBar.setText(" "); /* ステータスバーのテキストをクリアする */
            }
        }
        else if (type == HyperlinkEvent.EventType.ACTIVATED) {
            /* ハイパーリンクがクリックされた*
            /* イベントにジャンプ。URLを取出し、nullでなければ
                そのWebページにスイッチし、"site url"ラベルを更新 */
            if (he instanceof HTMLFrameHyperlinkEvent) {
                /*HTMLFrame内でのHyperlinkEventの場合の処理*/
                HTMLFrameHyperlinkEvent  evt = (HTMLFrameHyperlinkEvent)he;
                HTMLDocument doc = (HTMLDocument)pane.getDocument();/*ペインから文書を*/
                doc.processHTMLFrameHyperlinkEvent(evt);/* ハイパーリンクイベント処理 */
            } 
            else {
                try {
                    pane.setPage(he.getURL());/* ハイパーリンクからURLを入手し、ペインにセット */
                    if (urlField != null) {/* 同時にテキストフィールドにURLを表示 */
                        urlField.setText(he.getURL().toString());
                    }
                }
                catch (FileNotFoundException fnfe) {
                    pane.setText("ファイルをオープンできません: <tt>" + 
                        he.getURL() + "</tt>.<hr>");
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
ここで定義した、ハイパーリンクからgetURLメソッドを通してインターネット(ネットワーク) を利用するわけだが、これを呼び出すメインプログラム(Swingのフレーム→ペインを含むアプリケーション) が必要となります。 その例が以下のようです。
MiniBrowser.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/* プログラム7-3 ミニWebブラウザ */
import javax.swing.JFrame;/* フレーム */
import javax.swing.JEditorPane;/* エディタ・ペイン */
import javax.swing.JPanel;/* パネル */
import javax.swing.JTextField;/* テキストフィールド */
import javax.swing.JLabel;/* ラベル */
import javax.swing.JScrollPane;/* スクロールペイン */
import java.awt.event.ActionListener;/* アクションリスナー */
import java.awt.event.ActionEvent;/* アクションイベント */
import java.awt.BorderLayout;/* ボーダーレイアウト */
import java.io.File;/* Fileクラス */

public class MiniBrowser extends JFrame {

    private JEditorPane jep;/* エディタ・ペイン */

    public MiniBrowser(String startingUrl) { 
        /* フレームを表示 */
        super("MiniBrowser");/* 基底クラスのJFrameのコンストラクタを呼ぶ */
        setSize(600,400);/* デフォルトのブラウザのサイズ */
        setDefaultCloseOperation(EXIT_ON_CLOSE);/* 窓を閉じると終了 */

        /* スクリーン等のセットアップ、テキストフィールド、ラベル、リンク */
        JPanel urlPanel = new JPanel();/* JPanelクラスのオブジェクト生成 */
        urlPanel.setLayout(new BorderLayout());/* レイアウト設定 */
        JTextField urlField = new JTextField(startingUrl);/* テキストフィールド設定 */
        urlPanel.add(new JLabel("Site: "), BorderLayout.WEST);/* ラベルをパネル左に */
        urlPanel.add(urlField, BorderLayout.CENTER);/* テキストフィールドを中央に */
        final JLabel statusBar = new JLabel(" ");/* ステータスバーをラベルとして作成 */

        jep = new JEditorPane();/* エディタ用ペインを作成 */
        jep.setEditable(false);/* 重要:これでハイパーリンクが使える */
        try {
            jep.setPage(startingUrl);/* ペインにページを設定 */
        }
        catch(Exception e) {
            statusBar.setText("開始ページを表示できません。ブランクとします。");
        }
        JScrollPane jsp = new JScrollPane(jep); /* スクロールペインを作成 */

        /* GUIコンポーネントをペインに設定 */
        getContentPane().add(jsp, BorderLayout.CENTER);/* スクロールペイン:中央 */
        getContentPane().add(urlPanel, BorderLayout.NORTH);/* URLを上部 */
        getContentPane().add(statusBar, BorderLayout.SOUTH);/* ステータスバーを下部 */

        /* イベントハンドラーの設定 */
        urlField.addActionListener(new ActionListener() {/* アクションリスナー */
            public void actionPerformed(ActionEvent ae) {
                try {
                    jep.setPage(ae.getActionCommand());/* ページにアクションコマンド設定 */
                }
                catch(Exception e) {
                    statusBar.setText("エラー: " + e.getMessage());
                }
            }
        });
        /* ハイパーリンクリスナーをエディタ・ペインに設定 */
        jep.addHyperlinkListener(new SimpleLinkListener(jep, urlField, statusBar));
    }

    public static void main(String args[]) {
        String url = "";
        final String urlDefault = "http://www.kde.cs.tut.ac.jp/~aono/";/* デフォルトURL */
        if (args.length == 1) {
            url = args[0];
            if (!(url.startsWith("http:") || url.startsWith("file:"))) {
                if (url.startsWith("/")) {/* スラッシュからはじまるときはfile:をつけてみる */
                    url = "file:" + url;
                }
                else {
                    try {/* 相対パスと思う */
                        File f = new File(url);
                        url = f.toURI().toURL().toString();/* まずURIにしてからURLに変換 */
                    }
                    catch (Exception e) { url = urlDefault; }
                }
            }
        }
        else { url = urlDefault; }
        MiniBrowser mb = new MiniBrowser(url);/*上で定義したクラスのオブジェクト作成 */
        mb.setVisible(true);/* 表示 */
    }
}
34行目のJEditorPaneのsetPageメソッドでネットワーク上のWeb文書がダウンロードされる。 ダウンロードでエラーがある場合のため、 try~catchの例外処理でダウンロード中のエラー処理を行います。 32行目に書いているように、JEditorPaneでハイパーリンク処理を行う場合、JEditorPaneを編集不可能にしておく必要があります。 これは編集可能とすると、テキスト自体をアクションイベントで取れるようにすることと、 ハイパーリンクのイベントとを両方行おうとすると混乱が起こるためで、 どちらかのイベントに専念する必要があるためです。 マイクロソフトのオフィスでリンクが書かれたテキストを編集中に通常ダブルクリックしても何も起こらないのと同様です。 ただし、マイクロソフトオフィスでは、コントロールキーを押しながらクリックすれば、一応ハイパーリンク先にアクセスできることはご存知かと思います。

実行例は、以下のとおりです。 右はハイパーリンクをクリックし、この授業のシラバスを表示した例です。

なお、一般にはJavaScriptなどを含むページが多く、 このSwingでのミニブラウザでは、そこまでのレンダリングはサポート していないことを付記しておく。

データグラム(UDP)とソケットによるサーバ・クライアント間でのパケット送受信

これまでは、ハイレベルのネットワーク機能を見てきました。 ここでは、より低レベルのネットワーク機能をデータグラムを用いたパケット単位での サーバとクライアントでのパケット通信例を紹介します。 TCP (Transmission Control Protocol)では、 サーバとクライアントでネットワークコネクションを確立してからソケットストリームでデータをやりとりします。 この例は、UDP (User Datagram Protocol)を用います。 TCPと異なり、UDPの場合は、コネクションの確立は仮定しません。 したがって本当にパケットの送受信を確認しないという欠点がありますが、その代り、 インターネット越しでの「チャット」のようなアプリケーションを高速に実現できるという性質があります。 また、音声や映像などのマルチメディアに代表されるストリームデータをマルチキャストする場合にも重宝します。

たとえて言うと、TCPによる通信は、電話をしている状態で、 UDPによる通信は、郵便とか、電子メールでやりとりする感じに似ています。 電子メールのやりとりでは、相手がそこにいてすぐに返事するとは限りません。 サーバもクライアントも何かきたら処理すればよく、その点で 気楽な通信手段といえます。 では、UDPでのサーバとクライアントのプログラムを紹介します。 中心的なクラスは、 DatagramPacketクラスDatagramSocketクラス です。
Server.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/* プログラム 7-4  Server.java 
データグラムを利用したサーバ・クライアアント プログラム例のサーバ部分*/
import java.io.IOException;/* IO例外 */
import java.net.DatagramPacket;/* DatagramPacketクラス */
import java.net.DatagramSocket;/* DatagramSocketクラス */
import java.net.SocketException;/* ソケット例外クラス */
import java.awt.*; /*BorderLayout, MenuShortcut */
import javax.swing.*; /* JFrame,JScrollPane,Jmenu,JTextArea,JMenuBar,JMenuItem,SwinUtilities */
import java.awt.event.ActionListener;/* アクションイベントのリスナー */
import java.awt.event.ActionEvent;/* アクションイベント */

public class Server extends JFrame implements ActionListener {
    private JTextArea displayArea; /* 受信パケットを表示するテキスト領域 */
    private DatagramSocket socket; /* クライアントと連絡するソケット */
    protected String[] fileLabels = new String[]{"終了"};
    protected JMenuBar menubar;/*メニュバー用の変数*/
    final int PACKET_SIZE = 1024;/* 1024バイト */ 
    final int PORT_NUMBER = 5000; /* ポート番号 */
    final int FRAME_WIDTH = 400;/* サーバフレームの横サイズ (初期値)*/
    final int FRAME_HEIGHT = 300;/* サーバフレームの縦サイズ (初期値)*/

    public void actionPerformed(ActionEvent event){/*アクションイベントの処理*/
        String command = event.getActionCommand();/*アクションイベント取得*/
        if (command.equals("終了")) System.exit(0);/*終了*/
    }

    public Server(){
        JMenu fileMenu = new JMenu("ファイル");/* ファイルメニュ */
        for (int i=0 ; i < fileLabels.length ; i++ ){
            JMenuItem mi = new JMenuItem(fileLabels[i]);
            mi.setActionCommand(fileLabels[i]);/* アクションコマンド設定 */
            mi.addActionListener(this);/* アクションリスナを追加 */
            fileMenu.add(mi);/* JMenuItemを追加 */
        }
        menubar = new JMenuBar();/*メニュバーオブジェクト生成*/
        menubar.add(fileMenu);/*メニュバーにファイルメニュを追加*/

        displayArea = new JTextArea(); /* テキスト領域 */
        JScrollPane sp = new JScrollPane( displayArea );/* スクロールペイン */
        add( sp, BorderLayout.CENTER );/* 中央にスクロールペインを配置 */
        setSize( FRAME_WIDTH, FRAME_HEIGHT ); /* ウィンドウ生成 */
        setJMenuBar(menubar);/* メニュバーの設定 */
        setVisible( true ); /* ウィンドウ表示 */

        try {/* DatagramSocketを生成し送受信を制御 */
            socket = new DatagramSocket( PORT_NUMBER );/* ポートを使用 */
        }
        catch ( SocketException socketException ) {/* ソケット例外 */
            socketException.printStackTrace();/* 例外トレース */
            System.exit( 1 );/* 異常終了 */
        }
    }

    /* クライアントからのパケット到着を待機し、データをクライアントに送信し表示 */
    public void waitForPackets(){
        while ( true ) {
            try { /* パケット受信後、コンテンツを表示、コピーをクライアントに戻す */
                byte[] data = new byte[ PACKET_SIZE ]; /* パケット用バイト配列 */
                DatagramPacket receivePacket = new DatagramPacket( data, data.length );
                socket.receive( receivePacket ); /* パケット受信待機 */

                /* 受信パケット情報を表示 */
                displayMessage( "\n[サーバ]パケット受信:" + 
                    "\n[サーバ] アドレス: " + receivePacket.getAddress() + 
                    "\n[サーバ] ポート: " + receivePacket.getPort() + 
                    "\n[サーバ] 長さ: " + receivePacket.getLength() + 
                    "\n[サーバ] 内容:\n\t" + new String( receivePacket.getData(), 
                    0, receivePacket.getLength() ) );

                sendPacketToClient( receivePacket ); /* パケットをクライアントに送信 */

            } /* end try */
            catch ( IOException ioException ){
                displayMessage( ioException + "\n" );
                ioException.printStackTrace();
            } /* end catch */
        } /* end while */
    } /* end method waitForPackets */

    /* パケットをクライアントにエコーする */
    private void sendPacketToClient( DatagramPacket receivePacket ) throws IOException {
        displayMessage( "\n\nクライアント:エコー..." );
        /* データグラム・パケットを生成し送信する */
        DatagramPacket sendPacket = new DatagramPacket( 
            receivePacket.getData(), receivePacket.getLength(), 
            receivePacket.getAddress(), receivePacket.getPort() );
        socket.send( sendPacket ); /* パケットをクライアントに送信 */
        displayMessage( "[サーバ]: パケット送信\n" );
    } 

    private void displayMessage( final String messageToDisplay ){
        SwingUtilities.invokeLater(/* SwingUtilities.invokeLater呼び出す */
            new Runnable() {/* スレッド生成の内部クラス */
                public void run() {
                    displayArea.append( messageToDisplay );
                }
            } /* 内部クラス */
        ); 
    } 
} 
一方、クライアント側は以下のように与えます。クライアントでは JTextfieldで入力されたテキストをアクションイベントとして データグラムでサーバに送信すると仮定しています。
Client.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/* プログラム 7-5: Client.java 
データグラムを利用したサーバ・クライアアント プログラム例のクライアント部分*/
import java.io.IOException;/* IO例外 */
import java.net.DatagramPacket;/* DatagramPacketクラス */
import java.net.DatagramSocket;/* DatagramSocketクラス */
import java.net.InetAddress;/* InetAddressクラス */
import java.net.SocketException;/* ソケット例外クラス */
import java.awt.BorderLayout;/* BorderLayoutクラス */
import java.awt.event.ActionListener;/* アクションイベントのリスナー */
import java.awt.event.ActionEvent;/* アクションイベント */
import javax.swing.*;/* JFrame, JScrollPane, JTextArea, JTextField, SwingUtilities */

public class Client extends JFrame implements ActionListener  {
    private JTextField enterField; /* テキストフィールド(1行分) */
    private JTextArea displayArea; /* 受信パケットを表示するテキスト領域 */
    private DatagramSocket socket; /* サーバと連絡するソケット */
    final int PORT_NUMBER = 5000; /* ポート番号 */
    final int FRAME_WIDTH = 400;/* クライアントフレームの横サイズ (初期値)*/
    final int FRAME_HEIGHT = 300;/* クライアントフレームの縦サイズ (初期値)*/
    final int PACKET_SIZE = 1024;/* 1024バイト */ 

    public void actionPerformed( ActionEvent event ) {/* アクションイベントの処理 */
        try {
            String message = event.getActionCommand();/* テキストフィールドからコマンド */
            displayArea.append( "\n送信パケット: " +    message + "\n" );

            byte[] data = message.getBytes(); /* テキストフィールドをバイトデータ配列に変換 */
            DatagramPacket sendPacket = new DatagramPacket( /* パケット生成 */
                data, data.length, InetAddress.getLocalHost(), PORT_NUMBER );
            socket.send( sendPacket ); /* パケット送信 */
            displayArea.append( "パケット送信完了\n" );
            displayArea.setCaretPosition( displayArea.getText().length() );/* カーソル位置セット */
        } 
        catch ( IOException ioException ) {
            displayMessage( ioException + "\n" );/* ソケット送信で例外発生時 */
            ioException.printStackTrace();
        } 
    }

    public Client() {/* クライアントのコンストラクタ */
        enterField = new JTextField( "メッセージをここにタイプしてください" );
        enterField.addActionListener(this);/* アクションリスナ設定 */
        add( enterField, BorderLayout.NORTH );/* テキストフィールドは上部に配置 */
        displayArea = new JTextArea(); /* テキスト領域 */
        JScrollPane sp = new JScrollPane( displayArea );/* スクロールペイン */
        add( sp, BorderLayout.CENTER );/* 中央にスクロールペインを配置 */
        setSize( FRAME_WIDTH, FRAME_HEIGHT ); /* ウィンドウ生成 */
        setVisible( true ); /* ウィンドウ表示 */

        try {/* DatagramSocketを生成しパケットを送受信 */
            socket = new DatagramSocket();
        }
        catch ( SocketException socketException ) {/* ソケット例外 */
            socketException.printStackTrace();
            System.exit( 1 );
        }
    }

    /* サーバからのパケット到着を待機し、データをサーバから受信し表示 */
    public void waitForPackets(){
        while ( true ) {
            try { /* パケット受信後、コンテンツを表示 */
                byte[] data = new byte[ PACKET_SIZE ]; /* パケット用バイト配列 */
                DatagramPacket receivePacket = new DatagramPacket( data, data.length );
                socket.receive( receivePacket ); /* パケット受信待機 */

                /* 受信パケット情報を表示 */
                displayMessage( "\n[クライアント] パケット受信:" + 
                    "\n[クライアント] アドレス: " + receivePacket.getAddress() + 
                    "\n[クライアント] ポート: " + receivePacket.getPort() + 
                    "\n[クライアント] 長さ: " + receivePacket.getLength() + 
                    "\n[クライアント] 内容:\n\t" + new String( receivePacket.getData(), 
                    0, receivePacket.getLength() ) );
            }
            catch ( IOException exception ) {
                displayMessage( exception + "\n" );
                exception.printStackTrace();
            }  /* end catch */
        } /* end while */
    }/* end method waitForPackets */

    private void displayMessage( final String messageToDisplay ){
        SwingUtilities.invokeLater(/* SwingUtilities.invokeLater呼び出す */
            new Runnable() {/* スレッド生成の内部クラス */
                public void run() {
                    displayArea.append( messageToDisplay );
                }
            } /* 内部クラス */
        ); 
    } 
}
上記で定義したサーバとクライアントを呼び出すメインプログラム例を 以下に示します。
ServerTest.java
1 
2 
3 
4 
5 
6
7
8
9
/* プログラム 7-6: ServerTest.java サーバーテスト */
import javax.swing.JFrame;
public class ServerTest {
    public static void main( String[] args ){
        Server application = new Server(); /* サーバオブジェクト生成 */
        application.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        application.waitForPackets(); /* サーバアプリケーションを実行 */
    } /* end main */
} /* end class ServerTest */
ClientTest.java
1 
2 
3 
4 
5 
6
7
8
9
/* プログラム 7-7: ClientTest.java クライアントテスト */
import javax.swing.JFrame;
public class ClientTest {
    public static void main( String[] args ){
        Client application = new Client(); /* クライアントオブジェクト生成 */
        application.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        application.waitForPackets(); /* クライアントアプリケーションを実行 */
    } /* end main */
}  /* end class ClientTest */
以下では、サーバを1プロセス、クライアントを2つ動かして パケットを送受信した例です。 実行したコマンドとともに示します。

$ javacu Server.java
$ javacu Client.java
$ javacu ServerTest.java
$ javacu ClientTest.java
$ javau ServerTest &
[1] 6336

$ javau ClientTest &
[2] 6368

$ javau ClientTest &
[3] 6416
簡単なサーバ・クライアント型対話システムのコンパイルと実行(クライアントは2つ以上実行させる)

簡単なサーバ・クライアント型対話システム実行画面

TCPとソケットによるサーバ・クライアント間でのパケット送受信

UDPでは、サーバとクライアントの間でコネクションを確立することなく、 データをやりとりできました。 一方、TCP (Transmission Control Protocol)では、 コネクションの確立を前提とします。 HTTP, FTP, SSH, TelnetなどはTCPの上位プロトコルの代表です。
ChatServer.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/*
	プログラム7-8: ChatServer.java
	http://cs.lmu.edu/~ray/notes/javanetexamples/を参考
*/
import java.io.*;/* 入出力 */
import java.net.*;/* ネットワーク */
import java.util.*;/* ArrayListなど */
import java.awt.*;/* AWT */
import javax.swing.*;/* Swing */
import java.awt.event.*;/* AWT各種イベント */

/*
 簡素なマルチスレッドチャットルーム・サーバ.  
 以下のプロトコルとする。
(1)サーバはクライアントに "SUBMITNAME"というテキストを送る
(2)ユニークな名前が各クライアントから返ってくるまでひたすら待つ
(3)クライアントに"NAMEACCEPTED"という返事を送る
(4)クライアントからテキストが送られると、ヘッダに、
"MESSAGE "という文字列がついてブロードキャストされる。
 */
public class ChatServer extends JFrame implements KeyListener, ActionListener {

    private Socket socket;/* ソケット変数保持 */
    private JTextArea displayArea; /* 受信したパケットを表示する領域 */
    protected String[] fileLabels = new String[]{"終了" };/* ファイルラベル */
    protected int[] mnemonics = new int[]{ KeyEvent.VK_Q};
    protected JMenuBar menubar;/*メニュバー用の変数 */
    public void actionPerformed(ActionEvent event){/*アクションイベントの処理*/
        String command = event.getActionCommand();/*アクションイベント取得*/
        if (command.equals("終了")) quit();/*終了*/
    }
    public void quit(){ System.exit(0); }
    public void keyPressed(KeyEvent e){/* キーが押された時のイベント */
        if (e.getKeyCode()==KeyEvent.VK_ESCAPE) quit();/* VK_ESCAPEはキーボードのEscapeボタン */
    }
    public void keyReleased(KeyEvent e){}/* キーがリリースされたとき */
    public void keyTyped(KeyEvent e){}/* キーがタイプされたとき */

    public void init() throws Exception {
        JMenu fileMenu = new JMenu("ファイル");/* ファイルメニュ */
        for (int i=0 ; i < fileLabels.length ; i++ ){
            JMenuItem mi = new JMenuItem(fileLabels[i], mnemonics[i]);
            mi.setActionCommand(fileLabels[i]);/* アクションコマンド設定 */
            mi.addActionListener(this);/* アクションリスナを追加 */
            fileMenu.add(mi);/* JMenuItemを追加 */
        }
        menubar = new JMenuBar();/*メニュバーオブジェクト生成 */
        menubar.add(fileMenu);/*メニュバーにファイルメニュを追加*/
        addKeyListener(this);/* キーボードのリスナーを設定 */

        displayArea = new JTextArea(); /* JTextArea (Swing) */
        JScrollPane sp = new JScrollPane( displayArea );/* JScrollPane (Swing) */
        add( sp, BorderLayout.CENTER );/* BorderLayoutの中央にJScrollPaneを */
        setSize( 400, 300 ); /* ウィンドウサイズ */
        setJMenuBar(menubar);/* メニュバー */
        setVisible( true ); /* ウィンドウを可視化する */

        ServerSocket listener = new ServerSocket(PORT);/* サーバソケット生成 */
        try {
            while (true) {
                socket = listener.accept();/* クライアントからの応答をひたすら待つ */
                Handler handler = new Handler(socket, writers, names,displayArea);
                handler.start();/* ハンドラー(スレッドとプロトコル生成) の開始 */
            }
        } finally { listener.close();/* サーバソケットを閉じる */ }
    }

    private static final int PORT = 9001;/* TCPで使うポート番号(適当) */
    private static HashSet<String> names = 
        new HashSet<String>();/*クライアントの名前 */
    private static HashSet<PrintWriter> writers = 
        new HashSet<PrintWriter>();/* 全クライアントのプリントライタ (ブロードキャスト)*/

    public static void main(String[] args)  {
        ChatServer cs = new ChatServer();/* サーバ */
        try { cs.init(); }/* ハンドラーを起動 */
        catch(Exception e){e.printStackTrace();}
   }
}
Handler.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/*
    プログラム7-9: Handler.java
    http://cs.lmu.edu/~ray/notes/javanetexamples/を参照
*/
import java.io.*;
import java.net.Socket;
import java.util.*;
import javax.swing.*;

public class Handler extends Thread {
    /* シングル・クライアントに対処し、メッセージをブロードキャスト */
    private String name;/* 名前 */
    private Socket socket;/* ソケット */
    private BufferedReader in;/* バッファ-ドリーダ */
    private PrintWriter out;/* プリントライタ */
    private HashSet<PrintWriter> writers;/* プリントライタのハッシュセット */
    private HashSet<String> names;/* 名前のハッシュセット */
    private JTextArea displayArea; /* displayArea */

    private void displayMessage( final String messageToDisplay ){
        SwingUtilities.invokeLater(
            new Runnable() {
                public void run() {// updates displayArea
                    displayArea.append( messageToDisplay ); // display message
                } // end method run
            } // end anonymous inner class
        ); // end call to SwingUtilities.invokeLater
    } // end method displayMessage

    public Handler(Socket socket, HashSet<PrintWriter> writers, 
        HashSet<String> names, JTextArea displayArea) {
        this.socket = socket;/* ソケット */
        this.writers = writers;/* プリントライター */
        this.names = names;/* 名前 */
        this.displayArea = displayArea;/* displayArea */
    }

    /* サーバ側のプロトコル */
    public void run() {
        try {
            /* I/Oストリーム設定 */
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);

            /* ユニークな名前の入力待ち。synchronizedでロックして調査 */
            while (true) {
                out.println("SUBMITNAME");
                name = in.readLine();/* 名前の入力 */
                if (name == null) { return;}
                synchronized (names) {/* ロック */
                    if (!names.contains(name)) { names.add(name); break; }
                }
            }

            /* 名前はクリアされた状態 */
            out.println("NAMEACCEPTED");
            writers.add(out);

            /* メッセージをクライアントに送信 */
            while (true) {
                String input = in.readLine();/* メッセージを読む */
                if (input == null) {  return;  }

                Date date = new Date();/* 日付 */
                String c = new String(input);/* メッセージ本体 */
                int p = socket.getPort();/* ポート番号をゲット */
                displayMessage( "\n[サーバ]TCPIP バッファ受信:" + 
                    "\n[サーバ] アドレス: " + socket.getInetAddress() + 
                    "\n[サーバ] ポート: " + p + 
                    "\n[サーバ] 長さ: " + input.length() + 
                    "\n[サーバ] 日付: " + date + 
                    "\n[サーバ] 内容:\n\t" + c );
                 for (PrintWriter writer : writers) {/* すべてのプリントライタにブロードキャスト */
                    writer.println("MESSAGE " + name + ": " + input);
                }
            }
        } catch (IOException e) {
            return; // クライアント窓が閉じられた
        } finally {
            if (name != null) { names.remove(name);}/* 名前の削除 */
            if (out != null) {writers.remove(out);}/* プリントライターの削除 */
           try { socket.close(); }/* ソケットのクローズ */
            catch (IOException e) {}
        }
    }
}
ChatClient.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
	プログラム7-10: ChatClient.java
	http://cs.lmu.edu/~ray/notes/javanetexamples/を参照
*/

import java.io.*;/* 入出力 */
import java.net.*;/* ネットワーク */
import javax.swing.*;/* Swing */
import java.awt.event.*;/* AWT各種イベント */

/* サーバのプロトコルに合わせたクライアントのチャット制御 */
public class ChatClient extends JFrame implements Runnable, ActionListener {
    Thread chatThread;/* スレッド */
    BufferedReader in;/* バッファードリーダ */
    PrintWriter out;/* プリントライタ(ブロードキャスト) */
    JFrame frame;/* チャットフレーム */
    JTextField textField = new JTextField(40);/* テキストアリア(1行メッセージ)*/
    JTextArea messageArea = new JTextArea(8, 40);/* テキストアリア */
    Socket socket;/* サーバとの通信用ソケット */

    public void actionPerformed(ActionEvent e) {/* テキストエリアに入力されるとキックイン!*/
        out.println(textField.getText());/* 入力されたテキストをブロードキャスト */
        textField.setText("");
    }
    public ChatClient(String name) {
        super(name);/* JFrameのタイトル設定 */
        frame = this;/* JFrame:あとのJDialogで使用 */
        textField.setEditable(false);/* テキスト領域のエディット可能に */
        messageArea.setEditable(false);/* メッセージ領域をエディット可能に */
        getContentPane().add(textField, "North");/* テキスト領域は「北」に配置 */
        getContentPane().add(new JScrollPane(messageArea), "Center");/* スクロールペインは中央に*/
        pack();
        textField.addActionListener(this);/* リスナーの設定*/
    }

    public String getServerAddress() {/* サーバのアドレス待ち(localhostもOK) */
        return JOptionPane.showInputDialog(
            frame,
            "サーバのIPアドレス(ホスト名)を入力してください:",
            "ようこそチャッターへ",
            JOptionPane.QUESTION_MESSAGE);
    }

    public String getName() {/* ユニークな名前の入力待ち */
        return JOptionPane.showInputDialog(
            frame,
            "あなたの名前をインプットしてください:",
            "名前の選択パネル",
            JOptionPane.PLAIN_MESSAGE);
    }

    public void start() {/* サーバにコネクト、スレッド開始 */
        chatThread = new Thread(this);   // Create a thread
        chatThread.start(); // Start the thread.
    }

    public void run()  {/* コネクション設定:プロトコルに従う */
        String serverAddress = getServerAddress();/* サーバのIPアドレス(ホスト名)入力待ち */
        if (serverAddress == null) return;/* 入力されない間は何もしない */
        try {
            socket = new Socket(serverAddress, 9001);/* ポート9001で通信 */
            in = new BufferedReader(new InputStreamReader(
            socket.getInputStream()));/* 入力準備 */
            out = new PrintWriter(socket.getOutputStream(), true);/* 送信準備 */

            // Process all messages from server, according to the protocol.
            while (true) {
                  String line = in.readLine();/* サーバからの受信テキストでプロトコル振り分け */
                  if (line.startsWith("SUBMITNAME")) {/* ユニークな名前を入れる段階 */
                    out.println(getName());
                  } else if (line.startsWith("NAMEACCEPTED")) {/* 名前が受理された段階 */
                    textField.setEditable(true);
                  } else if (line.startsWith("MESSAGE")) {/* メッセージを送信できた段階 */
                    messageArea.append(line.substring(8) + "\n");
                  }
               }
        }
        catch(IOException e) { e.printStackTrace(); }
  }

  public static void main(String[] args) {/* クライアントのmainプログラム */
    ChatClient client = new ChatClient("Swing簡易チャットプログラム");
    client.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    client.setVisible(true);
    client.start();
  }
}
実行する場合、まず、サーバをバックグランドで動かし、引き続き、 1つ以上のクライアントをバックグラウンドで動かします。

$ javacu ChatServer.java
$ javacu ChatClient.java

$ javau ChatServer &
[1] 7244

$ javau ChatClient &
[2] 6796

$ javau ChatClient &
[3] 7192
TCPIPによる簡単なサーバ・クライアント型チャットシステムの実行例


クライアントを立ち上げると、プログラム7-10の36行目からはじまる getServerAddress()関数が呼び出され以下のような画面になります。



ここではローカルホスト(127.0.0.1)を入力しています。 この入力が正しいものであれば、 以下のような画面で、ユニークなチャット名が要求されます。 2つのクライアントで、それぞれ入力します。



この名前が(HashSetクラスのキーとして)ユニークであれば、 やっと、チャットスタートです。 1つ目のクライアントから、あるメッセージがあり、2つ目のクライアントが入力を開始した状態が以下のようです。



2つ目のクライアントが入力を終えたとき、サーバの画面は以下のようです。



終了するときは、まずすべてのクライアントの窓を閉じ、サーバの「ファイル」メニュから 「終了」を選択し、終了します。

TCPとUDPの比較

TCPとUDPの簡単な比較をします。Java言語でTCPとUDPを使い分けるクラスは以下のようです。



ソケットという言葉は両方に共通しますが、Socketクラス自体はTCP通信で使用します。 TCPの場合、以下の左図(日経BP ITO Pro社から引用)のように、1対1の通信を確立して通信を行います。これに対して、UDPでは、右図のように、1対多の通信が可能です。 それぞれ長所・短所を持っていますが、どちらも欠かせない手段として位置づけられています。

TCPの概念図 UDPの概念図

サーバサイドJava

Javaが現在、もっとも実用的に使用されているのは サーバ側で各種のデータ処理を行うためです。 このとき、サーバ側でのJava言語の役割は 幾つかあります。 代表的なものは、 前節のネットワークプログラミングでは、4番目の役割を、 クライアントとサーバの間での通信プログラムを通して例示しました。 ここで説明するサーバサイドJavaでは、主として1番目から3番目の役割を Tomcatと呼ばれるJava言語用のWebサーバを通して 実例を用いて解説することを目的とします。

TomcatのMac/Darwinでのインストール

ここでは、サーバ=クライアントと仮定し、各個人のクライアントで Tomcatを動かし、各個人のクライアントのWebブラウザ(Firefox等)で Tomcatを介するJSP (Java Server Pages)による プログラムの起動を体験してもらいます。 なお、JSPはTomcatにより自動的にコンパイルされてサーブレットに変化します。

Tomcatのインストールはいたって簡単です。 特別なインストールプログラムを動かす必要はありません。 適当なディレクトリに一連の階層的なTomcatフォルダをコピーするだけです。

  1. まずTomcatバージョン7 (7.0.96) (注:Tomcatバージョン8以降は、 以下で述べる各種設定方法が変更されていますので、ここでは使用しません)を、Apache Tomcat 7.0のWebサイトかダウンロードしてください。 具体的には、 Binary DistributionsのCoreの中からzipまたはtar.gzのどちらから選択してダウンロードしてください。 次に、それを解凍してください。 するとapache-tomcat-7.0.96というフォルダ(ディレクトリ)ができますので、 これを各自のホーム直下に移動してください。 以下のようなフォルダがあるかと思います。(他にもapache-tomcat-7.0.96以下にありますが、 以下で説明する配備(deploy)で修正する必要なフォルダと、 実行で使うフォルダだけ強調して表示しています)。 binフォルダ直下にTomcatのスタートアップとシャットダウンを行う startup.shならびにshutdown.shという名前のシェルスクリプトがあることに着目してください。 また、サーバサードJavaで、 必須となるサーブレットクラスを含むライブラリがlibフォルダ直下にあるservlet-api.jarという名前のファイルになります。



  2. 次に、これに以下のフォルダを新たに作成します。 結果として、以下のような構成になります。



  3. 以下のライブラリをlibフォルダに追加してください。 これらのライブラリは本授業の課題等で使用する可能性があるためです。

  4. 以下のコンフィグ・ファイルをconf/Catalina/localhostの下に置いてください。

  5. 以下のZIPファイルをダウンロードし、そのままdataフォルダ以下で展開してください。 ZIPがうまく展開できない場合は、 TextArea から、見えるTextAreaフォルダとその中身を全部(ひとつずつ)ダウンロードして、dataフォルダ以下に展開してください。

    ここまでの設定で以下のような構造になっていればOKです。



  6. 環境変数の設定(一度実行すればいいものです) 以下のように環境変数を設定しておいてください。具体的には、 CATALINA_HOMEという環境変数を設定してください。 このため、
    $ export CATALINA_HOME=/home/1/ma002/apache-tomcat-7.0.96
    をターミナル窓で(一度)実行しておいてください。
  7. apache-tomcat-bin以下の実行ファイルのモードの確認
    binフォルダに移動し、ls -l コマンドでシェルスクリプトが実行可能モードになっているか確認してください。 もし、以下の様に、シェルファイルが実行可能でない場合



    シェルファイルを実行可能モードに変更しておいてください。

    $ chmod +x *.sh





    こうなっていればOKです。
なお、以下では、Eclipseのような開発環境(IDE)は使わない場合の 手作業でのサーバサイドJavaの開発手順を示します。 Eclipseは、デバッグ等ではとても便利ですが、それに慣れすぎると、 Eclipseのない環境下でプログラムが動作しなかったり、文字化け したりすることがありますので、これらは自己責任のもとで使ってください。

Tomcatを使ったサーバサイドJava開発手順例

上に示した図にある開発手順例を述べます。 ここでは、JSP (Java Server Page)サーブレットの連携でサーバサイドJavaを例を通して学んでいきます。 以下の説明は大きく7つのステップからなります。
  1. Tomcatの起動
  2. Configuration(conf:コンフィグ・ファイル)の設定
  3. フォルダ(ディレクトリ)作成
  4. ビルドファイル設定 (build.xml)((オプション))
  5. サーブレット、JavaBean(s)等作成 ((オプション))
  6. JSPによる開始画面(index.jsp)等の作成
  7. http://localhost:8080/TextArea/からアクセス
  8. Tomcatの終了
以下は、これを順に述べます。 最初は、JavaのWebサーバであるTomcatをターミナル窓から起動 するところからはじめます。

(1) Tomcatの起動

起動するために、apache-tomcat-7.0.96/binディレクトリに移動し、 実行します。

$ ./startup.sh

このとき、以下のようなメッセージが出るかと思います。



Tomcatが正しくインストールできたかどうかを確かめるため FireFoxを立ち上げ、http://localhost:8080/ とタイプしてください。 以下のような画面が出れば成功です。



(2) Configuration(conf:コンフィグ・ファイル)の設定

ここからは、"TextArea"という名前でアクセスできるためのサーバ側の設定を行います。 設定ファイルの名前とそれを配備する 場所が重要になります。 ここでは名前がTextAreaですので、 以下のようにTextArea.xmlというXMLファイルで記述する必要があります。 XMLはHTMLと似たタグによる記述された半構造データです。 コメントなどはHTMLと同様ですが、開始タグと終了タグが 必ず一致している必要があります。 (注:HTMLの場合は、開始タグと終了タグが一致していなくても、 Webブラウザが賢いため、問題ありません。)

apache-tomcat-7.0.96/conf/Catalina/localhost/TextArea.xml

これは、apache-tomcat-7.0.96/conf/Catalina/localhost以下に置くことが必要です。 "conf"はconfigurationの略です。
TextArea.xml
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
<!--
    File: TextArea.xml (設定データ7-1)
    Author: Masaki Aono
-->

<Context 
	path="/TextArea"
	docBase="/home/1/ma002/apache-tomcat-7.0.96/data/TextArea/jsp" 
	reloadable="true" 
	privileged="true" 
	allowCasualMultipartParsing="true"
 	crossContext="true"
 	backgroundProcessorDelay="1"
 	antiResourceLocking="false"
 	antiJARLocking="false"
 	>
</Context>
コンフィグファイルで重要なのは、<Context>タグ内の、 7行目のpath属性で指定する"/TextArea"と8行目のdocBase属性です。 docBase属性は、実際にサーブレットまたはJSP等で指定される サーバ側のプログラムがどこに配備されているかを示します。 ここでは、 /home/1/ma002/apache-tomcat-7.0.96/data/TextArea/jsp以下に ***.jsp(JSPファイル群)が置いてあることを意味します。 JSPを扱う場合、通常、最初のWebページはindex.jspという名前のファイルとなります。 一方、path属性で指定する"/TextArea"とは、 このWebアプリを実行する際のURLが http://localhost:8080/TextArea/指定されることを表します。 なお、"localhost:8080"はTomcatのデフォルトのポート番号がlocalhostの8080番であることを表します。 従って、docBase属性と合わせると、最初に実行されるのは http://localhost:8080/TextArea/index.jspから実行されることを表します。

(3) フォルダ(ディレクトリ)作成

"TextArea"という名前のWebアプリを作成したい場合、 手作業でフォルダを作成する典型的な図式が、最初に示した図のようで、 apache-tomcat-7.0.96/data/以下のディレクトリにJSP, サーブレット、 クラスファイル等を正しく配備する必要があります。 具体的には、まず、apache-tomcat-7.0.96/data/直下にTextAreaというディレクトリを作成します。 その直下にjspsrcディレクトリ(フォルダ) (注:srcはサーブレットやJavaBeanがあるときのみ必要です)、ならびに build.xmlからなります。 build.xmlもsrcディレクトリが必要なときのみ作成すれば結構です。 jspディレクトリ(フォルダ)には、更に、 必ず必要となるindex.jspなどのJSPファイル群と WEB-INFディレクトリ(フォルダ)を作成します。 WEB-INFも、 srcディレクトリが必要な場合だけのオプションとなります。 このWEB-INFという名前自体は、 Tomcatでサーブレットを配備する場合の伝統的なディレクトリ名です。 ここに、サーブレットやJavaBeanとなるJavaプログラムをビルドファイルなどを通して Webアプリ開発者がコンパイルして得られたクラスファイルを置くフォルダ名となります。

後述するjspフォルダ直下に置くJSPファイル群は、 中身はHTML文書中に、Javaプログラムと、JSP独自のスクリプト( スクリプトレット) (scriptlet) で記述されたテキストファイルです。 スクリプトレットとは、「小さなJavaで記述されたプログラム片」という意味ですが、 場合によっては、かなり大きなプログラムになることもあります。 JSPに限らず、マイクロソフトのASP (Active Server Pages)も同じ発想でHTMLの中に、 プログラム片を挿入して使えます。これは、PHPでホームページを記述する場合も同様で、 シンタックスとして、<php? ...~...?>となるだけです。 JSPは、ユーザがTomcatで開発されたWebアプリにアクセスしたとき、 最初に一度(あるいは、JSP自体を修正したときに)、自動的にコンパイルされます。 勿論JSPファイル群がなくても、サーブレットだけでJavaで書かれたWebアプリは動作可能ですが、 JSPを先頭ページとすることで、サーブレットの開発はぐっとシンプルになります。

(4) ビルドファイル設定 (build.xml): Antを用いる場合

"TextArea"フォルダの直下に置かれるbuild.xmlの中身 は以下のようです。
build.xml(ビルドデータ7-1)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<!-- ===================================================================
<description> 
	Prerequisites:
   		jakarta-ant from http://jakarta.apache.org
	Build Instructions:
   		To compile
        	$ ant compile
</description>
==================================================================== -->

<project name="TextArea" default="compile" basedir=".">

	<property name="src" location="src"/>
	<property name="classes" location="jsp/WEB-INF/classes"/>
	<property name="fileUTF8" value="-encoding UTF-8"/>
	<property name="Tomcat" value="apache-tomcat-7.0.96"/> <!-- Tomcatのバージョン-->
	<property name="prefix" value="/home/1/ma002"/> <!-- 各自のディレクトリで要変更-->

	<target name="compile">
	<javac srcdir="${src}" destdir = "${classes}" includeantruntime="false">
	<compilerarg line="${fileUTF8}" />
	  <classpath> <!-- ここに、Tomcatで使用するライブラリをフルパスで書きます -->
	  <pathelement location="${prefix}/${Tomcat}/lib/servlet-api.jar"/>
	  <pathelement location="${prefix}/${Tomcat}/lib/common-fileupload-1.3.3.jar"/>
	  <pathelement location="${prefix}/${Tomcat}/lib/common-logging-1.2.jar"/>
	  <pathelement location="${prefix}/${Tomcat}/lib/common-io-2.6.jar"/>
	  </classpath>
	</javac>
	</target>

	<target name="clean">
		<delete dir="${classes}"/>
		<mkdir dir="${classes}"/>
	</target>

</project>
なお、上述のbuild.xml内の24~27行目にあるpathelementタグは Javaをコンパイルする際のクラスパスで、 指定したいライブラリが配備されている位置を記述します。 サーブレット・クラスなどをimportしている場合は、 24行目のサーブレット用のライブラリ(jarファイル)を含めることが必須です。 あとはコンパイルに必要なライブラリを適宜追加します。 このbuild.xmlを使ったJavaBeanのコンパイルはAntを使うと、 data/TextAreaフォルダ直下で、以下のように簡単に行うことができます。

JavaBean(s)とは、 サーバ上に置かれるJavaプログラムで、

という特徴をもっています。 後述のAction.javaはその例です。 また、プログラム7-12や7-13に示すJSPのプログラムから、

$ ant compile

(5) サーブレット、JavaBean等作成

サーブレットとは、狭義では、 javax.servlet で定義されているクラスを用いたJavaプログラムのことを意味します。 広義では、サーバ上で動作するJavaプログラム全般を指すこともあります。 JSPでもシンプルなプログラムの場合、 (5)ステップはないことがあります。 また、このTextAreaでも、Action.javaとして利用していますが、 サーバでデータを一時的に保持する領域を持ちたい場合など、 複雑な処理が入ってくると、 JavaBean(簡単なクラスを作成し、セッション中などで保持するクラスのデータを定義するJavaプログラム) などのJavaプログラムが必要となります。 TextAreaというWebアプリをとりあげたのは、今回のTomcatを 使ったWebアプリで(5)のプロセスがある例として示すためです。 TextAreaの場合、最初のindex.jspにより、 いわゆるHTMLでのFormのテキストエリアを利用します。 それで、任意サイズのテキストをFormのPOSTメソッドでサーバに送信したあと、 入力された文字列をサーバ側のAction.javaというJavaBeanで文字列を保持させます。 具体的には、以下のようなJavaプログラムです。
Action.java (JavaBeanの例)プログラム7-11
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
//	File: Action.java (プログラム7-11)
/* テキスト保持用JavaBean */

package textarea;/* パッケージ名 */

public class Action { 

	String body;/* テキストエリア文字列本体 */
	String analyzed;/* 適当な文字列解析後の文字列(サンプルでは未使用) */

	public void setBody(String body){/* body文字列にセット */
		this.body = new String(body);
	}
	public String getBody(){/* body文字列をゲット */
		return body;
	}
	public void setAnalyzed(String analyzed){/* 解析文字列をセット */
		this.analyzed = new String(analyzed);
	}
	public String getAnalyzed(){/* 解析文字列をゲット */
		return analyzed;
	}
} 
このプログラムは、一見何の変哲もないプログラムで、 2種類の文字列を保持し、Webブラウザの向こうにいるユーザの入力文字列を サーバで処理し、処理された文字列を、またユーザのいるWebブラウザに表示する、という役割を担っています。 一般に、JSPによるプログラム (サーブレットや他のスクリプト言語でのサーバサイド・プログラミングでも同様ですが)において、 あるブラウザのWebページからハイパーリンクで他のWebページに移動すると、 多くの変数は失われてしまいます。 そのようなとき、JavaBeanを使うことで、 保持しておきたいデータを、ひとつのJSPから、 次のJSPへとページがジャンプしても失われることがないよう管理することができます。

(4)で述べたbuild.xmlは、このJavaプログラムを(antで)コンパイルするために存在しています。

(6) JSPによる開始画面(index.jsp)等の作成

JSPによるプログラミングでは、開始画面は必ず必要です。 この中身は、よく見るとHTMLではないかと思うくらい、 作り方によっては、ほとんどHTMLで終わる場合もあれば、 Javaプログラムが大多数で、わずかにHTMLのタグが出ることもあります。 TextAreaの場合、以下のようなプログラムです。
index.jsp (TextArea用 プログラム7-12)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!--
	File: index.jsp
	Description: Javaサーバページ開始ページ(TextArea用)
-->
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ page import="textarea.*"%>

<!-- articleListをセッションで保持 -->
<jsp:useBean class="textarea.Action" id="action" scope="session"/>
<!-- -->

<html>
<head>
<title>テキストエリアとJavaBeanのテスト</title>
<script type="text/javascript">
	function funcCheck(){
		if(document.form1.body.value==""){
			alert("本文を入力してください");
			return false;
		}
	}
</script>

</head>
<body>
【入力テキスト】<br />

<center>
<h3>テキストエリアに文字列や文章を入力し、送信して下さい。</h3>
<form name="form1" action="action.jsp" method="POST"
	onsubmit="return funcCheck()"> 
	<textarea name="body" rows="18" cols="80"></textarea>
	<hr>
	<input type="submit" value=" 送 信 ">
	<hr>
</form>

</center>
</body>
</html>
先頭の1行目から4行目まではコメントです。 5行目と6行目がいわゆる ページディレクティブで、 5行目のほうは、文字コードなどを指定しています。 JSPでは、最初に必ずこれをつけると思ってください。 一方、6行目では、importという言葉からわかるように、 このJSPをTomcatが(ユーザの代わりにサーブレットに変換しJava言語として) コンパイルするときに必要なimport情報を並べることがよく行われます。 ここでは、(5)で述べたJavaBeanがtextareaという名前のパッケージに含まれていたため、 そのパッケージをimportしています。 これが使用されるのは9行目にある、JavaBeanの宣言文です。 JSPでは、<jsp:useBean>タグで指定します。 class=で指定される文字列がアクセスしたいクラス名です。 このJavaBean自体は、id="action"とあるように、 Javaからはactionという名前で、 プログラム7-11で定義したクラス内のpublicな変数や関数にアクセスできます。 scope="session"とあるのは、 このJavaBeanがWebブラウザが閉じられるまで(すなわち、セッションが有効な間)、 保持することを表します。 いってみれば、プログラム7-11のJavaBeanの賞味期限のようなものです。

TextArea自体は30~36行目のformタグの中で定義します。 そして、送信ボタンがクリックしたときに、 入力された文字列は、32行目にあるtextareaタグ内のname="body" で示されるbodyという名前で参照できる文字列としてPOSTメソッドで アクセスでき、制御はformタグ内にある action=action.jsp"で指定されるaction.jspに移動します。 action.jspは以下のようです。
action.jsp (プログラム7-13)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!--
	File: action.jsp
-->
<%@ page contentType="text/html; charset=UTF-8"%>
<jsp:useBean class="textarea.Action" id="action" scope="session"/>
<html>
<head>
<title>テキスト領域テスト</title>
</head>
<body>
<%
	try { request.setCharacterEncoding("UTF-8"); }
	catch (Exception e){	e.printStackTrace(); }

	String body = request.getParameter("body");
	action.setBody(body);
	String result = action.getBody();
%>
<center>
<h3>入力されたテキストは、下記のとおりです。</h3>
</center>
<hr>
<br><br>
<%=result%>
<hr>
<div align="right">
<a href="index.jsp"> 戻 る  </a>
</div>
<br />
</body>
</html>
プログラム7-13は典型的なJSPのプログラムです。 特に11行目の <% と18行目の %> に着目してください。 これらは、それぞれ、HTMLのタグで中にJavaプログラムの挿入開始と 挿入終了を表します。

また、 プログラム7-13はindex.jspから呼び出されたaction.jspでという名前のプログラムですが、 index.jspにおいてユーザが入力したテキストは、16行目にある、 request という変数のgetParameter("body")で利用している点に着目してください。 特に、このrequestという変数がどこにも定義されていないことが重要です。 JSPには、

などあらかじめ、 サーブレットのあるクラスの変数が暗黙オブジェクトとして設定されています。 逆にいうと、これらの暗黙オブジェクトは、Java言語の変数として使用すると混同しますので、 できるだけ使用しないことが好ましいわけです。 requestは具体的には、 HttpServletRequestという名前のインタフェースオブジェクトです。 HttpServletRequestは ServletRequest を継承したクラスで、ServletRequestまで調べると getParameter というメソッドがあることがわかるかと思います。 なお、最新のTomcatのJava API文書には日本語版がないことが多く、 上述したServletRequestなどのハイパーリンク先は英語の文書をリンクしています。

16行目に出てくるactionという変数こそが、 JavaBeanで定義された変数です。 ということで、actionからプログラム7-13で定義されたpublicな関数 (getXXX, setXXXなど)にアクセスしています。 17行目で最終的に入力された(潜在的に長い)テキストをresultという String型の変数に代入しています。 これを最終的にクライアントのWebブラウザに表示したいので、 24行目で<%=result%>という 文で指定します。 <%は、通常は、そこからJavaプログラムの挿入開始で %>で、挿入終了を表します。 一方、<%=で始めると、単独の変数の中身をWebブラウザに 書き出したいときに重宝する表現方法です。

(7) http://localhost:8080/TextArea/からアクセス

さて、最後のステップは、ローカルホストで動かしているTomcatに基づき、 Webブラウザを立ち上げて、Webアプリを起動することです。 TextAreaの場合、以下のようなindex.jspが提示する画面が現れます。

textareaにテキストが入力される前の様子


これに、適当なWebのニュース記事をコピー&ペイストしたものが 以下の図になります。

textareaへのインプット例


この後、送信ボタンを押すと、制御がaction.jspに移動し、以下のような結果が ブラウザに表示されます。

textareaでサーバに送られたテキストの表示例

(8) Tomcatの終了

Tomcatの終了は起動したターミナル窓で、Tomcat/binディレクトリに移動し、 以下のように実行します。

$ ./shutdown.sh

TextAreaをちょっと工夫して、日本語の形態素解析を実行し、 その結果を返すJSP+JavaBeansの例が、 めかぶ(和布蕪)による形態素解析サービスです。 ただし、セキュリティ上の理由で、これらのTomcat例は 学内からしかアクセスできませんので注意してください。

ファイルのアップロード

Tomcat.tgzの中に付随するFileUpload(ファイルをサーバにアップロード) を準備する手順を解説します。 最初に、index.jspを用意します。 これは、以下のようなファイルです。
index.jsp (FileUpload用 プログラム7-14)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--
	File: index.jsp
	ファイルアップロード
-->
<%@ page contentType = "text/html; charset=UTF-8"%>
<html>
<head>
<title>ファイルアップロード</title>
</head>
<body>
<h3>テキストファイル(UTF-8)のアップロードをテストします</h3>
<form method="post" enctype="multipart/form-data" action="Upload.jsp">
ファイル:
<input type="file" name="filename" size="30" />
<input type="submit" value="アップロード" />
</form>
</body>
</html>
12行目にあるaction="Upload.jsp"でアップロードしたあとのアクションが 指定されています。 FileUploadの コンフィギュレーション・ファイルとこのあと述べるUpload.jspが 準備できるとブラウザから http://localhost:8080/FileUpload/ より実行できます。 実行すると、以下のような画面が現れます。

ファイルアップロード用の画面例


この後、参照ボタンからローカルディスクにあるアップロードしたいテキストファイルを 選択します。 たとえば手元にあるjnews7.txtというファイルを選択したとします。 その後、アップロードボタンをクリックすることで サーバへアップロードされ、ファイルの中身が表示されます。 なお、UTF-8符号の日本語ファイルであっても文字化けすることがあります。 その場合、次に述べるUpload.jspファイルを一部修正することで 回避できる場合があります。
Upload.jsp (FileUpload用 プログラム7-15)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!--
	File:Upload.jsp
	Servlet3.0の機能を使ったアップロード
-->
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import = "java.io.*"%>
<html>
<head>
<title>ファイルアップロードのテスト</title>
<body>
<%
	response.setContentType("text/html; charset=UTF-8");
 	request.setCharacterEncoding("UTF-8");

	Part part = request.getPart("filename");/* javax.servlet.http.Part (Servlet 3.0以上)*/
	String header = part.getHeader("Content-Disposition");
	int index = header.indexOf("filename=");
	String filename = header.substring(index+10,header.length()-1);

	out.println("<h3>アップロードファイル:"+filename+"</h3><br/>");
	try {
		InputStream is = part.getInputStream();
		BufferedReader bin = new BufferedReader(	/* バッファリーダーでアクセス */
			new InputStreamReader(is));
		String line;
		while ((line = bin.readLine()) != null) 
			out.println(line+"<br/>");/* 行単位でWebブラウザにプリント */
	} catch (Exception e) {
    		e.printStackTrace();
  	}
%>
</body>
</html>
15行目にあるPart というクラスがファイルアップロードにおけるサーバでの重要なクラスになります、 これはサーブレットのバージョン3.0以上で使えるようになった機能です。 また、16~17行目は、ファイルアップロードの際に、クライアントからサーバへ送られるヘッダー情報として重要な処理です。すなわち、一般に
Content-Disposition: form-data; name="file"; filename="xxx"
のようなヘッダーがつき、この"name"の部分は、inputタグ内から与えられますが、 filename=のあとの文字列に、実際にアップロードされるファイル名が入ります。 18行目でこのファイル名を抽出しています。

文字コードに関してコメントすると、上記のJSPにおけるファイルアップロードで文字コードの問題に触れます。 英語のテキストはこのままで問題ありません。 日本語のテキストファイルをアップロードする際は、たとえ中身がUTF-8であっても 文字化けすることがあります。 この場合、たとえば、24行目のInputStreamReader(is)を InputStreamReader(is, "UTF-8") のように変更することで 文字化けを回避できる場合があります。 また、どのような符号かわからないとき、 InputStreamReader(is, "JISAutoDetect") とする場合もあります。 なお、13行目にもrequest.setCharacterEncoding("UTF-8"); とUTF-8符号を指定していますが、 こちらはリクエスト(index.jsp)から、Upload.jspに 制御が渡されるときの符号であって、アップロードされたファイルの内部 の符号を指定するものではありません。 とはいえ、request.setCharacterEncoding("UTF-8");は 前回説明したTextareaのようにテキストを直接サーブレット間で やりとりする場合は必須ですので、デフォルトで 必ず設定するようにしておくとよいでしょう。

上述したInputStreamReaderで直接UTF-8符号に設定した場合、 jnews7.txtをアップロードした結果は以下のように表示されます。


アップロードしたファイルを表示した様子

複数のファイルを同時にアップロードする場合は、 まず、プログラム7-14の14行目で示したinputタグを2つ以上用意し、 <input>タグ内のname="filename"の部分を 別名に変更します。たとえば2つ目をfilename2とします。 次に、プログラム7-15に相当するサーバ側の受け手では、 15行目のPart part = request.getPart("filename"); の部分では、2つ目のアップする方は Part part2 = request.getPart("filename2");とします。 なお、17行目のindexOF("filename=")を変更する必要はありません。 理由はアップされたファイル名を取出すやり方は Partに関わらず同じようなプロトコルで取り交わされるからです。 プロトコルの中身を知りたい場合は、 out.println(part+"<br>");などのような命令で ブラウザにPartで取得できるものの中味を書き出すとわかると思います。

2つ以上のファイルのアップロード(例)

プログラム7-15で簡単なファイルアップロード例を見ました。 では、2種類以上のファイルをアップロードするにはどうしたらいいでしょうか? これは以下のようにすれば可能です。
Part partN = request.getPart("filename");/* ニュースファイル用*/ Part partQ = request.getPart("queryFile");/* クエリファイル用*/ String headerN = partN.getHeader("Content-Disposition");/* ニュースファイルヘッダ */ String headerQ = partQ.getHeader("Content-Disposition");/* クエリファイルヘッダ */ int indexN = headerN.indexOf("filename="); int indexQ = headerQ.indexOf("filename="); String newsFile = headerN.substring(indexN+10,headerN.length()-1);/* ニュースファイル名 */ String queryFile = headerQ.substring(indexQ+10,headerQ.length()-1);/* クエリファイル名 */ try {/* ニュースファイルの処理 */ String line = null;/* 各行の文字列を保持する変数 */ String token = null;/* 各行から英単語候補を保持する文字列変数 */ String[] result = null;/* splitされた文字列保持用 */ String delimeter = " ";/* デリミタ */ int n;/* トークン(英単語数)の総数を保持する変数 */ char c;/* 英単語候補の文字列の最初の文字を判定用に保持する変数 */ ... WordLookup wl = new WordLookup();/* 後述のプログラム7-16参照 */ InputStream isN = partN.getInputStream(); InputStreamReader isrN = new InputStreamReader(isN, "UTF-8"); BufferedReader brN = new BufferedReader(isrN); while ((line = brN.readLine()) != null){/* ニュースの単語をハッシュ表に */ line = CharFilter.filter(line);/* 後述のプログラム7-17参照 */ result = line.split(delimeter); ... } ... } catch (...){...} try {/* クエリファイルの処理 */ InputStream isQ = partQ.getInputStream(); InputStreamReader isrQ = new InputStreamReader(isQ, "UTF-8"); BufferedReader brQ = new BufferedReader(isrQ); ... } catch (...){...}

JavaBeanやユーティリティJavaプログラムの作り方(例)

Tomcatで配備可能なサーバ側の手続きはJSPでほとんど記述可能です。 ただし、TextAreaで説明したJavaBeanやユーティリティ的に使えるクラスなどは Javaだけで記述し、それをJSP内から参照するという手法がしばしば用いられます。 たとえば、ハッシュ表のデータを追加するlookup(String, HashMap<String,Integer>) のような関数をJavaプログラムとして、適当なパッケージ名、たとえばWordQuery以下に つくるとします。 そのコードは以下のようです。
WordLookup.java (プログラム7-16)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
	File: WordLookup.java
	ハッシュ表保持用クラス
*/
package WordQuery;/* パッケージ名 */

import java.util.HashMap;/* ハッシュ表 */

public class WordLookup { 

    /* 単語がすでに登録されているかどうかを調べる関数 */
    public int lookup(String str, HashMap<String, Integer> map){
         /* 単語がすでに登録されているかどうかを調べる関数 */
        Integer value = (Integer)map.get(str);/* ハッシュ表にキーでアクセスし、Integerクラス(の値)にキャスト */
        if (map.containsKey(str)){/* 見つかった */
            int count = value.intValue() + 1; /* カウントアップ */
            map.put(str, new Integer(count));/* ハッシュ表を書き換える */
            return 0;
        }
        else {
            value = new Integer(1);/* 新規エントリを作る */
            map.put(str, value);/* 新規エントリをハッシュ表に追加 */
            return 1;/* 新規作成終了 */
        }
    }
} 
まず、このクラスは、以前紹介したbuild.xmlを使いAntで コンパイルしておくか、または 単独で事前にコンパイルし、資料13で示した配備図のsrcフォルダ以下においておきます。 なおコンパイル結果はWEB-INF => classes => WordQuery => WordLookup.class が置かれます(build.xmlでコンパイルしたとき)。 手動の場合、マニュアルで配備してください。

JSP側で、このクラスを呼び出したいときは 先頭で


<%@ page import = "WordQuery.WordLookup"%>

を宣言し、どこかでWordLookup wL = new WordLookup(); のようにオブジェクトを生成しておきます。 本当に参照する所で、

String token;
HashMap<String, Integer> map;
...
...
wL.lookup(token, map);

のようにして使えばいいわけです。 このように外部のクラスもパッケージ化することで JSPから参照することができるようになります。

また、文字列中に含まれるデリミタを考慮して単語の分割を行う関数を用意したい場合、 たとえば以下のようなJavaBeanを準備して対応することができます。
CharFilter.java (プログラム7-17)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
	File: CharFilter.java
	特殊文字フィルター用クラス
*/
package WordQuery;/* パッケージ名 */

public class CharFilter {
    
    public static char[] delimiter = {/* デリミタ */
            '\t', '/', ',', '.', '(', ')', '[', ']',    '<', '>', '{', '}',
             '=', '!', '"',     '\'', '?', '_', ';', ':'
    };
    public static String filter(String line){
        if (line == null || line == "") return line;
        for (int i = 0 ; i < line.length() ; i++ ){
            for (int j = 0 ; j < delimiter.length ; j++){
                if (line.charAt(i) == delimiter[j]) {
                    if (i == 0){/* 最初の文字 */
                        line = line.substring(i+1);    break;
                    }
                    if (i == line.length()-1){/* 最後の文字 */
                        line = line.substring(0,i-1); 
                        return line;
                    }
                    line = line.substring(0,i)+" "+line.substring(i+1);
                }
            }
        }
        return line;
    }
} 
プログラム7-17では、staticな関数filterがあります。staticであるため、 このクラスに関しては、オブジェクトは共有され、filterにアクセスするときは、 直接CharFilter.filer(文字列)のようにアクセスできます。 これらのJavaのクラスは、前回紹介したJavaBeansと少し違う使用法になっています。 すなわち、getやsetメソッドでアクセスするクラス変数があるわけではなく、 publicな関数があるだけです。

JSON

JSON (JavaScript Object Notation)は、その名の通り、 JavaではなくJavaScriptのための軽量のデータ交換フォーマットです。 人間にとって読み書きが容易で、マシンにとっても簡単にパースや生成を行なえる形式です。 JSONはJavaScriptプログラミング言語 (ECMA-262標準第3版 1999年12月)の一部をベースに作られています。 JSONは完全に言語から独立したテキスト形式ですが、C、C++、C#、Java、JavaScript、Perl、Python、 なお多く言語を使用するプログラマにとって、馴染み深い規約が使われています。 これらの性質が、JSONを理想的なデータ交換言語にしています。 JSONに関する記述は、 www.json.org/json-ja.htmlにもありますが、 JSPやサーブレットでJSONデータで交換する場合に重要なのでここで反復しておきます。

まず、JSONには2つの構造があります。

これらは普遍的なデータ構造であり、ほとんどすべてのプログラミング言語でサポートされています。 JSONで重要な要素は以下の5種類です。 ここでオブジェクトという言葉が出てきますが、これが実はJavaのObjectクラスと ほぼ対応しています。 JSONでのオブジェクトの定義は、英語では以下のように記述されます。
An object is an unordered collection of zero or more name/value pairs, where a name is a string and a value is a string, number, boolean, null, object, or array.
図で表すと以下の通りです。



















JSONの例を示します。
{
	"name":"山田太郎",
	"age":22,
	"messages":["メッセージ1","メッセージ2","メッセージ3"]
}
この例では、全体がオブジェクトで、最初の図にあるように、 "name"というstringがきてコロンがきます。 その後、"山田太郎"というvalueがきて、カンマがきて、 オブジェクトが繰り返されます。 valueには単純な文字列だけでなく、 "[" ... "]"で記されるarrayもきます。 ここでは、3つのメッセージがarrayの例を与えています。

さて、元々はJavaScript用に設計されたJSON形式のデータをJava言語から操作するインタフェースとして、使用されてきているJacksonというAPIを使用する例を紹介します。 最新版のJacksonは、前述のリンク先を参照してください。 以下では、初期に公開されたJacksonを使って説明します。 この初期のバージョンのJacksonを使用する際には、以下の2つのJARファイルとapache commonsからの追加JARファイル を使用します。

Tomcatから使用する場合、ダウンロードして各自のホームディレクトリのapache-tomcat-7.0.96/lib以下に配備(コピー)してください。

上に示した簡単なJSONデータをjacksonを使いパージングするプログラム例 (JSPでなく、通常のJavaプログラム)を紹介しておきます。
JacksonReadExample.java (プログラム7-18)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* JSONデータ(フィールドを知っているデータ)を読み込み、表示する */
import java.io.File;/* Fileクラス */
import java.io.IOException;/* IO例外 */
import org.codehaus.jackson.*;/* jacksonクラス */
import org.codehaus.jackson.map.*;/* jackson mapperクラス */
 
public class JacksonReadExample {
   public static void main(String[] args) {
        try {
            JsonFactory jfactory = new JsonFactory();/* Factoryデザインパターン */
            JsonParser jParser = jfactory.createJsonParser(new File(args[0]));/* JSONパーザ */

            /* トークン"}"までループ */
            while (jParser.nextToken() != JsonToken.END_OBJECT) {
                String fieldname = jParser.getCurrentName();/* 現在のフィールドのstringを得る */
                if ("name".equals(fieldname)) {/* "name"に等しい場合 */
                    jParser.nextToken();/* 名前に対応する値を得る */
                    System.out.println(jParser.getText()); /* パーズした値(テキスト)を表示 */
                }
                if ("age".equals(fieldname)) {/* "age"に等しい場合 */
                    jParser.nextToken();/* "age"に対応する値を得る */
                    System.out.println(jParser.getIntValue());  /* パーズした値(整数)を表示 */
                }
                if ("messages".equals(fieldname)) {/* "message"に等しい場合 */
                    jParser.nextToken(); /* "["(array)に対応するトークンを得る */
                    /* "]"(array)に対応するトークンを得る */ 
                    while (jParser.nextToken() != JsonToken.END_ARRAY) {
                        System.out.println("\t"+jParser.getText());/* 3つのメッセージを表示 */
                    }
                }
            }
            jParser.close();/* パーザを閉じる */
        } catch (JsonGenerationException e) {/* Jackson生成例外 */
            e.printStackTrace();
        } catch (JsonMappingException e) {/* Jacksonマッパー例外 */
            e.printStackTrace();
        } catch (IOException e) {/* ファイルIO例外 */
            e.printStackTrace();
        }
    }
}

Livedoorの天気予想Webサービス

天気予報のWebサービスは、気象庁関連からのデータを受けて実施されている場合が多いです。 Livedoorの天気予報(今日、明日、明後日予報)は全国の主たる都市での都市コードを入力すると、 天気と気温の予想を返してくれるWebサービスです。 以前は、受信するデータはXML形式だけサポートされていましたが、 数年前からJSONだけに変わりました。

例として、豊橋のコード(230020)で返ってくるJSONの生データを示します。緑色の部分が抽出したい部分です。

{"pinpointLocations":[{"link":"http://weather.livedoor.com/area/forecast/2320100","name":"\u8c4a\u6a4b\u5e02"},{"link":"http://weather.livedoor.com/area/forecast/2320700","name":"\u8c4a\u5ddd\u5e02"},{"link":"http://weather.livedoor.com/area/forecast/2321102","name":"\u8c4a\u7530\u5e02\u6771\u90e8"},{"link":"http://weather.livedoor.com/area/forecast/2321400","name":"\u84b2\u90e1\u5e02"},{"link":"http://weather.livedoor.com/area/forecast/2322100","name":"\u65b0\u57ce\u5e02"},{"link":"http://weather.livedoor.com/area/forecast/2323100","name":"\u7530\u539f\u5e02"},{"link":"http://weather.livedoor.com/area/forecast/2356100","name":"\u8a2d\u697d\u753a"},{"link":"http://weather.livedoor.com/area/forecast/2356200","name":"\u6771\u6804\u753a"},{"link":"http://weather.livedoor.com/area/forecast/2356300","name":"\u8c4a\u6839\u6751"}],"link":"http://weather.livedoor.com/area/forecast/230020","forecasts":[{"dateLabel":"\u4eca\u65e5","telop":"\u66c7\u308a","date":"2015-11-29","temperature":{"min":null,"max":null},"image":{"width":50,"url":"http://weather.livedoor.com/img/icon/8.gif","title":"\u66c7\u308a","height":31}},{"dateLabel":"\u660e\u65e5","telop":"\u6674\u308c","date":"2015-11-30","temperature":{"min":{"celsius":"7","fahrenheit":"44.6"},"max":{"celsius":"16","fahrenheit":"60.8"}},"image":{"width":50,"url":"http://weather.livedoor.com/img/icon/1.gif","title":"\u6674\u308c","height":31}},{"dateLabel":"\u660e\u5f8c\u65e5","telop":"\u6674\u6642\u3005\u66c7","date":"2015-12-01","temperature":{"min":null,"max":null},"image":{"width":50,"url":"http://weather.livedoor.com/img/icon/2.gif","title":"\u6674\u6642\u3005\u66c7","height":31}}],"location":{"city":"\u8c4a\u6a4b","area":"\u6771\u6d77","prefecture":"\u611b\u77e5\u770c"},"publicTime":"2015-11-29T17:00:00\u002b0900","copyright":{"provider":[{"link":"http://tenki.jp/","name":"\u65e5\u672c\u6c17\u8c61\u5354\u4f1a"}],"link":"http://weather.livedoor.com/","title":"(C) LINE Corporation","image":{"width":118,"link":"http://weather.livedoor.com/","url":"http://weather.livedoor.com/img/cmn/livedoor.gif","title":"livedoor \u5929\u6c17\u60c5\u5831","height":26}},"title":"\u611b\u77e5\u770c \u8c4a\u6a4b \u306e\u5929\u6c17","description":{"text":" \u672c\u5dde\u4ed8\u8fd1\u306f\u3001\u9ad8\u6c17\u5727\u306b\u8986\u308f\u308c\u3066\u3044\u307e\u3059\u304c\u3001\u4e0a\u7a7a\u306e\u6c17\u5727\u306e\u8c37\u304c\u901a\u904e\u3057\u3066\u3044\u307e\n\u3059\u3002\n\n \u6771\u6d77\u5730\u65b9\u306f\u3001\u304a\u304a\u3080\u306d\u66c7\u308a\u3068\u306a\u3063\u3066\u3044\u307e\u3059\u3002\n\n \u4eca\u591c\u306f\u3001\u4e0a\u7a7a\u306e\u6c17\u5727\u306e\u8c37\u306e\u5f71\u97ff\u3067\u3001\u304a\u304a\u3080\u306d\u66c7\u308a\u3068\u306a\u308b\u3067\u3057\u3087\u3046\u3002\n\n \u660e\u65e5\u306f\u3001\u304a\u304a\u3080\u306d\u6674\u308c\u307e\u3059\u304c\u3001\u4e0a\u7a7a\u306e\u6c17\u5727\u306e\u8c37\u3084\u5bd2\u6c17\u306e\u5f71\u97ff\u3067\u3001\u671d\u6669\u306f\u96f2\n\u306e\u5e83\u304c\u308b\u6240\u304c\u3042\u308b\u3067\u3057\u3087\u3046\u3002\u5c90\u961c\u770c\u98db\u9a28\u5730\u65b9\u3067\u306f\u3001\u5915\u65b9\u304b\u3089\u96e8\u306e\u964d\u308b\u6240\u304c\u3042\n\u308b\u898b\u8fbc\u307f\u3067\u3059\u3002","publicTime":"2015-11-29T16:36:00\u002b0900"}}
生のJSONデータでは、このように日本語の部分がUnicode文字になっていて、何が書かれているかわかりません。 このようなときに便利なのが、apache commonsのlangツールにある、org.apache.commons.lang3パッケージにある StringEscapeUtilsクラスのunescapeJavaというstatic関数です。static関数なので、インスタンスを作ることなく、 直接呼び出すことができます。 この関数でUnicode文字を可読のUTF-8コード文字に変換できます。 この変換のために、WordQueryで紹介したものと同様の変換関数を有するJavaBeanのクラスを作ることにします。
myUTFDecoder.java (JavaBean プログラム7-19)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
	文字列からUTFエスケープ文字へのデコーダ用JavaBean 
*/
package Weather;/* パッケージ名 */

import org.apache.commons.lang3.*;/* Java.langパッケージの拡張クラスを含むパッケージ */
public class myUTFDecoder {
    public static String decode(String line){
        String result = "";/* 結果を入れる文字列 */
        String code;/* バックスラッシュuではじまるUnicode文字列を取り出す */
        if (line == null) return(null);
        if (line.length() <= 6) return(line);/* 6文字以内なら何もしない */ 
        for (int i = 0 ; i < line.length(); i++){
            if (line.charAt(i) == '\\' && line.charAt(i+1) == 'u'){/* Unicode文字列の開始 */
                /* Unicode sequence */
                code = "\\u"+line.charAt(i+2)+line.charAt(i+3)+
                    line.charAt(i+4)+line.charAt(i+5);/* 5文字先までデータがあると仮定して読む */
                String st = StringEscapeUtils.unescapeJava(code);/* この関数で文字列→文字変換 */
                result += st; /* UTF文字に変換したものを返す文字列に追加 */
                i += 5;/* 5文字進める */
                continue;
            }
            else
                result += line.charAt(i);/* i番目の文字をそのまま取り出す */
        }
        return result;
    }
} 
これを適用すると、先に述べたJSONは以下のように日本語部分が明らかになります。
{"pinpointLocations":[{"link":"http://weather.livedoor.com/area/forecast/2320100","name":"豊橋市"},{"link":"http://weather.livedoor.com/area/forecast/2320700","name":"豊川市"},{"link":"http://weather.livedoor.com/area/forecast/2321102","name":"豊田市東部"},{"link":"http://weather.livedoor.com/area/forecast/2321400","name":"蒲郡市"},{"link":"http://weather.livedoor.com/area/forecast/2322100","name":"新城市"},{"link":"http://weather.livedoor.com/area/forecast/2323100","name":"田原市"},{"link":"http://weather.livedoor.com/area/forecast/2356100","name":"設楽町"},{"link":"http://weather.livedoor.com/area/forecast/2356200","name":"東栄町"},{"link":"http://weather.livedoor.com/area/forecast/2356300","name":"豊根村"}],"link":"http://weather.livedoor.com/area/forecast/230020","forecasts":[{"dateLabel":"今日","telop":"曇り","date":"2015-11-29","temperature":{"min":null,"max":null},"image":{"width":50,"url":"http://weather.livedoor.com/img/icon/8.gif","title":"曇り","height":31}},{"dateLabel":"明日","telop":"晴れ","date":"2015-11-30","temperature":{"min":{"celsius":"7","fahrenheit":"44.6"},"max":{"celsius":"16","fahrenheit":"60.8"}},"image":{"width":50,"url":"http://weather.livedoor.com/img/icon/1.gif","title":"晴れ","height":31}},{"dateLabel":"明後日","telop":"晴時々曇","date":"2015-12-01","temperature":{"min":null,"max":null},"image":{"width":50,"url":"http://weather.livedoor.com/img/icon/2.gif","title":"晴時々曇","height":31}}],"location":{"city":"豊橋","area":"東海","prefecture":"愛知県"},"publicTime":"2015-11-29T17:00:00+0900","copyright":{"provider":[{"link":"http://tenki.jp/","name":"日本気象協会"}],"link":"http://weather.livedoor.com/","title":"(C) LINE Corporation","image":{"width":118,"link":"http://weather.livedoor.com/","url":"http://weather.livedoor.com/img/cmn/livedoor.gif","title":"livedoor 天気情報","height":26}},"title":"愛知県 豊橋 の天気","description":{"text":" 本州付近は、高気圧に覆われていますが、上空の気圧の谷が通過していま\nす。\n\n 東海地方は、おおむね曇りとなっています。\n\n 今夜は、上空の気圧の谷の影響で、おおむね曇りとなるでしょう。\n\n 明日は、おおむね晴れますが、上空の気圧の谷や寒気の影響で、朝晩は雲\nの広がる所があるでしょう。岐阜県飛騨地方では、夕方から雨の降る所があ\nる見込みです。","publicTime":"2015-11-29T16:36:00+0900"}}
以下では、東海地方を中心とした都市を選択することで、その地域の天気予報を行う、 WebアプリケーションをJSPで構成するプログラムを紹介します。 最初に、index.jspを用意します。 これは、以下のようなファイルです。
index.jsp (Weather用 プログラム7-20)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<%-- 
	File: Index.jsp
	Description: 地域IDから天気予報を表示
--%>
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<head>< title>天気予報したい地域を入力</title></head>
<body bgcolor="lightYellow">
<center>

<h2>地域の入力から(今日、明日、明後日)の天気予報を表示します</h2>
<hr>
<br><h4>地域を選択し、送信してください。</h4>
<br>
以下の表から予報したい都市を選択してください。<br>
<form method="post" action="Weather.jsp">
<table border="0" cellspacing="1" cellpadding="5" >
<td  style=""> 
<br>* 静岡県 
<input type="radio" name="place" value="220010" id="id1"><label for="id1">静岡</label>
<input type="radio" name="place" value="220020" id="id2"><label for="id2">網代</label>
<input type="radio" name="place" value="220030" id="id3"><label for="id3">三島</label>
<input type="radio" name="place" value="220040" id="id4"><label for="id4">浜松</label>
<br>* 愛知県 
<input type="radio" name="place" value="230010" id="id5"><label for="id5">名古屋</label>
<input type="radio" name="place" value="230020" id="id6"><label for="id6">豊橋</label>
<br>* 岐阜県 
<input type="radio" name="place" value="210010" id="id7"><label for="id7">岐阜</label>
<input type="radio" name="place" value="210020" id="id8"><label for="id8">高山</label>
<br>* 三重県 
<input type="radio" name="place" value="240010" id="id9"><label for="id9">津</label>
<input type="radio" name="place" value="240020" id="id10"><label for="id10">尾鷲</label>
<br>* その他 
<input type="radio" name="place" value="016010" id="id11"><label for="id11">札幌</label>
<input type="radio" name="place" value="130010" id="id12"><label for="id12">東京</label>
<input type="radio" name="place" value="270000" id="id13"><label for="id13">大阪</label>
<input type="radio" name="place" value="471010" id="id14"><label for="id14">那覇</label>
</td>< /tr>
</table>< br>
<br>
<input type="submit" value="送信"/>
</form>
</center>
</body>
</html>
Webブラウザから適当な地域が選択されると、以下のような画面が現れます。

天気予報サービスの入力Webページ例


この後、JSON形式のデータから欲するデータを抽出するWeather.jspが実行されます。
Weather.jsp (プログラム7-21)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<!--
	File: Weather.jsp ライブドアの天気予報WebサービスJSONデータ利用例
-->

<%@ page contentType="text/html; charset=UTF-8"%>
<%@ page import="java.net.URLDecoder"%>
<%@ page import="java.net.URLEncoder"%>
<%@ page import="java.net.URL"%>
<%@ page import="java.io.*"%>
<%@ page import="org.codehaus.jackson.*"%>
<%@ page import="org.codehaus.jackson.map.*"%>
<%@ page import="org.apache.commons.lang3.*"%>
<%@ page import="Weather.myUTFDecoder"%>

<%
    try { request.setCharacterEncoding("UTF-8"); }/* UTF-8符号 */
    catch (UnsupportedEncodingException e){      e.printStackTrace(); }

    String city = request.getParameter("place");/* placeで地域コードを得る */
    String result1 = null;/* "title"に対応する値を入れる文字列 */
    String result2 = null;/* "telop"に対応する値を入れる文字列 */
    String result3 = null;/* "text"に対応する値を入れる文字列 */
    String utfString = "";/* UTF文字列 */
    try {
        //ライブドアの天気予報Webサービス
        String t= "http://weather.livedoor.com/forecast/webservice/json/v1?";
        String s= t + "city="+city;/* 都市コードを入れる */
        URL url = new URL(s);/* URL */
        String str = URLDecoder.decode(s, "UTF-8");/* デコード */
        InputStream in = url.openStream();      /*入力ストリームを取得する */

        /* JSONの処理 */
        JsonFactory jfactory = new JsonFactory();/* Factoryパターン */
        JsonParser jParser = jfactory.createJsonParser(in);/* Factoryパターンのミソ */
        boolean finish = false;
        while (!finish){
            jParser.nextToken();/* 次のトークンを取得 */
            String fieldname = jParser.getCurrentName();
            if ("title".equals(fieldname)) {/* stringが"title"の場合 */
                  jParser.nextToken();/* 次のトークンを取得 */
                  result1 = jParser.getText();/* 文字列を取得 */
            }
            if ("description".equals(fieldname)) {/* stringが"description"の場合 */
                while (jParser.nextToken() != JsonToken.END_OBJECT) {
                  String name = jParser.getCurrentName();
                  if ("text".equals(name)) {/* stringが"text"の場合 */
                     jParser.nextToken();/* 次のトークンを取得 */
                     String rawData = jParser.getText();/* 文字列を取得 */
                     result3 = myUTFDecoder.decode(rawData);/* UTFコードの1文字に変換 */
                     finish = true;
                     break;
                  }
               }
            }
            if ("forecasts".equals(fieldname)) {/* stringが"forecasts"の場合 */
                while (jParser.nextToken() != JsonToken.END_OBJECT) {
                  String name = jParser.getCurrentName();
                  if ("telop".equals(name)) {/* stringが"telop"の場合 */
                     jParser.nextToken();/* 次のトークンを取得 */
                     result2 = jParser.getText();/* 文字列を取得 */
                     break;
                  }
               }
            }
        }
        jParser.close();
    } catch (JsonGenerationException e){/* JSONのエラー */
        out.println("Error JSON: " + e.getMessage());
    } catch (JsonMappingException e) {/* JSON mapperのエラー */
        out.println("Error JSON mapper: " + e.getMessage());
    } catch(IOException e){/* IOエラー */
        e.printStackTrace(); out.println("IO error");
    }
%>
<html>
<head>
<title>地域と(今日、明日、明後日)から天気予報</title>
</head>
<body bgcolor="lightYellow">
<center>
<h3>地域と(今日、明日、明後日)から天気予報は、下記のとおりです。</h3>
</center>
<hr>
<br><br>
<%=result1%>
<table>
<tr><td width="50">天気</td> <td><%=result2%></td></tr>
<tr><td width="50">概況</td><td><%=result3%></td></tr>
</table>
<br><br>
<hr>
<div align="right"><a href="index.jsp"> 戻 る  </a></div>
<br>
</body>
</html>
以下が実行結果です。

JSONをサーバ側で解読する天気予報Webサービスの実行例