Shammer's Philosophy

My private adversaria

Socketからのデータ読み込み考察-InputStream編

Javaで、Socketからデータを読み込むときに、どこまでデータを読み込むか、それをどうやって制御するのかということを考えてみる。様々なサイトに情報があるが、java.io.InputStreamReaderを使うサイトが多いようだ。たしかにこれを使うといろいろ簡単なのかもしれないが、ここではあえてこのクラスを使用せず、java.io.InputStreamからバイト情報を読み取る、ということを考える。

まず、サーバ側の実装。

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class InputStreamEchoServer 
{
    public static void main(String[]args)
    {
	if( !(args.length == 0 || args.length == 1) ){
	    System.out.println("Usage: java SimpleEchoServer <LISTENPORT>");
	    System.exit(1);
	}
	int listenPort = 7;
	if( args.length == 1 ){
	    listenPort = Integer.parseInt(args[0]);
	}

	ServerSocket server = null;
	Socket client = null;
	try {
	    server = new ServerSocket(listenPort);
	    client = server.accept();
	    System.out.println("Accept client, address:" + client.getInetAddress().getHostAddress() + ",port:" + client.getPort() + ".");
	    InputStream in = client.getInputStream();
	    int c;
	    while( (c = in.read()) != -1 ){
		System.out.println(c);
	    }
	    System.out.println("FINISH!");
	}
	catch( Exception e ){
	    e.printStackTrace();
	}
	finally{
	    if( client != null ){
		try {
		    System.out.println("Closing client socket...");
		    client.close();
		}
		catch( Exception ignore ){}
	    }
	}
    }
}

次に、クライアント側の実装。

import java.io.OutputStream;
import java.net.Socket;

public class SendOnlyEchoClient {
    public static void main(String[]args) {
	if( !(args.length == 1 || args.length == 3) ) {
	    System.err.println("Usage: java TCPEchoClient <Server> <Port> <Message>");
	    System.err.println("Usage: java TCPEchoClient <Message>// This is the same java TCPEchoClient localhost 7 <Message>");
	    System.exit(1);
	}
	String server  = (args.length == 3) ? args[0] : "localhost";
	int serverPort = (args.length == 3) ? Integer.parseInt(args[1]) : 7;
	try {
	    byte[] message = args[2].getBytes();
	    Socket socket = null;
	    try {
		socket = new Socket(server, serverPort);
		System.out.println("Connected to server ... sending echo string.");
		OutputStream out = socket.getOutputStream();
		out.write(message);
		out.flush();
		System.out.println("Finish the sending message the value of " + args[2]);
	    }
	    catch( Exception e ) {
		e.printStackTrace();
	    }
	    finally {
		try {
		    if( socket != null ) {
			System.out.println("Closing socket...");
			socket.close();
		    }
		}
		catch( Exception e ) {
		    e.printStackTrace();
		}
	    }
	}
	catch( Exception e ) {
	    e.printStackTrace();
	}
	finally {
	    // Nothing To do.
	}
    }
}

サーバ側を以下のように起動する。

$ java InputStreamEchoServer 7777

無応答のように見えるが、ちゃんとクライアントからの接続を待っている。

クライアント側を以下のように起動すると、以下のようになる。

$ java SendOnlyEchoClient localhost 7777 Test
Connected to server ... sending echo string.
Finish the sending message the value of Test
Closing socket...
$ 

クライアントを起動すると、サーバ側に以下のようなメッセージが出る。

Accept client, address:127.0.0.1,port:51351.
84
101
115
116
FINISH!
Closing client socket...

送られる文字は、すべて数字になる。サーバ側の

	    while( (c = in.read()) != -1 ){
		System.out.println(c);
	    }

の、System.out.println(c)を、System.out.println((char)c)にすれば、数字でなくて文字になる。そして、クライアント側から2バイト文字を送ろうとすると、思ったように送る事ができない。2バイト文字は、1バイト文字2つと認識されてしまう。charでは、1バイト文字しか扱えず、ASCII範囲外の数字があるとすべて?とかになってしまう。

もう一つ難点が。この実装では、クライアントはデータを送信したらすぐにSocketをクローズするからあまり問題にならないけれども、サーバ側ではまだクライアントからデータが来るのか、もう終わりなのかを判断する方法が少ないことだ。考えられる方法としては、

  • クライアント側のクローズで判断する
  • クライアントがサーバ側へあらかじめ送るデータのバイト数を知らせておく
  • 何らかの特殊文字を決めておき、それに合致する文字が来たらクライアントからのデータ送信完了と判断

がある。クライアントのクローズで判断すると、クライアント側にデータを返したい場合に困る。なので、一方通行な通信でしか利用できないクライアントがサーバ側へあらかじめ送るデータのバイト数を知らせる、というのは、HTTPのContent-Lengthなどで採用している方法。特殊文字もHTTPのチャンク転送などで使用されているが、2バイト文字を含む環境だと、あらかじめ決めておいた文字がその2バイト文字を分解した際に出てこないか心配。

こう考えると、HTTPプロトコルはよく考えられていると思う。ヘッダとボディを分けて、ヘッダは特殊文字(改行2つ)で、ボディは、必要に応じてヘッダでContent-Lengthを指定しておく、という両方のアプローチをとっている。もっとも、Keep-Aliveが無効ならSocketのクローズでよいのだが。

まだ完璧、というレベルにはほど遠いが、Keep-Aliveを有効にしたアプリケーションを作成する場合にはいろいろ難しい点がありそう、ということはわかった。このサーバを起動して、そこにHTTPブラウザで適当なページを指定してアクセスすれば、HTTPリクエストを覗ける簡易アプリになる。リクエストで、13、10、13、10の組み合わせがあれば、そこでHTTPリクエストは完結、となりそうだ。この実装をベースにHTTPサーバを作るとすれば、受信済みデータを解析して、13、10、13、10と続いたら次の処理を行うようにInputStreamからの読み込み条件を修正する、というような感じになるだろう。