ソフトウエア基礎
ohmi@rsch.tuis.ac.jp
解答例

ネットワーク(2)

I/O ストリーム

ここでは、Javaでファイルやネットワーク通信を扱う場合に必要な ストリームについて学ぶ。

Javaでは、何らかの入出力を行なうのにストリームを使うのが原則である。 標準入出力(キーボード入力、画面出力)、ファイル、ソケット(ネットワーク)、 シリアル通信、パラレル通信などJavaで扱える入出力の多くはストリームを 使う。

Javaでは、これら様々な種類の入出力先に対し、 ストリームという統一された仕組みを使うので、 入出力先がファイルであってもソケットであっても、 ほとんどの場合全く同じ書き方でプログラムを書けば済む

ストリームは、java.ioパッケージで提供されている。 このため、import java.io.*; と宣言する必要がある。

ストリームは大きく分けて、入力ストリームと出力ストリームがある。

また、バイトストリームと文字ストリームの2つにも分けられる。

この他にオブジェクトストリームもあるが、ここでは取り上げない。

以下に主なストリームの分類を示す。

バイトストリーム文字ストリーム
入力 FileInputStream
FilterInputStream
ByteArrayInputStream
BufferedInputStream
FileReader
FilterReader
CharArrayReader
BufferedReader
出力 FileOutputStream
FilterOutputStream
ByteArrayOutputStream
BufferedOutputStream
FileWriter
FilterWriter
CharArrayWriter
PrintWriter

このように、入力バイトストリームにはInputStream、 出力バイトストリームにはOutputStream、 入力文字ストリームにはReader、 出力バイトストリームにはWriterがクラス名についている。 実際、それぞれ、それらの名前の抽象クラスから継承されたクラスである (例: FileInputStreamは InputStreamのサブクラス)。

ファイルI/O

ここで、Javaプログラムからファイルへの入出力を行ってみる。

Javaアプリケーションではファイル入出力が可能であるが、 Javaアプレットではセキュリティの問題から初期設定ではファイル入出力が 行なえない。設定を変える手段が用意されているがむやみに変更すると セキュリティの問題が生じる恐れがある。

ファイルの入出力は一般的には以下の3つの段階を取る。

  1. ファイルを開く
  2. ファイル処理(入力、出力)を行なう
  3. ファイルを閉じる

ファイルへの出力

以下はファイル出力のプログラムである。

import java.io.*;

public class FileOutput1 {
    public static void main(String[] args) {
	try {
	    FileWriter fw = new FileWriter("test.txt");
	    fw.write("This is a test.");
	    fw.close();
	} catch (IOException e) {
	    System.err.println("File can't write.");
	}
    }
}

ファイルへの出力には FileWriterクラスを使う。

FileWriterオブジェクトを生成する時に、指定したファイルが開かれる。 ファイル名は、コンストラクタの引数として指定する。 指定したファイルが存在しない場合は、空のファイルが作られる。 既に存在している場合は、開いた時点でファイルの内容を空にする。 この例の場合は、カレントディレクトリに "test.txt" ファイルが作られる。

ファイルを開いた後は、FileWriterオブジェクトに対して、writeメソッド を呼んで文字列データを出力すれば良い。

最後にcloseメソッドを呼んでファイルを閉じる。

ファイルを開く際に、開けない(ファイルの作成ができない場合等)時や、 出力時に何らかのエラーが生じた場合は、IOExceptionという例外が発生する。 上記のプログラムの場合は、"File can't write."と表示するようにしてある。

上記のwriteメソッドは、文字列か、1文字か、 char型の配列しか出力できない。System.out.println(100)という風に、 int型データなどを直接出力するような手段は用意されていない。 printやprintlnメソッドを使用したければ、PrintWriterクラスで包めば (ラップすれば)よい。

以下はPrintWriterクラスを使用した例である。

import java.io.*;

public class FileOutput2 {
    public static void main(String[] args) {
	try {
	    PrintWriter pw = new PrintWriter(new FileWriter("test.txt"));
	    pw.print("This is a test again.");
	    pw.close();
	} catch (IOException e) {
	    System.err.println("File can't write.");
	}
    }
}

PrintWriterオブジェクトを生成している行は複雑だが、 以下のように二行に分けると理解しやすい。

            FileWriter fw = new FileWriter("test.txt");
            PrintWriter pw = new PrintWriter(fw);

このように、FileWriterオブジェクトを生成して、それを PrintWriterのコンストラクタに引数に指定して、PrintWriter オブジェクトを作っている。FileWriterオブジェクトを PrintWriterオブジェクトで包んでいると言える。 後は、PrintWriterオブジェクトに対して print, println メソッドが使える。

ファイルからの入力

以下はファイル入力のプログラムである。

import java.io.*;

public class FileInput1 {
    public static void main(String[] args) {
	try {
	    FileReader fr = new FileReader("test.txt");
	    int c;
	    while((c = fr.read())!= -1) {
		System.out.print((char)c);
	    }
	    fr.close();
	} catch (IOException e) {
	    System.err.println("File can't read.");
	}
    }
}

ファイルから入力には FileReaderクラスを使う。

FileReaderオブジェクトを生成する時に、指定したファイルが開かれる。 ファイル名は、コンストラクタの引数として指定する。 この例では、カレントディレクトリの"test.txt"ファイルを開く。 ファイルが存在しない場合は、IOExceptionという例外が起きる。

ファイルを開いた後は、FileReadオブジェクトに対して、readメソッド を呼んで文字データを入力する。 read()メソッド(引数なし)は、戻り値として1文字のデータを返す。 1文字を返すので戻り値はchar型だと思うのが自然であるが、実際はint型 である。これはストリームが最後まで来た(ファイルの最後まで読んだ)時 に-1を返すようになっており、これを文字データと区別するために、 char型よりビット数の多い int型にしているのである。

このプログラムでは、while文で ファイルから1文字読んでは、 それをprintメソッドで標準出力に出している。 printメソッドで(char)c という風に、cの値を一度 char型にキャスト してから引数として渡していることに注意せよ。int型のまま渡すと 整数値と見なされるため画面に数字ばかりが出てしまうためである。

ファイルの終わりまで読んだ場合は、read()メソッドは-1を返し whileの繰り返しが終了する。 最後にcloseメソッドを呼んでファイルを閉じる。

上記のreadメソッドは、1文字か、char型の配列でしか結果を返せない。 例えば、1行分だけを読むような高級な手段は用意されていない。 1行分だけを読むreadLineメソッドを使用したければ、 BufferedReaderクラスで包めば(ラップすれば)よい。

以下はBufferedReaderクラスを使用した例である。

import java.io.*;

public class FileInput2 {
    public static void main(String[] args) {
	try {
	    BufferedReader br = new BufferedReader(new FileReader("test.txt"));
	    String s;
	    while((s = br.readLine())!= null) {
		System.out.println(s);
	    }
	    br.close();
	} catch (IOException e) {
	    System.err.println("File can't read.");
	}
    }
}

BufferedReaderオブジェクトを生成している行は以下のようにも書ける。 FileReaderオブジェクトをBufferedReaderオブジェクトで包んでいる。

            FileReader fr = new FileReader("test.txt");
            BufferedReader br = new BufferdReader(fr);

readLine()メソッドは戻り値としてストリームから読み込んだ一行分の 文字列を返す。ただし、改行コードは含まない。

ストリームの最後に到達した時には、readLine()はnullを返す。 このwhile文では、readLineで一行分読んでそれをprintlnで 画面に表示している。readLineが返す文字列は改行コードを 含まないので、printlnで改行している。 readLineがnullを返せばwhile文は終了する。

標準入力から文字列を入力する場合に、 以下のようなプログラムを習ったであろう。 現在では、このプログラムの詳細が理解できるのではないだろうか。
import java.io.*;
public class StdInput {
    public static void main(String[] args) throws IOException {
        String s;
        BufferedReader buf = new BufferedReader(new InputStreamReader(System.in));
        s = buf.readLine();
        System.out.println("input data is " + s);
    }
}
実は、System.in は InputStream型のオブジェクトである。 入力バイトストリームであるので、このままではバイト単位の入力しか 行なえない。そこで、System.inを InputStreamReaderオブジェクトで包み、 さらにBufferReaderオブジェクトで包んで、 readLine()メソッドが使えるようにしているのである。 同様に System.outとSystem.err は PrintStream型のオブジェクトである。 これは、PrintWriter型とほぼ同じ機能を持ち、print, println などの高級なメソッドが使用できる。

Javaによるクライアント作成(1)

ここでは、簡単なネットワーククライアントをJavaで作成する。

TCP/IPのレベルで通信を行なう場合、ソケットを使うのが一般的である。 ソケットは、2つのホスト間を接続する方法である。

Javaのソケットは接続が確立すると、以下のように出力ストリームと 入力ストリームが使えるようになる。 クライアントから見ると出力ストリームはサーバに要求を出すストリーム、 入力ストリームはサーバからの応答を受け取るストリームである。

Socket

接続が確立されたソケットは、 ソケットペアと呼ばれる以下の4つ組の値を維持している。

ここではクライアントを作成するので、クライアント側から見ると、 ローカル側はクライアントが動作しているホスト、つまり今 Javaのプログラムを実行しようとしているホストである。対して、 リモート側はサーバが動いているホストである。

Socket Pair

ローカル側(この場合クライアント)のポート番号は、 空いているポートが自動的に割り振られ、同じホスト内であれば、 他のソケットと番号が重なることはない。 このため、1つのクライアントから複数のソケットで同一サーバに 接続することが可能になっている。

Javaでクライアントから何らかのサーバに接続する場合、 典型的には以下の手順を踏む。ファイル処理と類似していることが分かる。

  1. ソケットを作成し、サーバ(リモートホスト)に接続する。
  2. ソケットに対してデータの送受信を行なう。
  3. ソケットの接続を切断する。

daytimeサーバへのアクセス

以下は、時刻を出力するdaytimeサーバにアクセスするための クライアントプログラムである。

daytime
import java.net.*;
import java.io.*;

public class DaytimeClient {
    public static void main(String[] args) {
        String remotehost = "localhost";
	try {
	    Socket sock = new Socket(remotehost, 13);
	    
	    InputStream is = sock.getInputStream();
	    int c;
	    while((c=is.read()) != -1) {
		System.out.print((char)c);
	    }
	    sock.close();
	} catch (UnknownHostException e) {
	    System.err.println(e);
	} catch (IOException e) {
	    System.err.println(e);
	}
    }
}

順番に説明する。 まず最初にSocketオブジェクトを生成する。 コンストラクタの引数として、リモートホスト名(あるいはアドレス)、 リモートポート番号を指定する。 なお、ローカルホスト名は自明であり、ローカルポート番号は接続時 に自動的に割り振られる(空いているポートが割り振られる)。 このためローカルホストに関しては指定する必要がない。

接続に成功すると次に進むが、失敗すると例外が発生する。 リモートホストが見つからない場合には、UnknownExceptionが発生する。 リモートホストは見つかったが、指定したポートに接続できない (多くの場合接続を拒否している)場合は、SocketExceptionが発生する。 その他の場合、IOExceptionが発生する場合がある。 このプログラムの場合は、SocketExceptionを捕捉していないが、 SocketExceptionはIOExceptionのサブクラスであるので、 SocketExceptionが発生するとcatch (IOException e)の箇所で捕捉される。

入力ストリームは、Socketオブジェクトに対し、getInputStream()、 出力ストリームは、getOutputStream()を呼ぶことで得られる。 上記の例では、sock.getInputStream() として入力ストリームだけを得て isに代入している。 daytimeサーバは、クライアントから何も送信しなくても結果を出力するので、 出力ストリームは必要ないからである。

サーバからの受信は、read()メソッドを使って1文字ずつ読んでは、 printメソッドで1文字ずつ標準出力に出力している。 ファイル処理の説明にあったように、BufferedReaderで包んで、 readLine()を使い一行単位で処理することも可能である(課題にある)。

サーバからの受信が終われば(read()が-1を返せば)、ソケットに対して、 closeメソッドを読んでソケットを切断している。 daytimeサーバの場合、サーバが時刻を送れば、サーバ側からソケットが切断さ れるので、closeメソッドを呼ばなくても正常に動作するが、 サーバ側から自動的に切断されない場合もあるので、 切断すべき時点でcloseメソッドを呼ぶ習慣をつけるべきである。

ソケット情報の取得

前に述べたソケットが持つ4つの値を得るには、以下のメソッドを使う。

以下にローカル側のポート番号を表示するプログラムを示す。

import java.net.*;
import java.io.*;

public class DaytimeClient2 {
    public static void main(String[] args) {
        String remotehost = "localhost";

	try {
            Socket sock = new Socket(remotehost, 13);
	    InputStream is = sock.getInputStream();
	    System.out.println("Local port: " + sock.getLocalPort());
	    int c;
	    while((c=is.read()) != -1) {
		System.out.print((char)c);
	    }
	    sock.close();
	} catch (UnknownHostException e) {
	    System.err.println(e);
	} catch (IOException e) {
	    System.err.println(e);
	}
    }
}

echoサーバへのアクセス

echoサーバはクライアントから送信した文字列を、クライアントにそのまま 返すサーバである。

基本的には、クライアントから一行送信すると、サーバがクライアント側に 一行返信する。この一行送信と一行返信を永遠に繰り返す。 繰り返しを止めるには、クライアント側から接続を切断する。

daytime

以下は、一行("Hello Java World!")だけを送信し、そして、 一行だけサーバから受信し、その後、切断するプログラムである。

import java.net.*;
import java.io.*;

public class EchoClient {
    public static void main(String[] args) {
        String remotehost = "localhost";
	String message = "Hello Java World!";

	try {
	    Socket sock = new Socket(remotehost, 7);
	    
	    PrintWriter out = new PrintWriter(sock.getOutputStream());
	    BufferedReader in = new BufferedReader(
	                    new InputStreamReader(sock.getInputStream()));

	    out.print(message + "\r\n");
	    out.flush();
	    String s = in.readLine();
	    System.out.println(s);
	    sock.close();
	} catch (UnknownHostException e) {
	    System.err.println(e);
	} catch (IOException e) {
	    System.err.println(e);
	}
    }
}

ソケットでサーバに接続した後に、PrintWriter out と、 BufferedReader in というストリームオブジェクトを生成している。 outはサーバへ出力する出力文字ストリーム、inはサーバからの 入力を受ける入力文字ストリームである。

次に、out.print で、メッセージの内容と改行コードを出力している。 つまり、クライアントからサーバに一行のデータを送っている。 この時に、out.println(message)としても改行できるが、 この理由は以下の「改行コードの問題」で説明する。

次に、out.flush() を呼んでいる。flushは、現時点で出力ストリーム に溜っているデータを全て放出するメソッドである。

ストリームにはバッファされる(一時的に溜められる)種類のものがある。 そのようなストリームではprintやwriteで単に出力しただけでは、 相手先に送られず、バッファに溜ったままになることがある。 このため、こまめにflush()を呼んでバッファから放出する必要がある。

送信が終わると、in.readLine()で入力ストリームから一行分を読む。 これを標準出力に出力する。 readLine() で得た文字列には改行コードが含まれていないので、 printlnで改行する。そして、close()で、ソケットを切断して終了する。

改行コードの問題

Javaの文字ストリームにおいてprintlnメソッドを使う時、出力される 改行コードはプラットホーム(Windows,Linux,Macintosh...)によって異なる。 WindowsはCR+LF、Linux(UNIX)はLF、MacintoshはCRである。 このように改行コードの出力がまちまちでも、Javaの場合、 入力側でreadLine()を使えば、どの改行コードでも正しく解釈し、 一行分を読むので問題ない。

しかし、それはクライアントとサーバの両方をJavaで書いた場合に限られる。 例えば、サーバ側はC言語で書かれていて、 改行はCR+LF以外は受け付けないかもしれない。サーバも動作している OSやプロトコルの種類によって改行の扱いが異なることがある。

このためネットワークプログラミングでは、プロトコルの改行コードを きちんと把握するべきである。 ここでは、改行コードをCR+LF(Javaの文字列表現であれば"\r\n")で統一している。 プラットホームに依存せずに、CR+LFを正しく出力するには、 printlnやnewLine(改行だけを出力するメソッド)を使わずに、printやwrite を使って、"\r\n"として出力すれば良い。

課題net-12

上記のFileOutput1.javaを実行せよ。 どのようなファイルが出力されたか報告せよ。 そのファイルには改行コードが含まれているか確認せよ。

課題net-13

FileOutput1.java を再度実行せよ。 出力されたファイルの内容がどうか報告せよ。

課題net-14

8の階乗を計算し、結果をfrac.txt というファイルに出力する プログラムを作成せよ。

課題net-15

テキストファイル data.txt の各行にテストの点数(0〜100の整数値)が 入っているものとする。そのファイルを読み込み、平均点を標準出力 に出力するプログラムを作成せよ。 平均点は実数値で出力せよ。
data.txt はテキストエディタ等で各自事前に作成せよ。
ヒント: 文字列データを整数値に変換する。

    String s = "125";
    int i = Integer.parseInt(s);

課題net-16

課題net-15のテキストファイル data.txt を読み込み、 点数を昇順にソートしたものをファイル sorted-data.txt に出力するプログラムを作成せよ。

課題net-17

上記のDaytimeClient.javaを修正し、コマンドライン引数として ホスト名を指定できるようにせよ。つまり、 java DaytimeClient localhostといった具合に 指定できるようにせよ。localhost以外にも接続してみよ。

課題net-18

課題net-17のプログラムを元に、BufferedReaderを使い、 readLine()を使うことで一行単位で受信するようにプログラムを書き換えよ。
※ InputStreamReaderで包み、さらにBufferedReaderで包む必要がある。

課題net-19

課題net-18のプログラムを元に、リモート側のホスト名とポート番号、 ローカル側のホスト名とポート番号を標準出力に出力するように、 プログラムを書き換えよ。何回か実行してみて、実行結果を示せ。 実行するごとに変化するものがあれば報告せよ。

課題net-20

EchoClient.javaの中で、out.flush(); をコメントアウトせよ。 プログラムを実行するとどうなるか。なぜそうなるかを説明せよ。

課題net-21

EchoClient.java を修正し、 キーボードからメッセージを入力し、それをサーバに送信するようにせよ。 また、何行でも送受信を繰り返すようにせよ。 ただし、空行を入力したらサーバとの接続を切り終了するようにせよ。


ソフトウエア基礎

ohmi@rsch.tuis.ac.jp

Valid HTML 4.01!