ChatServer.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 ChatServer { // ChatTaskのリスト private final ArrayList<ChatTask> chatTasks = new ArrayList<>(); public static void main(String[] args) { ChatServer chatServer = new ChatServer(); } /** * コンストラクタ */ public ChatServer() { // Scannerクラスのインスタンス(標準入力System.inからの入力) try (Scanner scanner = new Scanner(System.in)) { System.out.print("ChatServer (" + getMyIpAddress() + ") > Input server port > "); // ポート番号入力 int port = scanner.nextInt(); // スレッドプールの生成 ExecutorService executorService = Executors.newCachedThreadPool(); // ServerSocketクラスのインスタンスをポート番号を指定して生成 try (ServerSocket serverSocket = new ServerSocket(port)) { System.out.println("ChatServer (" + getMyIpAddress() + ") > Started and Listening for connections on port " + serverSocket.getLocalPort()); while (true) { // ServerSocketに対する要求を待機し,それを受け取ったらSocketクラスのインスタンスからChatTaskを生成 ChatTask chatTask = new ChatTask(serverSocket.accept()); // ChatTaskのインスタンスをリストに保管 chatTasks.add(chatTask); // タスクの実行 executorService.submit(chatTask); } } catch (IOException ex) { Logger.getLogger(ChatServer.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(ChatServer.class.getName()).log(Level.SEVERE, null, ex); } return null; } /** * メッセージの同報通知(サーバ->クライアント) * * @param message メッセージ */ private synchronized void broadcast(String message) { // ChatTashのArrayListの各要素に対して chatTasks.forEach((chatTask) -> { // PrintWriter経由でメッセージ送信 chatTask.getPrintWriter().println(message); }); } private final class ChatTask implements Callable<Void> { // ソケット private Socket socket; // データ送信用Writer(サーバ->クライアント用) private PrintWriter writer = null; // データ受信用Reader(クライアント->サーバ用) private BufferedReader reader = null; // ニックネーム private String nickname = null; /** * コンストラクタ * * @param socket Socketクラスのインスタンス */ ChatTask(Socket socket) { this.socket = socket; try { // Socket経由での書込用PrintWriter生成(サーバ->クライアント用) writer = new PrintWriter(socket.getOutputStream(), true); // Soket経由での読込用BufferedReader生成(クライアント->サーバ用) reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); // クライアントから接続直後に送られてくるニックネームを取得 nickname = reader.readLine(); // 入室状況の文言作成 String str = ""; if (chatTasks.size() > 0) { for (ChatTask chatTask : chatTasks) { str += nickname + "[" + socket.getRemoteSocketAddress() + "]さん "; } str += "が入室しています"; } else { str = "誰も入室していません"; } // 入室状況の文言送信 writer.println(str); // 入室したことを他のユーザに同報通知 broadcast(nickname + "[" + socket.getRemoteSocketAddress() + "]さんが入室しました"); System.out.println("ChatServer (" + getMyIpAddress() + ") > Accepted connection from " + socket.getRemoteSocketAddress() + "[" + nickname + "]"); } catch (IOException ex) { Logger.getLogger(ChatServer.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(nickname + "[" + socket.getRemoteSocketAddress() + "] > " + inputLine); System.out.println("ChatClient (" + socket.getRemoteSocketAddress() + ") > " + inputLine); } } catch (IOException ex) { Logger.getLogger(ChatServer.class.getName()).log(Level.SEVERE, null, ex); } finally { // 退出した事を同報通知 broadcast(nickname + "[" + socket.getRemoteSocketAddress() + "]さんが退出しました"); System.out.println("ChatServer (" + getMyIpAddress() + ") > Terminated connection from " + socket.getRemoteSocketAddress() + "[" + nickname + "]"); 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(ChatServer.class.getName()).log(Level.SEVERE, null, ex); } } // ChatTaskのリストから自身を削除 chatTasks.remove(this); return null; } } }
ChatClient.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.concurrent.Task; import javafx.event.ActionEvent; import javafx.geometry.Pos; 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.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class ChatClient extends Application { // ソケット private Socket socket = null; // データ送信用Writer private PrintWriter writer = null; // データ受信用Reader private BufferedReader reader = null; // 受信タスク private TaskReceiver taskReceiver = null; // 操作可能コンポーネント private TextField textFieldServerIpAddress = null; private TextField textFieldServerPortNumber = null; private TextField textFieldNickname = null; private Button buttonEnter = null; private Button buttonExit = null; private TextArea textAreaView = null; private TextField textFieldMessage = null; private Button buttonMessage = null; @Override public void start(Stage primaryStage) throws Exception { // Stageのタイトル primaryStage.setTitle(getClass().getName()); // Stageのサイズ primaryStage.setWidth(400); primaryStage.setHeight(300); // ルートノード(VBox:単一の垂直列に子をレイアウト) VBox root = new VBox(); // Sceneを介してルートノードをStateに貼付け primaryStage.setScene(new Scene(root)); // Stageの終了ボタンが押下された時の対応 primaryStage.setOnCloseRequest((event) -> { exit(); }); // [第1段] IPアドレス,ポート番号,ハンドル名の設定 // GridPaneレイアウト(行と列の柔軟なグリッド内に子をレイアウト) GridPane gridPaneConfig = new GridPane(); gridPaneConfig.setAlignment(Pos.CENTER); // 各コンポーネントの生成 Label labelIpAddress = new Label("サーバIPアドレス:"); textFieldServerIpAddress = new TextField(""); Label labelPortNumber = new Label("サーバポート番号:"); textFieldServerPortNumber = new TextField(""); Label labelNickname = new Label("ニックネーム:"); textFieldNickname = new TextField(""); buttonEnter = new Button("入室"); buttonExit = new Button("退室"); // 各コンポーネントの配置 GridPane.setConstraints(labelIpAddress, 0, 0); GridPane.setConstraints(textFieldServerIpAddress, 1, 0); GridPane.setConstraints(labelPortNumber, 0, 1); GridPane.setConstraints(textFieldServerPortNumber, 1, 1); GridPane.setConstraints(labelNickname, 0, 2); GridPane.setConstraints(textFieldNickname, 1, 2); GridPane.setConstraints(buttonEnter, 2, 1); GridPane.setConstraints(buttonExit, 3, 1); // GridPaneに各コンポーネント追加 gridPaneConfig.getChildren().addAll(labelIpAddress, textFieldServerIpAddress, labelPortNumber, textFieldServerPortNumber, labelNickname, textFieldNickname, buttonEnter, buttonExit); // buttonEnterボタンが押下された時の動作 buttonEnter.setOnAction((ActionEvent event) -> { try { // textFiledServerIpAddressテキストフィールドに入力された文字列からサーバIPアドレスを指定 InetAddress serverInetAddress = InetAddress.getByName(textFieldServerIpAddress.getText()); // textFieldServerPortNumberテキストフィールドに入力された文字列からサーバポート番号を指定 int serverPortNumber = Integer.valueOf(textFieldServerPortNumber.getText()); // textFiledNicknameテキストフィールドに入力された文字列からニックネームを指定 String nickname = textFieldNickname.getText(); // ニックネームの長さが0なら if (nickname.length() == 0) { new Alert(Alert.AlertType.ERROR, "ハンドル名が空です", ButtonType.OK).show(); } else { try { // Socket生成 socket = new Socket(serverInetAddress, serverPortNumber); // Socket経由での書込用PrintWriter生成(クライアント->サーバ用) writer = new PrintWriter(socket.getOutputStream(), true); // Soket経由での読込用BufferedReader生成(サーバ->クライアント用) reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 接続直後にサーバにニックネームを通知 writer.println(nickname); // コンポーネットの表示切替 setComponents(true); // スレッドプールをSingleThreadで生成 ExecutorService executorService = Executors.newSingleThreadExecutor(); // TaskReceiver生成 taskReceiver = new TaskReceiver(); // タスク実行 executorService.submit(taskReceiver); // スレッドプールを停止 executorService.shutdown(); } catch (IOException ex) { // ダイアログ(エラー用)生成 new Alert(Alert.AlertType.ERROR, "サーバに接続出来ません", ButtonType.OK).show(); } } } catch (UnknownHostException ex) { new Alert(Alert.AlertType.ERROR, "IPアドレスが不正です", ButtonType.OK).show(); } catch (NumberFormatException ex) { new Alert(Alert.AlertType.ERROR, "ポート番号が不正です", ButtonType.OK).show(); } }); // [第2段] チャット内容の表示ビュー // TextArea生成 textAreaView = new TextArea(); // 表示用なので,書込不可 textAreaView.setEditable(false); // [第3段] メッセージ入力 // HBoxレイアウト(単一の水平行に子をレイアウト) HBox hBoxMessage = new HBox(); hBoxMessage.setAlignment(Pos.CENTER); hBoxMessage.setSpacing(10); // 各コンポーネントの生成 textFieldMessage = new TextField(); buttonMessage = new Button("送信"); // HBoxレイアウトに各コンポーネントを貼付け hBoxMessage.getChildren().addAll(textFieldMessage, buttonMessage); // buttonMessageボタンが押下された時の動作 buttonMessage.setOnAction((ActionEvent event) -> { // textFieldMessageテキストフィールドに入力された文字列を取得 String inputLine = textFieldMessage.getText(); // 入力文字列長が0より大きい if (inputLine.length() > 0) { // PrintWriterに書込(クライアント->サーバ) writer.println(inputLine); } // textFiledMessageテキストフィールドをクリア textFieldMessage.clear(); }); // コンポーネントの表示切替(初期化) setComponents(false); // 3段のレイアウトをルートノードに貼付け root.getChildren().addAll(gridPaneConfig, textAreaView, hBoxMessage); // Stageの表示 primaryStage.show(); } /** * ”退室”ボタンが押下された時の処理 */ private void exit() { // タスクをキャンセル taskReceiver.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(); } } /** * 各コンポーネントの変更可否 * * @param connected 接続時を意味するフラグ */ private void setComponents(boolean connected) { // サーバ接続時に無効 textFieldServerIpAddress.setDisable(connected); textFieldServerPortNumber.setDisable(connected); textFieldNickname.setDisable(connected); buttonEnter.setDisable(connected); // サーバ接続時に有効 buttonExit.setDisable(!connected); textAreaView.setDisable(!connected); textFieldMessage.setDisable(!connected); buttonMessage.setDisable(!connected); } /** * サーバからのメッセージを受信するためのタスク */ private class TaskReceiver extends Task<Void> { @Override protected Void call() throws Exception { String inputLine; // Readerから読み込んだ一行をTextAreaに追記 while ((inputLine = reader.readLine()) != null) { textAreaView.appendText(inputLine + "\n"); } return null; } } public static void main(String[] args) { launch(args); } }