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からの読み込み条件を修正する、というような感じになるだろう。