Java ウェブサーバー作り〜Soket通信アプリ作成〜

イントロダクション

世の中では、「ウェブサーバー」というものが「普通」に存在しています。というか多くの人が「常識のように」言葉として使用します。

しかし、その中身はどうなっているのでしょうか?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();
}

処理としては、以下の通りです。

  1. サーバーソケットをポート番号"portNo"で作成
  2. サーバーソケットがバインドされていない場合は、新たにバインドし直し
  3. サーバーソケットのタイムアウト時間を3秒に設定

サーバーソケットの送受信処理

インスタンスを作成したら、クライアントとの送受信を行います。手順は以下の通りです。

  1. サーバーを起動する
  2. クライアントアプリでアクセスする

なので、先にサーバーを作成します。そして、一番下にメインメソッドを作っています。

/**
 * コンストラクタで作成されたサーバーソケットで
 * 受付処理を開始する。
 */
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();
}

次に、クライアントアプリの作成を行います。
クライアントの処理は、以下の通りです。

  1. クライアントソケットの作成(ローカルホスト、ポート番号指定)
  2. 受診するデータを準備(InputStream)
  3. 送信するデータを準備(OutputStream)
  4. リクエストの送信
// リクエスト送信
writer.write("ping...\n".getBytes());
writer.flush();
  1. サーバーのレスポンスを受信
            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();
}

実行結果は、後日アップロードします。。。

でわでわ。。。

関連記事

投稿者:

takunoji

音響、イベント会場設営業界からIT業界へ転身。現在はJava屋としてサラリーマンをやっている。自称ガテン系プログラマー(笑) Javaプログラミングを布教したい、ラスパイとJavaの相性が良いことに気が付く。 Spring framework, Struts, Seaser, Hibernate, Playframework, JavaEE6, JavaEE7などの現場経験あり。 SQL, VBA, PL/SQL, コマンドプロント, Shellなどもやります。

コメントを残す