イントロダクション
世の中では、「ウェブサーバー」というものが「普通」に存在しています。というか多くの人が「常識のように」言葉として使用します。
しかし、その中身はどうなっているのでしょうか?ITエンジニアではない人ならば、知る必要ないと思うかもしれません。ITエンジニアであれば、知っていて当然みたいな雰囲気が感じられますが、実際はどうでしょうか?
何がいいたいかというと、「ウェブサーバー作って見ませんか?」ということです。当然、自分ウェブサーバーを作ったらウェブサーバーを理解しているのも必然です。
早い話が、作ってみれば理解できるということです。
ウェブサーバーを作る
ここで使用する言語はJavaです。自分がJava屋なのもありますが、ウェブ系のアプリといえば、だいたいJavaで実装していることが多いです。(業務アプリに関して。。。)
そして、今回作成したサーバーは、いまだに不完全です。SOcけt同士の通信ならば問題なくできるのですが、ブラウザからアクセスを受付た場合は、うまくいきません。セキュリティ的にアクセスが拒否られているようです。
Socketでの通院は、下のクラスを起動することでやり取りができます。
Client App
public class ClientMain {
/** 自身のインスタンス */
private static ClientMain main;
/** クライアントソケット */
private Socket sock = null;
/** 送信用のクラス */
private PrintWriter send;
/** 受信用のクラス */
private BufferedReader response;
/** 改行コード */
private static final char SEP = (char) 10;
public ClientMain() {
try {
sock = new Socket("localhost", NaturalBornServlet.PORT_NO);
} catch (IOException e) {
System.out.println("*** ソケットで例外が発生しました。 ***");
}
}
/**
* デストラクタ
*/
@Override
public void finalize() {
try {
sock.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
sock = null;
}
}
/**
* 標準入力からサーバーソケットへデータを送信する。
*
* @return 終了時のメッセージ
*/
public String execute() {
try (Scanner scan = new Scanner(System.in)) {
while(true) {
System.out.println("*** クライアントソケット: 入力 ***");
String input = scan.nextLine();
// リクエストの送信
sendRequest(input);
if ("bye".equals(input)) {
System.out.println("*** クライアントアプリを終了します。***");
break;
}
// レスポンスを受信する
System.out.println("クライアント: " + getResponse());
}
}
return "クライアントを終了しました。";
}
private void sendRequest(String in) {
try {
//System.out.println("*** クライアントソケット: リクエスト送信" + in + " ***");
send = new PrintWriter(sock.getOutputStream());
send.write(in + SEP);
send.flush();
// send.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* サーバーからのレスポンスを受ける。
* @throws IOException
*/
private String getResponse() {
StringBuilder build = new StringBuilder();
try {
// System.out.println("*** クライアントソケット: レスポンス受信 ***");
if (response == null) {
response = new BufferedReader(new InputStreamReader(sock.getInputStream()));
}
while (true) {
int read = response.read();
if (read == 13 || read == 10) {
break;
}
char ch = (char) read;
build.append(ch);
}
//response.close();
} catch (IOException e) {
e.printStackTrace();
build.append(" Errors... ");
}
// System.out.println("*** クライアントソケット: 完了:レスポンス受信 ***");
return build.toString();
}
public static void main(String[] args) {
ClientMain main = new ClientMain();
String mes = main.execute();
System.out.println(mes);
}
}
java.net.ServerSocket
Javaでのサーバー作成で最もベーシックな低レベルAPIがこのクラスです。巷で出回っているフレームワークも結局はこのテクノロジーを使用しています。
ServerSocketとは
このクラスはサーバー・ソケットを実装します。サーバー・ソケットは、ネットワーク経由で要求が送られてくるのを待ちます。これは、その要求に基づいていくつかの操作を実行します。その後、場合によっては要求元に結果を返します。
サーバー・ソケットの実際の処理は、SocketImplクラスのインスタンスによって実行されます。アプリケーションは、ソケット実装を作成するソケット・ファクトリを変更することで、ローカル・ファイアウォールに適したソケットを作成するようにアプリケーション自体を構成することができます。
このように解説がJavaDocドキュメントにはありますが、実装の方は下のようになります。
ServerSocketの実装
サーバーソケットの作成(インスタンス化)
実際に下のようにコーディングしました。これは、サーバーソケットのインスタンスを作成する処理です。
try {
server = new ServerSocket(portNo);
if (server.isBound() == false) {
server.bind(new InetSocketAddress("localhost", portNo));
server.setSoTimeout(3000);
}
} catch (IOException e) {
this.finalize();
e.printStackTrace();
}
処理としては、以下の通りです。
- サーバーソケットをポート番号"portNo"で作成
- サーバーソケットがバインドされていない場合は、新たにバインドし直し
- サーバーソケットのタイムアウト時間を3秒に設定
サーバーソケットの送受信処理
インスタンスを作成したら、クライアントとの送受信を行います。手順は以下の通りです。
- サーバーを起動する
- クライアントアプリでアクセスする
なので、先にサーバーを作成します。そして、一番下にメインメソッドを作っています。
/**
* コンストラクタで作成されたサーバーソケットで
* 受付処理を開始する。
*/
public void startServer() {
try {
System.out.println("*** SocketServer start " + server.isBound() + "***");
Socket recieve = server.accept();
System.out.println("*** Server Get Request start***");
// 受信データの読み込み
StringBuilder responseTxt = new StringBuilder(" Response");
// 受信状態
System.out.println("クライアント: " + recieve.getRemoteSocketAddress());
System.out.println("接続: " + recieve.isConnected());
System.out.println("入力ストリームが閉じている: " + recieve.isInputShutdown());
// 受信したリクエスト
InputStream in = recieve.getInputStream();
// 返却するレスポンス
OutputStream writer = recieve.getOutputStream();
System.out.print("Server recieve is ...");
int c = 0;
// CRとCRLFの場合で入力が終了している時がある
while((c = in.read()) != -1) {
char ch = (char)c;
responseTxt.append(ch);
// 空の場合
if (c == 10 || c == 13) {
break;
}
}
System.out.println(responseTxt.toString());
System.out.println("*** Server Send Response start***");
// レスポンス送信
writer.write((responseTxt.toString() + System.getProperty("file.separator")).getBytes());
writer.flush();
in.close();
writer.close();
System.out.println("*** SocketServer end ***");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 開いたストリームを閉じる
try {
closeServer();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* メインメソッド
* @param args プログラム引数
*/
public static void main(String[] args) {
SocketServerBasic serverSocket = new SocketServerBasic(8080);
serverSocket.startServer();
}
次に、クライアントアプリの作成を行います。
クライアントの処理は、以下の通りです。
- クライアントソケットの作成(ローカルホスト、ポート番号指定)
- 受診するデータを準備(InputStream)
- 送信するデータを準備(OutputStream)
- リクエストの送信
// リクエスト送信
writer.write("ping...\n".getBytes());
writer.flush();
- サーバーのレスポンスを受信
int c = 0; while((c = reader.read()) != -1) { System.out.print((char)c); }
ここで、注意点があります。受信時の処理で、BufferedReader#readLine()では、データの区切りが取得できないケースがあります。
そのような時は、下のように、改行文字をデータの区切りとして取得すれば、処理がループを抜けない状態を回避することができます。
// レスポンス
response = new PrintWriter(socket.getOutputStream());
int ch = -1;
StringBuilder build = new StringBuilder();
while((ch = request.read()) != -1) {
build.append((char) ch);
if (ch == 10 || ch == 13) {
break;
}
}
/**
* コンストラクタ
* @param portNo ポート番号
*/
public static void main(String[] args) {
System.out.println("*** SocketClient start ***");
try (Socket sock = new Socket(HOST, PORT)) {
// 受信したレスポンス
InputStream reader = sock.getInputStream();
// 送信するリクエスト
OutputStream writer = sock.getOutputStream();
System.out.println("接続済み: " + sock.isConnected());
System.out.println("接続サーバー: " + sock.getRemoteSocketAddress());
// リクエスト送信
writer.write("ping...\n".getBytes());
writer.flush();
System.out.println("*** Testing start " + sock.getInetAddress() + " ***");
// レスポンス受信
int c = 0;
while((c = reader.read()) != -1) {
System.out.print((char)c);
}
System.out.println("*** Testing end***");
// 終了処理
reader.close();
writer.close();
sock.close();
System.out.println("*** SocketClient end ***");
} catch (IOException e) {
e.printStackTrace();
}
}
これで、一応は動きます。「一応」というのは下のソースでは、while文が終わらないという事象に見舞われました。以下の部分です。
while((c = reader.read()) != -1) {
System.out.print((char)c);
}
他のサイトを調べてみると、うまく動いているようですが、自分のところでは動きませんでした。
そして、悩んだ結果次のように修正しました。
// CRとCRLFの場合で入力が終了している時がある
while((read = in.read()) > 0) {
// 空の場合
if (read == 10 || read == 13) {
System.out.println("*** read break ***");
break;
}
char ch = (char) read;
inputTxt.append(ch);
}
受信したデータの終わりがわからない状態になってしまうのでループ処理が終わらなかったというわけです。
これを解消するために、終わりを示す「改行」文字をINT型で判別しています。intの10, 13はchar型の改行コードの値に相当します。つまり、(10)という値は、LF改行のことを指します。
なので、改行コードと取得した値が同じ場合は処理を抜けるというわけです。
if (read == 10 || read == 13) {
System.out.println("*** read break ***");
break;
}
そして、最終的にはこちらのようなコードになりました。全体はGithubにあります。
try {
System.out.println("*** サーバーソケット起動 ***");
Socket sock = server.accept();
// リクエスト
request = new BufferedReader(new InputStreamReader(sock.getInputStream()));
// レスポンス
response = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
// リクエストの読み込み
while(true) {
String inputTxt = readRequest(request);
if (inputTxt.contains("HTTP/1.1")) {
System.out.println("HTTP: " + inputTxt);
inputTxt = getHttpResponse(inputTxt);
} else if ("".equals(inputTxt) ) {
System.out.println("***> 空リクエスト: " + inputTxt);
inputTxt = "<html><body>Hello World!</body></html>";
} else {
System.out.println("***> else: " + inputTxt);
if (isBye(inputTxt)) {
break;
}
}
if (DEBUG) System.out.println("*** サーバーソケット: リクエスト受信 " + inputTxt + "***");
// レスポンスを返す
response.write(inputTxt + SEP);
response.flush();
System.out.println("*** サーバーソケット: レスポンス送信 " + inputTxt + "***");
inputTxt = null;
// デバック用コード
// request.close();
// response.close();
// sock.close();
// request = null;
// response = null;
// break;
}
} catch (IOException e) {
e.printStackTrace();
}
実行結果は、後日アップロードします。。。
でわでわ。。。