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

ネットワーク(3)

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

Webサーバへのアクセス

telnetを使ったWebサーバへのアクセスと同様に、 httpポートに接続したら、httpリクエストをサーバに送り、 サーバからhttpレスポンスを受け取れば良い。

以下は、Webサーバにアクセスする例である。

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

public class HttpClient1 {
    public static void main(String[] args) {
	try {
	    String host = "www.rsch.tuis.ac.jp";
	    String path = "/~ohmi/software-basic/network-test.html";
	    int port = 80;

	    // httpサーバに接続
	    Socket sock = new Socket(host, port);
	    
	    PrintWriter out = new PrintWriter(sock.getOutputStream());
	    BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
	    System.out.println("[Connected]");

	    // HTTPリクエスト送信
	    out.print("GET " + path + " HTTP/1.1\r\n");
	    out.print("Host: " + host + "\r\n");
	    out.print("Connection: close\r\n");
	    out.print("\r\n");
	    out.flush();
	    System.out.println("[HTTP request sent.]");

	    String s;
	    // HTTPレスポンス受信
	    while((s = in.readLine()) != null) {
		System.out.println(s);
	    }
	    sock.close();
	} catch (UnknownHostException e) {
	    System.err.println(e);
	} catch (IOException e) {
	    System.err.println(e);
	}
    }
}

ソケットを使って、httpサーバに接続し、次に、httpリクエストを 送信し(リクエストの終りは空行)、httpレスポンスを受信している。 受信したhttpレスポンスは、標準出力に表示するようにしてあるが、 ヘッダ部分、空行、本体という構造になっていることが分かる。

メールサーバへのアクセス

これも、telnetを使ってSMTPサーバに接続した場合と同様である。 接続したら、HELO, MAIL FROM:, RCPT TO:, DATA, QUIT のコマンドを 順番に使用すれば良い。

以下は、SMTPサーバに接続してメールを送信するプログラムである。

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

public class SmtpClient1 {
    static void check(String r, int status) {
	String start = "" + status + " ";
	if (!r.startsWith(start)) {
	    System.err.print("ERROR:");
	    System.err.println(r);
	    System.exit(-1);
	}
	System.out.println("<" + r + ">");
    }

    public static void main(String[] args) {
	try {
	    String mailhost = "mailhost.edu.tuis.ac.jp";
	    String from = "s03888xx@edu.tuis.ac.jp";
	    String to = "s03999yy@edu.tuis.ac.jp";
	    String subject = "test mail";
	    String[] mailbody = {
		"This is a test mail.",
		"--",
		"Taro Johou <s03888xx@edu.tuis.ac.jp>"};
	    int port = 25;

	    // smtpサーバに接続
	    Socket sock = new Socket(mailhost, port);
	    
	    PrintWriter out = new PrintWriter(sock.getOutputStream());
	    BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
	    System.out.println("[Connected]");

	    String r;

	    r = in.readLine();
            check(r, 220);

	    out.print("HELO " + InetAddress.getLocalHost().getHostName()
		      + "\r\n");
	    System.out.println("HELO " + InetAddress.getLocalHost().getHostName());
	    out.flush();
	    r = in.readLine();
	    check(r, 250);

	    out.print("MAIL FROM:" + from + "\r\n");
	    System.out.println("MAIL FROM:" + from);
	    out.flush();
	    r = in.readLine();
	    check(r,250);

	    out.print("RCPT TO:" + to + "\r\n");
	    System.out.println("RCPT TO:" + to);
	    out.flush();
	    r = in.readLine();
	    check(r,250);

	    out.print("DATA\r\n");
	    System.out.println("DATA");
	    out.flush();
	    r = in.readLine();
	    check(r,354);

	    // メイルヘッダ送信
	    out.print("Subject: " + subject + "\r\n");
	    System.out.println("Subject: " + subject);
	    out.print("To: " + to + "\r\n");
	    System.out.println("To: " + to);
	    out.print("\r\n"); //空行
	    System.out.println();
	    
	    //メイル本文送信
	    for (int i = 0; i < mailbody.length; i++) {
		out.print(mailbody[i] + "\r\n");
		System.out.println(mailbody[i]);
	    }
	    out.print(".\r\n"); // . 送信
	    System.out.println(".");
   	    out.flush();
	    r = in.readLine();
	    check(r,250);

	    out.print("QUIT\r\n");
	    System.out.println("QUIT");
	    out.flush();
	    r = in.readLine();
	    check(r,221);
	    sock.close();

	    System.out.println("[Mail sent]");
	} catch (UnknownHostException e) {
	    System.err.println(e);
	} catch (IOException e) {
	    System.err.println(e);
	}
    }
}

ソケットでSMTPサーバに接続したら、まず、サーバからの応答(一行) を受信する(in.readLine)。応答は正常であれば、 "220 サーバの名称とバージョン"という形式になる。 そこで、checkメソッドを呼び出し、行の先頭にあるステータス番号が220番か どうかを確認している。

checkメソッド内部では、応答メッセージが、ステータス番号 + " "で 始まっているかどうかを startWithメソッドを使って確認し、 始まっていなければエラーメッセージを表示して、System.exit(-1)で 異常終了している(Javaアプリケーションが終了する)。

サーバからの応答が正常であれば、次に、HELOコマンドを送信する(out.print)。 そしてflushし、応答を受信する(in.readLine)。 この場合250番のステータス番号が返ってくることが期待され、 checkメソッドを呼び出し確認している。

その後、同様に、MAIL FROM: , RCPT TO: コマンドを送信している。 これらも正常なら250番のステータス番号を返すはずである。

そして、DATAコマンドを送信して、メール本体の送信を行う。 DATAコマンドを送信して成功すれば、354番のステータス番号が返る。 そして、その後はメール本体を送信する。メール本体は、ヘッダと 本文に分けられる。最初の空行までがヘッダを意味する。 このプログラムでは、Subject: と To: ヘッダを送信している。 その後、空行を送信し、メール本文を送信する。 本文は、String型の配列に入れてあり、for文を使って順番に送信している。 本文の送信が終わると、送信を最後を示す "."だけからなる行を送信する。 すると、250番のステータスがサーバから返ってくるので、checkメソッドで 確認する。

最後にQUITコマンドを送信し、250番が返ってくるかどうかチェックする。 そしてソケットを閉じて終了する。

Javaによるサーバ作成

ここでは、簡単なサーバの作成を行ない、Javaでサーバを作成する方法を学ぶ。

サーバプログラムの概要

通常、サーバは、常時稼働しているもので、 基本的には以下の流れ図の要領でクライアントからの要求に答える。

server
この流れ図はあくまで原則である。クライアントの要求を待たずに、 サーバが応答を返したりする(非同期)ようなものも中にはある。 また、daytimeサーバのように、クライアントから何も要求がなくても 即座に応答を返すものもある。

以下に具体的なサーバのプログラムを示していく。

echoサーバの作成

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

public class EchoServer1 {
    static int port = 10007;
    
    public static void main(String[] args) {
	try {
	    ServerSocket server = new ServerSocket(port);
	    Socket sock = null;
	    System.out.println("Server Ready");
	    while(true) {
		try {
		    sock = server.accept(); // クライアントからの接続を待つ

		    System.out.println("Connected");
		    BufferedReader in = new BufferedReader(
                        new InputStreamReader(sock.getInputStream()));
		    PrintWriter out = new PrintWriter(sock.getOutputStream());
		    String s;
		    while((s = in.readLine()) != null) { // 一行受信
			out.print(s + "\r\n"); // 一行送信
			out.flush();
			System.out.println(s);
		    }
		    sock.close(); // クライアントからの接続を切断
		    System.out.println("Connection Closed");
		} catch (IOException e) {
		    System.err.println(e);
		}
	    }
	} catch (IOException e) {
	    System.err.println(e);
	}
    }
}

以下でプログラムの詳細を説明する。

	    ServerSocket server = new ServerSocket(port);

まず最初に、ServerSocketオブジェクトを作る。 コンストラクタの引数は、サーバが接続を待つポート番号である。 この場合は、10007とする。UNIXの多くは、1023までのWell-known ポートで接続要求を待てるのでは管理者(root)に限られる。 このため、多くの場合自由に使える、 大きなポート番号をここでは使用することにする。

		    sock = server.accept();

次に、ServerSocketオブジェクトである、serverに対して、 acceptメソッドを呼ぶ。そうすると、クライアントからの接続要求を 待ち続ける。接続要求があったら、戻り値としてSocketのオブジェクト を返す。後は、このオブジェクトを利用してクライアントとの間で データの送受信が行なえる。

Socketのオブジェクトsockの使い方は、 クライアントの場合と全く同じである。ただし、 クライアントと立場が逆転するので、 入力ストリーム(getInputStreamで得る)は、 クライアントからの要求を受信するストリーム、 出力ストリーム(getOutputStreamで得る)は、 サーバからクライアントへの応答を送信するストリームとなる。 つまり、サーバの入力ストリームはクライアントの出力ストリームであり、 サーバの出力ストリームはクライアントの入力ストリームである。

Fig. Socket
サーバ/クライアントにおける入力と出力ストリームの関係
		    while((s = in.readLine()) != null) { // 一行受信
			out.print(s + "\r\n"); // 一行送信
			out.flush();
			System.out.println(s);
		    }

上記のプログラムでは、in.readLine()でクライアントから一行を受信し、 out.print()でクライアントにその一行を送信している。 これを永遠に繰り返す。ただし、クライアントから接続を切った場合には、 in.readLine()の戻り値がnullになるため、whileループが終了する。 そうすると、ソケットを切断し、外側のwhileループを繰り返す。 つまり、またクライアントからの接続を待ち、入力/出力ストリームを得て、 一行受信/一行送信を繰り返す、といった具合に、同じことを繰り返す。

簡易Webサーバの作成

ここでは、非常に簡単なWebサーバをJavaで実装する。

以下はその例である。

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

public class WebServer1 {
    static int port = 10080;
    
    public static void main(String[] args) {
	try {
	    ServerSocket server = new ServerSocket(port);
	    Socket sock = null;
	    String[] html_body = {
		"<html><head>",
		"<title>Network test</title>",
		"</head>",
		"<body>",
		"<h1>Network test page</h1>",
		"<address>joho-taro@hogehoge.tuis.ac.jp</address>",
		"</body>",
		"</html>"}; // 送信するHTMLの内容
	    int content_length = 0;
	    int i;
	    // HTMLの内容のバイト数を数える
	    for (i = 0; i < html_body.length; i++) {
		content_length += html_body[i].length() + 2;
	    }

	    System.out.println("*** Server Ready ***");
	    while(true) {
		try {
		    sock = server.accept(); // 接続を待つ
		    System.out.println("*** Connected ***");
		    boolean connected = true; // まだ接続中かどうか?
		    BufferedReader in = new BufferedReader(
                        new InputStreamReader(sock.getInputStream()));
		    PrintWriter out = new PrintWriter(sock.getOutputStream());
		    String s;
		    // HTTPリクエストを受信する
		    while(true) {
			s = in.readLine();
			if (s == null) { // ソケットが切断された
			    out.print("HTTP/1.1 400 Bad Request\r\n");
			    out.flush();
			    connected = false;
			    break;
			}
			if (s.equals("")) break; // 空行に到達した
			System.out.println(s);
		    }
		    // HTTPレスポンスを返す
		    if (connected) { 
		        // HTTPステータス行の送信
			out.print("HTTP/1.1 200 OK\r\n");
		        // HTTPヘッダ部の送信
			out.print("Server: Simple Java Web Server \r\n");
			out.print("Content-Length: " + content_length + "\r\n");
			out.print("Connection: close\r\n");
			out.print("Content-Type: text/html\r\n");
			// 空行(ヘッダ部終了)の送信
			out.print("\r\n");
			// 本文(この場合HTML)の送信
			for (i = 0; i < html_body.length; i++) {
			    out.print(html_body[i] + "\r\n");
			}
			out.flush();
		    }
		    sock.close(); // ソケットを切断する
		    System.out.println("*** Connection Closed ***");
		} catch (IOException e) {
		    System.err.println(e);
		}
	    }
	} catch (IOException e) {
	    System.err.println(e);
	}
    }
}

基本的には、Webクライアントからの接続を待ち、接続があったら、 httpリクエストを受信し、それに基いてhttpレスポンスを返して接続を切る。 これを永遠と繰り返すわけである。

この場合は、非常に簡単にするために、どのようなhttpリクエストが来ても 全く同じhttpレスポンス(固定のHTML)を返すことにする。

echoサーバと同様に、Well-knownポートではなく、 10080番でサーバを動かすことにする。

ServerSocketオブジェクトを作成し、accept()で接続要求を待ち、 Socketオブジェクトを得るところはechoサーバと同じである。 そして入力ストリームをin、出力ストリームをoutとしている。

まず、whileループでクライアントから送られるhttpリクエストを全て受信する。 ここでは、httpリクエストを受信するだけでその内容を全く処理していない。 非常に乱暴であるが、必ず同じ内容を返すのであるから一応これで動作する。 空行を受信すれば、break文でwhile文を抜けて次にすすむ。 また、空行に到達せずに接続が切られたら、400 Bad Request の結果を返す。

httpリクエストを正常に受信した後は、httpレスポンスを返す。 一連の、out.print(...) で出力している箇所である。 それが終われば、flushを行ない、ソケットを切断している。

外のwhile文は while(true) となっているので、以上の一連の動作を 永遠に繰り返すことになる。

課題net-22

接続すると時刻を返すdaytimeサーバをJavaアプリケーションとして作成せよ。
ヒント: 時刻を扱うにはDateクラスを使用すれば良い

    Date d = new Date();
    System.out.println(d);

課題net-23

echoサーバを改造し、クライアントから送られてきた文字列を前後逆にして 返すようにせよ。例えば、"abcde xyz"と送られてきたら、"zyx edcba" と 返すようにすれば良い。文字列は、1バイト文字だけ扱えれば良いものとする。

課題net-24

echoサーバを改造し、クライアントから送られてきた文字列の大文字と小文字を 逆にして返すようにせよ。例えば、"Game Over"と送られてきたら、"gAME oVER" と 返すようにすれば良い。文字列は、1バイト文字だけ扱えれば良いものとする。

課題net-25

Webサーバの例 WebServer1.java を改造し、 HTMLの内容として時刻を返すようにせよ。 例えば、<h1>15 DEC 2004 11:27:07 JST</h1> という具合に HTMLの中に埋め込むようにせよ。

課題net-26

Webサーバにアクセスするプログラム HttpClient1.javaで、自分で作成した Webページにアクセスしてみよ。ソースのうち修正した部分と、実行結果を 報告せよ。実行結果が長い場合は、HTMLの途中を省略せよ。

課題net-27

SMTPサーバにアクセスするプログラム SmtpClient1.javaで、 差出人と宛先の双方とも自分のメールアドレスとして、メールを送信してみよ。 ソースのうち修正した部分と、 受信したメールの内容をメールヘッダを含めて報告せよ。

課題net-28

Webサーバにログファイルを作成する機能を追加せよ。
ログファイルの仕様:
ファイル名は、access_log とする。
一回のアクセスで一行出力するようにし、一行は、 <クライアントのホスト名もしくはIPアドレス> <URLのパス> <HTTPプロトコルバージョン> とせよ。


ソフトウエア基礎

ohmi@rsch.tuis.ac.jp

Valid HTML 4.01!