協調お絵かきシステムの作成

下記のDrawServerTcp.javaとDrawClientTcp.javaをコピーし,動作を確認してみましょう.

DrawServerTcp.java

package jp.ac.utsunomiya_u.is;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

public class DrawServerTcp {

    // DrawTaskのリスト
    private final ArrayList<DrawTask> drawTasks = new ArrayList<>();

    public static void main(String[] args) {
        DrawServerTcp drawServer = new DrawServerTcp();
    }

    public DrawServerTcp() {
        // Scannerクラスのインスタンス(標準入力System.inからの入力)
        try (Scanner scanner = new Scanner(System.in)) {
            System.out.print("DrawServerTcp (" + getMyIpAddress() + ") > Input server port > ");
            // ポート番号入力
            int port = scanner.nextInt();
            // スレッドプールの生成
            ExecutorService executorService = Executors.newCachedThreadPool();
            // ServerSocketクラスのインスタンスをポート番号を指定して生成
            try (ServerSocket serverSocket = new ServerSocket(port)) {
                System.out.println("DrawServerTcp (" + getMyIpAddress() + ") > Started and Listening for connections on port " + serverSocket.getLocalPort());
                while (true) {
                    // ServerSocketに対する要求を待機し,それを受け取ったらSocketクラスのインスタンスからChatTaskを生成
                    DrawTask drawTask = new DrawTask(serverSocket.accept());
                    // ChatTaskのインスタンスをリストに保管
                    drawTasks.add(drawTask);
                    // タスクの実行
                    executorService.submit(drawTask);
                }
            } catch (IOException ex) {
                Logger.getLogger(DrawServerTcp.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                // スレッドプールの停止
                executorService.shutdown();
            }
        }
    }

    /**
     * 自ホストのIPアドレス取得
     *
     * @return 自ホストのIPアドレス
     */
    private static String getMyIpAddress() {
        try {
            // 自ホストのIPアドレス取得
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException ex) {
            Logger.getLogger(DrawServerTcp.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

    /**
     * メッセージの同報通知(サーバ->クライアント)
     *
     * @param message メッセージ
     */
    private synchronized void broadcast(String message) {
        // ChatTashのArrayListの各要素に対して
        drawTasks.forEach((drawTask) -> {
            // PrintWriter経由でメッセージ送信
            drawTask.getPrintWriter().println(message);
        });
    }

    private final class DrawTask implements Callable<Void> {

        // ソケット
        private Socket socket;
        // データ送信用Writer(サーバ->クライアント用)
        private PrintWriter writer = null;
        // データ受信用Reader(クライアント->サーバ用)
        private BufferedReader reader = null;

        /**
         * コンストラクタ
         *
         * @param socket Socketクラスのインスタンス
         */
        DrawTask(Socket socket) {
            this.socket = socket;
            try {
                // Socket経由での書込用PrintWriter生成(サーバ->クライアント用)
                writer = new PrintWriter(socket.getOutputStream(), true);
                // Soket経由での読込用BufferedReader生成(クライアント->サーバ用)
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                System.out.println("DrawServerTcp (" + getMyIpAddress() + ") > Accepted connection from " + socket.getRemoteSocketAddress());
            } catch (IOException ex) {
                Logger.getLogger(DrawServerTcp.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        /**
         * PrintWriterのゲッタ
         *
         * @return PrintWriter
         */
        public PrintWriter getPrintWriter() {
            return writer;
        }

        @Override
        public Void call() {
            try {
                String inputLine;
                // readerから一行読み込み
                while ((inputLine = reader.readLine()) != null) {
                    // 受信文をそのまま同報通知
                    broadcast(inputLine);
                    //System.out.println("DrawClientTcp (" + socket.getRemoteSocketAddress() + ") > " + inputLine);
                }
            } catch (IOException ex) {
                Logger.getLogger(DrawServerTcp.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                System.out.println("DrawServerTcp (" + getMyIpAddress() + ") > Terminated connection from " + socket.getRemoteSocketAddress());
                try {
                    // socket, reader, writerのclose
                    if (socket != null) {
                        socket.close();
                    }
                    if (reader != null) {
                        reader.close();
                    }
                    if (writer != null) {
                        writer.close();
                    }
                } catch (IOException ex) {
                    Logger.getLogger(DrawServerTcp.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            // ChatTaskのリストから自身を削除         
            drawTasks.remove(this);
            return null;
        }
    }
}

DrawClientTcp.java

package jp.ac.utsunomiya_u.is;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class DrawClientTcp extends Application {

    // ルートノード
    private final Group root = new Group();
    // ソケット
    private Socket socket = null;
    // データ送信用Writer
    private PrintWriter writer = null;
    // データ受信用Reader
    private BufferedReader reader = null;
    // 受信タスク
    private ReceiverTask receiverTask = null;

    @Override
    public void start(Stage primaryStage) throws Exception {
        // Stageのタイトル
        primaryStage.setTitle(getClass().getName());
        // Stageのサイズ
        primaryStage.setWidth(800);
        primaryStage.setHeight(800);
        // Stageの終了ボタンが押下された時の対応
        primaryStage.setOnCloseRequest((event) -> {
            exit();
        });
        // Sceneインスタンス生成
        Scene scene = new Scene(root);
        // Scene上でマウスがドラッグされた時の動作
        scene.setOnMouseDragged((event) -> {
            if (socket != null && socket.isConnected()) {
                // マウスの座標から文字列生成
                String str = event.getX() + "," + event.getY();
                for (int i = 0; i < 1000; ++i) {
                    str += "," + event.getX() + "," + event.getY();
                }
                // サーバに情報送信
                writer.println(str);
                // マウス座標を中心とする赤い円を描画
                root.getChildren().add(new Circle(event.getX(), event.getY(), 2, Color.RED));
            }
        });
        // StageにSceneを貼付け
        primaryStage.setScene(scene);

        // IPアドレスとポート番号設定用ペインを作成し,ルートに貼付け        
        root.getChildren().add(new NetworkConfigurePane());

        // Stageの表示
        primaryStage.show();
    }

    /**
     * ”退室”ボタンが押下された時の処理
     */
    private void exit() {
        // タスクをキャンセル
        receiverTask.cancel();
        try {
            // SocketとReaderとWriterをclose
            if (socket != null) {
                socket.close();
            }
            if (reader != null) {
                reader.close();
            }
            if (writer != null) {
                writer.close();
            }
        } catch (IOException ex) {
            new Alert(Alert.AlertType.ERROR, "ソケットを閉じることが出来ません", ButtonType.OK).show();
        }
    }

    private class NetworkConfigurePane extends GridPane {

        //  操作可能コンポーネント
        private final TextField textFieldServerIpAddress = new TextField("localhost");
        private final TextField textFieldServerPortNumber = new TextField("10000");
        private final Button buttonEnter = new Button("入室");
        private final Button buttonExit = new Button("退室");

        public NetworkConfigurePane() {
            // 各コンポーネントの生成
            Label labelIpAddress = new Label("サーバIPアドレス:");
            Label labelPortNumber = new Label("サーバポート番号:");
            // 各コンポーネントの配置
            GridPane.setConstraints(labelIpAddress, 0, 0);
            GridPane.setConstraints(textFieldServerIpAddress, 1, 0);
            GridPane.setConstraints(labelPortNumber, 0, 1);
            GridPane.setConstraints(textFieldServerPortNumber, 1, 1);
            GridPane.setConstraints(buttonEnter, 2, 1);
            GridPane.setConstraints(buttonExit, 3, 1);
            // GridPaneに各コンポーネント追加
            getChildren().addAll(labelIpAddress, textFieldServerIpAddress, labelPortNumber, textFieldServerPortNumber, buttonEnter, buttonExit);
            //  コンポーネント表示設定切替
            setConnection(true);
            // 
            buttonEnter.setOnAction((event) -> {
                try {
                    // textFiledServerIpAddressテキストフィールドに入力された文字列からサーバIPアドレスを指定
                    InetAddress serverInetAddress = InetAddress.getByName(textFieldServerIpAddress.getText());
                    // textFieldServerPortNumberテキストフィールドに入力された文字列からサーバポート番号を指定
                    int serverPortNumber = Integer.valueOf(textFieldServerPortNumber.getText());
                    // Socket生成
                    socket = new Socket(serverInetAddress, serverPortNumber);
                    // Socket経由での書込用PrintWriter生成(クライアント->サーバ用)
                    writer = new PrintWriter(socket.getOutputStream(), true);
                    // Soket経由での読込用BufferedReader生成(サーバ->クライアント用)
                    reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    // スレッドプールをSingleThreadで生成
                    ExecutorService executorService = Executors.newSingleThreadExecutor();
                    // TaskReceiver生成
                    receiverTask = new ReceiverTask();
                    // タスク実行
                    executorService.submit(receiverTask);
                    // スレッドプールを停止
                    executorService.shutdown();
                } catch (UnknownHostException ex) {
                    new Alert(Alert.AlertType.ERROR, "IPアドレスが不正です", ButtonType.OK).show();
                } catch (NumberFormatException ex) {
                    new Alert(Alert.AlertType.ERROR, "ポート番号が不正です", ButtonType.OK).show();
                } catch (IOException ex) {
                    new Alert(Alert.AlertType.ERROR, "サーバに接続出来ません", ButtonType.OK).show();
                }
                if (socket != null && socket.isConnected()) {
                    setConnection(false);
                }
            });
            //  ”退室”ボタンが押下された時の処理
            buttonExit.setOnAction((event) -> {
                exit();
                if (socket == null || socket.isClosed()) {
                    setConnection(true);
                }
            });
        }

        /**
         * 各コンポーネントの変更可否
         *
         * @param connected 接続時を意味するフラグ
         */
        private void setConnection(boolean connected) {
            textFieldServerIpAddress.setDisable(!connected);
            textFieldServerPortNumber.setDisable(!connected);
            buttonEnter.setDisable(!connected);
            buttonExit.setDisable(connected);
        }
    }

    /**
     * サーバからのメッセージを受信するためのタスク
     */
    private class ReceiverTask extends Task<Void> {

        @Override
        protected Void call() throws Exception {
            String inputLine;
            while ((inputLine = reader.readLine()) != null) {
                // 受信データを","で分解              
                String[] position = inputLine.split(",");
                Platform.runLater(() -> {
                    // 1番目の要素を中心のX座標,2番目の要素を中心のY座標とする青い円を描画
                    root.getChildren().add(new Circle(Double.valueOf(position[0]), Double.valueOf(position[1]), 2, Color.BLUE));
                });
            }
            return null;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

協調お絵かきシステムの動作例

番号説明サーバクライアント1クライアント2
1サーバ(ここではポート番号10000)を起動します
DrawServerTcp (192.168.11.100) > Input server port > 10000
DrawServerTcp (192.168.11.100) > Started and Listening for connections on port 10000
2クライアント(クライアント1と呼びます)を起動します.サーバのIPアドレスは"localhost",ポート番号は10000番で,”入室”ボタンを押します
3サーバでは接続が確認出来ます.クライアント1のフィールドでマウスをドラッグしながら動かしてみましょう.
DrawServerTcp (192.168.11.100) > Accepted connection from /127.0.0.1:52782
4もう一つ別のクライアント(クライアント2と呼びます)を起動します.サーバのIPアドレスは"localhost",ポート番号は10000番で,”入室”ボタンを押します
5サーバでは接続が確認出来ます.クライアント1のフィールドでマウスをドラッグしながら動かしてみましょう.
DrawServerTcp (192.168.11.100) > Accepted connection from /127.0.0.1:52782
6クライアントのウィンドウを右上の閉じるボタンで閉じると,サーバでクライアントが切断された事が確認できます
DrawServerTcp (192.168.11.100) > Terminated connection from /127.0.0.1:52782
DrawServerTcp (192.168.11.100) > Terminated connection from /127.0.0.1:52792
周りの人とサーバ役とクライント役を交代しながら動作を確認してください.