最終更新日:2021/01/30 原本2017-

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

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

DrawServerTcp.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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
周りの人とサーバ役とクライント役を交代しながら動作を確認してください.