Java ネットワーク ~Socket通信アプリを作る~

イントロダクション

以前から中途半端になっていた「プロコンゲーム」の作成に関して、進展がありました。
テキストRPGと合体させた形にしようと思いました。
詳細に関しては後日。。。

取り入れる要素としては、テキストRPGを取り入れることにしようと思います。

このテキストRPGとU16-プログラミングコンテストを組み合わせたようなものを作成しようと考えています。

ユースケースとしては、下のようなものです。

Socket通信について

JavaFXで作成した画面にクライアントアプリからアクセスします。
<JavaFX画面>

そのアクセス時に、使用するのがSocket通信です。
具体的には下のようなコードを書きます。

/**
 * クライアントのリクエストを受け付けるサーバー。
 * 通称プロコンサーバー
 *
 * @author 実装者の名前
 */
private class MainServer extends Thread {
    /** サーバ */
    private ServerSocket server;
    /** サーバー停止フラグ  */
    private boolean isStop;

    public MainServer() throws Exception {
        server = new ServerSocket(ProConServerConst.SERVER_PORT);
    }

    public void run() {
        isStop = false;
        ExecutorService service = Executors.newCachedThreadPool();

        try {
            while (isStop == false) {
                System.out.println("Run FxServer");
                Socket socket = server.accept();
                service.submit(new ProConServer(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            server = null;
        }
    }
    public void finalize() {
        try {
            if (server != null) server.close();
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            server = null;
        }
    }
}

finalizeメソッドはガベージコレクションでインスタンスが破棄されるときに動くメソッドなので、メモリの開放処理をいれています。

Executerクラス

Executerフレームワークを使用して、複数スレッドの処理を実行します。このときに使用している

ProConServer

クラスには、

Runable

インターフェースの実装と

Observable

クラスを継承しています。

これにより、ProConRPGServerクラスがクライアントから送信された初期情報を受け取ったときに受信したデータを

Observable

を継承した

ProConRPGLogic

クラスで受け取れるようにします。

図にすると下のようなイメージです。

  1. クライアントAPからアクセス
  2. MainServerでリクエスト受信
  3. ProConServerクラスを新しいスレッドで起動
  4. それぞれのスレッドでクライアントAPからのリクエストを待機、処理を行う

ProConRPGLogicクラス

コードは下のように定義します。

public class ProConRPGLogic extends Application implements Games, Observer {

このクラスは、インナークラスにMainServerを持っています。そして、このクラス自体は画面を表示します。JavaFXのメイン処理を行います。

つまり、下のようなイメージになります。

ProConRPGLogicクラスで画面を起動して、インナークラスのMainServerでSocket通信でデータを送ってくるクライアントアプリからのリクエストを待ちます。

実装としては、下のようなコードです。JavaFXのApplicationクラスを継承しているので、「start()」メソッドをオーバーライドして、画面を起動しまします。もちろん、呼び出し元では、「launch()」を呼びます。
<呼び出し元>

public static void main(String[] args) throws Exception {
    ProConRPGLogic logic = new ProConRPGLogic();
    ProConRPGEngine target = new ProConRPGEngine(logic);
    target.start();

}

<ProConRPGLogic>

@Override
public void start(Stage primaryStage) {
    try {
        exeServer();
    } catch (Exception e1) {
        e1.printStackTrace();
    }
    primaryStage.initStyle(StageStyle.TRANSPARENT);
    System.out.println("Hello");
    // FXMLのロード
    FXMLLoader loader = new FXMLLoader(getClass().getResource("/views/ProConTitleView.fxml"));
    VBox root = null;

    try {
        root = (VBox) loader.load();
        root.setStyle("-fx-background-color: transparent;");
    } catch (Exception e) {
        e.printStackTrace();
    }

    Scene scene = new Scene(root);
    scene.setFill(null);
    scene.getStylesheets().add(getClass().getResource("/views/ProConTitleView.css").toExternalForm());
    primaryStage.setTitle("Video Processing");
    primaryStage.setScene(scene);
    primaryStage.show();

}

まだ実装できていませんが、リクエストを受信後、黒い四角の部分にクライアントからのイメージを表示、そのほか名前などを表示し、スタートボタンでゲーム開始という形で実装しようと考えています。

そして、クライアントアプリに関して

クライアントAP

単純にSocketクラスを使用します。

/**
 * プロコンRPGを開始する。
 */
public void exevuteRpg() throws Exception {
    BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
    // 送受信するクラス
    ClientData data = createInitData();
    // 初期リクエストを送信する
    this.sendRequest(output, data);

    input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    System.out.println("exevuteRpg Client: " + this.getResponse(input));

    output = new ObjectOutputStream(socket.getOutputStream());
    this.sendRequest(new PrintWriter(socket.getOutputStream()), "bye");
    // サーバーとの通信処理
    while (isStop == false) {
        input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        output = new ObjectOutputStream(socket.getOutputStream());
        // サーバーからのレスポンスを取得する
            String response = this.getResponse(input);
            String request = handleResponse(response);
System.out.println("exevuteRpg Client Recieve: " + request);
        // 処理の待機処理(U16プロコンサーバーに習う)
//          Thread.sleep(ProConServerConst.WAIT);
        data.setCommand("bye");
        this.sendRequest(output, data);
        //isStop = true;
    }
}

このクラスも、サーバー側と同じような実装になります。localhostの2000番ポートにアクセスして、データをサーバーに送信、レスポンスを受信します。
<送信>

/**
 * リクエストをプロコンサーバーへ送信する。
 *
 * @param message
 * @throws IOException
 */
public void sendRequest(ObjectOutputStream request, ClientData message) throws IOException {
    if (isEmpty(message)) {
        throw new IOException("リクエストメッセージが入っていません");
    }
    request.writeObject(message);
    request.flush();
//      request.close();
}

初回送信時には、クラスをサーバーに送信するのでObjectOutputStreamを使用しました。

<受信>

/**
 * サーバーから受信したデータを受け取る。
 *
 * @param response
 * @return
 * @throws IOException
 */
public String getResponse(BufferedReader response) throws IOException {

    String line = null;
    StringBuilder build = new StringBuilder();
    int ch = -1;
    System.out.println("*** Client ***");
    while ((ch = response.read()) != -1) {
        build.append((char) ch);
        if (ch == 10 || ch == 13) {
            break;
        }
    }
    //response.close();
    System.out.println("Client Recieve: " + build.toString());
    return build.toString();
}

受信に関しては、レスポンスがテキストで帰ってくるように設計したのでBufferedReaderを使用しています。

注意点としては、サーバーとクライアントでINとOUTが混同しやすいというところです。
<クライアント>

  1. 送信:OutputSteamを使用
  2. 受信:InputStreamを使用する

<サーバー>

  1. 受信:OutputSteamを使用する
  2. 送信:InputStreamを使用する

処理の順番が逆になります。自分は結構混乱しました。。。

イメージの送受信

Socketでのデータ送信(ネットワーク経由のデータ送信)では、データを一度直列化する必要があります。なので、送信するデータクラスは下のようにSerializableインターフェースを実装する日宇町があります。

public class SocketTest implements Serializable { ... }

そして、イメージなどの大きなデータはbyte配列で送信する必要があります。具体的に下のように書きます。ちなみに、lombokを使用しているのでgetter setterは実装していません。

<データクラス>

/**
 * クライアント(ゲームプレーヤー)のデータクラス。
 *
 * @author 実装者の名前
 *
 */
@Data
public class ClientData implements Serializable {
    /** アクセスコード */
    private String accessCd;
    /** プレーヤー番号 */
    private int playerNo;
    /** プレーヤーの名前 */
    private String name;
    /** 生年月日 */
    private String birthDay;
    /** 32 x 32の画像データ(透過PNG) */
    byte[] imageByte;
    /** 32 x 32の画像データ(透過PNG) */
    BufferedImage image;

    /** 送信するコマンド */
    private String command;
}</pre>

<イメージの変換処理>
<pre>/**
 * イメージファイルを取得して、バイト配列に変換する
 * 取得するイメージファイルは、src/main/resources/char以下のPNGファイル)
 *
 * @param fileName ファイルの名前(拡張子は除く)
 * @return
 */
protected byte[] imageToByte(String fileName) {
    byte[] result = null;
    try {
        URL imgUrl = Paths.get("resources/char_img/" + fileName + ".png").toUri().toURL();
        BufferedImage img = ImageIO.read(imgUrl);
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        // イメージをバイト配列に書き込み
        ImageIO.write(img, "png", outStream);

        result = outStream.toByteArray();
        //result = ByteBuffer.allocate(ProConServerConst.MAX_IMG_SIZE).putInt(outStream.size()).array();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

<バイト配列からイメージ>

// バイト配列からBufferedImage
BufferedImage image = ImageIO.read(new ByteArrayInputStream(playerImage));

マルチスレッド処理

このゲームでは複数のプレーヤーが遊ぶ予定です。なので、複数のプレーヤーを受け付けるための実装が必要になります。

クライアントからの受付をする

Socket通信なので、受付待機と受付後の処理を実装します。下のメソッドはThreadクラスのrunメソッドをオーバーライドして実装していますので、複数スレッドでの実行が可能となっています。

@Override
public void run() {
    isStop = false;
    ExecutorService service = Executors.newFixedThreadPool(4);

    try {
        System.out.print("Run FxServer");
        Socket socket1 = server1.accept();
        System.out.println(" accept!");

        ProConServer pro1 = new ProConServer(socket1, logic);

        service.submit(pro1);

        // ProConServerからの通知待機
        synchronized(pro1) {
            System.out.println("sync MainServer");
            while(pro1.isReady() == false) {
                pro1.wait();
            }
            Platform.runLater(() -> {
                try {
                    logic.setClientData(pro1.getFirstRequest(), ProConServerConst.PLAYER1_NO);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (server1 != null) server1.close();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
}

ポイントとしては、Executorクラスを使用しているので、複数スレッドでの処理が簡単に実装できるというところです。
<通知処理>

// 通知
synchronized(this) {
    System.out.println("sync ProConServer");
    setReady(true);
    notify();
}

<待機処理>

// ProConServerからの通知待機
synchronized(pro1) {
    System.out.println("sync MainServer");
    while(pro1.isReady() == false) {
        pro1.wait();
    }
    Platform.runLater(() -> {
        try {
            logic.setClientData(pro1.getFirstRequest(), ProConServerConst.PLAYER1_NO);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}

そして、下のコードでは、他のクラスの処理の完了町を行う処理を実装しています。
Observerパターンを使用(java.util.current.Observable)を使用した方法でも実装できそうでしたが、うまくいきませんでした。

でわでわ。。。

関連ページ一覧

[Eclipse セットアップ](http://zenryokuservice.com/wp/2020/09/01/%e9%96%8b%e7%99%ba%e7%92%b0%e5%a2%83%e6%a7%8b%e7%af%89%ef%bd%9ewindows%e7%89%88eclipse%e3%81%ae%e8%a8%ad%e5%ae%9a%ef%bd%9e/)

  1. Java Install Eclipse〜開発ツールのインストール〜
  2. TensorFlow C++環境〜EclipseCDTをインストール〜
  3. Setup OpenGL with JavaJOGLを使う準備 for Eclipse
  4. Eclipse Meven 開発手順〜プロジェクトの作成〜
  5. Java OpenCV 環境セットアップ(on Mac)
  6. Eclipse SceneBuilderを追加する
  7. JavaFX SceneBuilder EclipseSceneBuilder連携~

Java Basic一覧

  1. Java Basic Level 1 〜Hello Java〜
  2. Java Basic Level2 〜Arithmetic Calculate〜
  3. Java Basic Level3 〜About String class〜
  4. Java Basic Level 4〜Boolean〜
  5. Java Basic Level 5〜If Statement〜
  6. Java Basic Summary from Level1 to 5
  7. Java Basic Level 6 〜Traning of If statement〜
  8. Java Basic Level8 〜How to use for statement〜
  9. Java Basic Level 8.5 〜Array〜
  10. Java Basic Level 9〜Training of for statement〜
  11. Java Basic Level 10 〜While statement 〜
  12. Java Basic Swing〜オブジェクト指向〜
  13. Java Basic Swing Level 2〜オブジェクト指向2〜
  14. サンプル実装〜コンソールゲーム〜
  15. Java Basic インターフェース・抽象クラスの作り方
  16. Java Basic クラスとは〜Step2_1〜
  17. Java Basic JUnit 〜テストスイートの作り方〜

Git関連

  1. Java Git clone in Eclipse 〜サンプルの取得〜
  2. Eclipse Gitリポジトリの取得 GitからソースをPullしよう〜
  3. IntelliJ IDEA GitGitリポジトリからクローン〜

JavaFX関連ページ

  1. Eclipse SceneBuilderを追加する
  2. JavaFX SceneBuilder 〜EclipseとSceneBuilder連携~
  3. JavaFX SceneBuilder〜ボタンにメソッドを割り当てるワンポイント〜
  4. Java プロコンゲーム 〜見た目の作成(SceneBuilderの使用)〜

ステップアップ関連ページ一覧

  1. Java 初めてでも大丈夫〜ステップアッププログラミングのススメ〜
  2. ステップアッププログラミング〜Java FxでHelloWorld解説〜
  3. Java StepUpPrograming〜JavaFX で四則計算〜
  4. Java StepUpPrograming〜JavaFXで画面切り替えを作る1〜
  5. Java StepUpPrograming〜JavaFXで画面切り替え2ボタン作成〜
  6. Java StepUpPrograming〜JavaFXで画面切り替え3アクション〜
  7. Java StepUpPrograming〜JavaFXで画面切り替え4Pane切り替え〜
  8. Java StepUpPrograming〜JavaFXで画面切り替え5WebEngine

JavaFX + ND4Jで機械学習準備

  1. JavaFX + ND4J〜数学への挑戦1:ND4Jのインストール〜
  2. JavaFX + ND4J〜数学への挑戦2: 行列の計算〜
  3. Java + ND4J 〜数学への挑戦3: ベクトル(配列)の作成方法〜

オブジェクト指向関連ページ

  1. [オブジェクト指向の概念1〜OracleDocのチュートリアル1〜](https://zenryokuservice.com/wp/2019/10/301. /%e3%82%aa%e3%83%96%e3%82%b8%e3%82%a7%e3%82%af%e3%83%88%e6%8c%87%e5%90%91%e3%81%ae%e6%a6%82%e5%bf%b5-%e3%80%9coracledoc%e3%81%ae%e3%83%81%e3%83%a5%e3%83%bc%e3%83%88%e3%83%aa%e3%82%a2%e3%83%ab%ef%bc%91/)
  2. オブジェクト指向の概念2〜クラスとは〜

Java Discord

  1. IntelliJ IDEA Discord Botを作る〜Gradle環境のセットアップ〜
  2. Java Discord セットアップ〜Hello Discord〜
  3. Java Discord ピンポン〜Discordプログラム〜
  4. Java Discord Listener実装〜コマンドを好きなだけ追加しよう〜

投稿者:

takunoji

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

コメントを残す