last modified: Sep./09th/2002
ネットワークを使うGUIアプリケーションをつくってみましょう。ここではサーバ・サイドでポートを監視するアプリケーションと、クライアントから特定のポートのサーバ・アプリケーションを利用するアプリケーションを作成してみます。これはソケット通信の簡単なサンプルです。
既に説明したとおり、ソケットはIPアドレスとポート番号によって、サーバとクライアント間で構築するパイプであり、概念的な存在です。Javaではデータの通り道としてソケットを構築し、ソケットに対する入出力はストリームを使います。ネットワーク・レイヤで考えると、ルーティングや転送/中継で目的地(ホストのネットワーク・インタフェース)を識別するためのネットワーク層ではIP (Internet Protocol)を使いますが、そのの下層で誤り制御やフロー制御など実際のデータ通信の基礎を提供するためのトランスポート層ではUDP/TCP (User Datagram Protocol/Transmission Control Protocol)のプロトコルが利用できます。このサンプルではTCPで通信するためのものです。
小規模で専用線を用いたネットワークなどで、トラフィックが少なく、ネットワークの信頼性の高い場合は、信頼性のチェックを行わないUDPが高速で適しています。UDPではデータはデータグラムと呼ばれる概念で送受信され、信頼性の確保はその上で実行されるネットワーク・アプリケーション側の責任です。一方、公衆回線を利用したりトラフィックが多かったりルータを多数経由する必要のある、ネットワークの信頼性の低い場合は、プロトコルで信頼性のチェックを実装するTCPが適しています。TCPでは通信するホスト間でコネクションと呼ばれる通信経路の確立を行います。IPレイヤで目的地を指定するデータを付加したセグメントに、ポート番号やデータの完全性を確保するための確認応答のためのデータを付加したセグメントを送受信します。目的地から一定時間確認応答が無い場合、送信元ではデータの再送を行うことで完全性を確保します。コネクションを確立してから通信を行うTCPをコネクション・オリエンティッドと呼び、コネクションを確立しないで送りっぱなしのUDPをコネクションレスと呼びます。
尚、TCPにせよUDPにせよ通信は一回こっきりの、要求/応答の組み合わせであり、これをセッションと呼びます。前後のセッション間に脈絡の無い通信をステートレスと呼びます。例えば、一回目のセッションの直後にネットワークが途絶し、接続し直して別のセッションを持ったとしても、前後のセッション間には関係が無いので、ネットワークの途絶はなにも影響を及ぼしません。ネットワーク・プロトコルがステートレスであるのは、始点/終点のホスト間で、他人の施設した信頼性の低いネットワークを経由することが前提になっているからです。ネットワーク施設者とホスト管理者の間に面識があり得ない、第一種通信事業者による公衆回線を経由するネットワーク(WAN)はその最たる例ですが、専用線のイントラネット(LAN)でも他部門の施設した物理層を経由する必要があれば同様です。
ソケットに対する入出力にもI/Oストリームを使います。ここでは、先に紹介したテキスト・エディタのサンプルとは異なる文字ストリームのラッピングを使っています。詳細をAPI仕様書で調べてください。
サーバ・マシンのポートを監視し、TCPプロトコルで通信するサーバのアプリケーションを実装します。
SimpleServer.java:
import java.net.*;
import java.io.*;
class SimpleServer {
private static final int PORT = 10000;
public static void main(String[] args) {
Socket sc;
BufferedReader br = null;
PrintWriter pw = null;
try {
ServerSocket socket = new ServerSocket(PORT); // PORT にソケットを作成
System.out.println("待機します");
while (true) {
try {
sc = socket.accept(); // 接続の受付
try {
br = new BufferedReader(
new InputStreamReader(sc.getInputStream())
);
pw = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(sc.getOutputStream())
)
);
} catch (Exception exc) {
exc.printStackTrace();
}
while (true) {
try {
String msg = br.readLine();
pw.println(sc.getLocalAddress()
+ ": " + msg); // 出力ストリームへ書き込み
pw.flush(); // 出力ストリームから書き出し
} catch (Exception exce) {
try {
pw.close();
br.close();
sc.close();
break;
} catch (Exception excep) {
excep.printStackTrace();
}
}
}
System.out.println("またお越しください。");
} catch (Exception ex) {
ex.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
上で作ったサーバに接続するためのクライアントをGUIでつくってみましょう。ここまでくると、問題はGUIではなく、目的を果たすためのロジックになってきていることがお分かり頂けると思います。GUIは目的を果たすための道具に過ぎないのです。縦横無尽に利用できるようになってください。
ここでは複雑なレイアウト・マネージャの例としてGridBagLayoutを使っています。このレイアウト・マネージャは、GridLayoutと同様にコンテナを矩形領域に分けてコンポーネントを追加しますが、矩形領域やコンポーネントの性質をGridBagConstraintsという型のオブジェクトの変数として指定可能です。
例えば、一番左、上から二行目の矩形領域に、コンポーネントlabelを貼り付け、領域の伸縮に対してx方向に伸縮し、y方向には伸縮しない、コンポーネントは矩形領域の水平方向いっぱいに広がって表示されるという指定を行うとき、コードは次のようになります。
panel.setLayout(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.gridx = 0; // 一番左 c.gridy = 1; // 上から二行目 c.weightx = 1.0; // x方向に伸縮する c.weighty = 0.0; // y方向には伸縮しない c.fill = GridBagConstraints.HORIZONTAL; // 領域の水平方向いっぱいに広がる panel.add(label, c);
GridBagLayoutでは、他にも高度なレイアウト指定が可能であり、便利な使い方も沢山あります。詳細はAPI仕様書を参照し、こののサンプルをぐりぐり弄って色々な指定を試してみてください。
ネットワークに関しては、サーバ側のアプリケーションと、とても似ていることにも注意してください。リモートホストからの要求を処理すると言う点では同じロジックが必要になります。したがって、逆にサーバ側ロジックとの差異に注目してください。
SimpleClient.java:
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import javax.swing.*;
class SimpleClient extends JFrame {
private static final int PORT = 10000;
private JLabel urlLabel, msgLabel;
private JTextField urlField, msgField;
private JTextArea resArea;
private JScrollPane area;
private JPanel panel;
private JButton submit;
private String url = "localhost";
private Socket socket;
private BufferedReader br;
private PrintWriter pw;
public static void main(String[] args) {
SimpleClient sc = new SimpleClient();
}
SimpleClient() {
super("クライアント・アプリケーション");
// コンポーネントの作成
urlLabel = new JLabel("URL ");
msgLabel = new JLabel("メッセージ ");
urlField = new JTextField();
msgField = new JTextField();
resArea = new JTextArea();
area = new JScrollPane(resArea);
panel = new JPanel();
submit = new JButton("送信");
// GridBagLayoutの例
panel.setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
// (0, 0)に追加
c.gridx = 0; c.gridy = 0;
c.weightx = 0.0; // x方向に伸縮しない
panel.add(urlLabel, c);
// (1, 0)に追加
c.gridx = 1;
c.weightx = 1.0; // x方向に伸縮する
c.fill = GridBagConstraints.HORIZONTAL; // 水平方向いっぱいに広がる
panel.add(urlField, c);
// (0, 1)に追加
c.gridx = 0; c.gridy = 1;
c.weightx = 0.0;
panel.add(msgLabel, c);
// (1, 1)に追加
c.gridx = 1;
c.weightx = 1.0;
c.fill = GridBagConstraints.HORIZONTAL;
panel.add(msgField, c);
// コンテント・ペインの取得
Container cont = getContentPane();
// コンテント・ペインへ部品を追加
cont.setLayout(new BorderLayout());
cont.add(panel, BorderLayout.NORTH);
cont.add(area, BorderLayout.CENTER);
cont.add(submit, BorderLayout.SOUTH);
// リスナの登録
submit.addActionListener(new EventHandler());
// フレームのセットアップ
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300, 300);
setVisible(true);
}
// ネットワーク接続
class Connection {
public void connect() {
try {
InetAddress dst = InetAddress.getByName(url);
socket = new Socket(dst, PORT);
br = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
pw = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream()
)
)
);
while (true) {
try {
String msg = br.readLine();
resArea.append(msg + "\n");
} catch (Exception ex) {
br.close();
pw.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 内部クラスとしてイベント・リスナーを作成
class EventHandler implements ActionListener {
public void actionPerformed(ActionEvent ae) {
try {
url = urlField.getText();
String msg = msgField.getText();
// ネットワーク接続
Connection con = new Connection();
con.connect();
pw.println(msg);
pw.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
まず、クライアント要求を待機するためのサーバを起動します。
>javac SimpleServer.java >java SimpleServer
サーバが起動したら、別のコマンド プロンプトから、クライアントを起動して要求を送ってみましょう。サーバが起動したあとでクライアントを起動するようにしてください。サーバを終了させたらクライアントも終了させて、サーバが起動したのを確認してからクライアントを起動してください。
>javac SimpleClient.java >java SimpleClient
クライアントで任意の文字列を入力し、「送信」ボタンを押してください。サーバからの応答が表示されます。最初は、自分のマシン内でサーバとクライアントを実行して動作を確認します。クライアントの最初のフィールドにlocalhostと入力、第二フィールドにメッセージを入力、そして「送信」ボタンを押します。サーバから返される文字列には、SocketクラスのgetLocalAddress()によってサーバ・マシンのIPアドレスが付加されます。localhostはローカルループバックと呼ばれ、テスト用のIPが割り振られます。その値は127.0.0.1です。
続いて、リモートホストでサーバを起動し、ローカルマシンでクライアントを起動し、同様に動作確認します。正しく接続できないときは、リモートホストへpingコマンドを発行し、IPパケットをが正しく届いて応答が行われるか確認してください。
実はこのサーバは不良品です。というのも、クライアント処理をメインのコンピュータ制御のwhileループで実行してしまうため、その間は他のことができなくなってしまいます。一つのクライアントからの要求しかさばけません。これを回避するには、後続の章で説明する「スレッド」というものを使います。一つのスレッドは一つのコンピュータ制御に対応し、複数のスレッドを作成して実行すれば、あたかも複数のCPUで複数のアプリケーションを実行しているかのように、複数のコンピュータ制御を並行して実行できます。
クライアント側のソース・コードでも不思議な事が一点あります。SimpleClientのコンストラクタの最後に、Connectionクラスのconnect()メソッドが呼ばれており、whileループによってコンピュータ制御を握って放さないはずです。それなのに、ボタンを押すイベント・コードが実行されます。これはなんとしたことでしょうか。
実はJavaのイベントは、実行環境であるJavaVMによって自動的に作成されたイベント・ディスパッチング・スレッドと呼ばれるスレッド上で実行されます。したがって、main()メソッドのメイン・スレッドと平行したコードの実行が可能になるのです。他にも標準クラスでは自動的にスレッドが作成されるメソッドが存在し、明示的にスレッドを作成していなくとも、実際はマルチスレッドで動作していることが多くあります。
言語仕様としてマルチスレッドをサポートしていることは、Javaの大きな特徴の一つです。