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);
}
}