Ver código fonte

feat:客户端GUI实现群聊,群聊信息撤回,私聊,UDP推送用户列表信息

yang yi 2 meses atrás
pai
commit
78fec05a9c
22 arquivos alterados com 1469 adições e 225 exclusões
  1. 16 0
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/Application.java
  2. 2 3
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/Client.java
  3. 0 126
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ReadHandler.java
  4. 172 0
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/core/Client.java
  5. 216 0
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/core/ReadHandler.java
  6. 40 0
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/core/WriterHandler.java
  7. 60 0
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/ClientApplication.java
  8. 259 0
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/controller/ClientController.java
  9. 77 0
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/controller/LoginController.java
  10. 155 0
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/controller/PrivateChatController.java
  11. 75 0
      chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/controller/StartController.java
  12. 80 0
      chat-gwng/chat-client/src/main/resources/fxml/client.fxml
  13. 96 0
      chat-gwng/chat-client/src/main/resources/fxml/index.fxml
  14. 20 0
      chat-gwng/chat-client/src/main/resources/fxml/loginScene.fxml
  15. 29 0
      chat-gwng/chat-client/src/main/resources/fxml/privateChatScene.fxml
  16. 6 0
      chat-gwng/chat-commom/src/main/java/space/anyi/chatCommom/Message.java
  17. 22 3
      chat-gwng/chat-server/src/main/java/space/anyi/chatServer/core/ChatServer.java
  18. 32 24
      chat-gwng/chat-server/src/main/java/space/anyi/chatServer/core/ReadHandler.java
  19. 24 0
      chat-gwng/chat-server/src/main/java/space/anyi/chatServer/core/WriteHandler.java
  20. 79 51
      chat-gwng/chat-server/src/main/java/space/anyi/chatServer/ui/controller/ServerController.java
  21. 7 15
      chat-gwng/chat-server/src/main/java/space/anyi/chatServer/ui/controller/StartController.java
  22. 2 3
      chat-gwng/chat-server/src/main/resources/fxml/server.fxml

+ 16 - 0
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/Application.java

@@ -0,0 +1,16 @@
+package space.anyi.chatClient;
+
+import space.anyi.chatClient.ui.ClientApplication;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: Application
+ * @Author: 杨逸
+ * @Data:2025/9/26 19:55
+ * @Description:
+ */
+public class Application {
+    public static void main(String[] args) {
+        ClientApplication.launch(ClientApplication.class,args);
+    }
+}

+ 2 - 3
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/Client.java

@@ -1,16 +1,15 @@
 package space.anyi.chatClient;
 
+import space.anyi.chatClient.core.ReadHandler;
 import space.anyi.chatCommom.Encoder;
 import space.anyi.chatCommom.Message;
 
-import java.awt.*;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.channels.SelectionKey;
 import java.nio.channels.Selector;
 import java.nio.channels.SocketChannel;
-import java.nio.charset.StandardCharsets;
 import java.util.Scanner;
 
 /**
@@ -33,7 +32,7 @@ public class Client {
         //读
         Selector selector = Selector.open();
         socketChannel.register(selector, SelectionKey.OP_READ);
-        ReadHandler readHandler = new ReadHandler(selector);
+        ReadHandler readHandler = new ReadHandler(selector,null,null);
         new Thread(readHandler).start();
         boolean nameFlag = false;
         Scanner scanner = new Scanner(System.in);

+ 0 - 126
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ReadHandler.java

@@ -1,126 +0,0 @@
-package space.anyi.chatClient;
-
-import space.anyi.chatCommom.Encoder;
-import space.anyi.chatCommom.Message;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.net.SocketException;
-import java.nio.ByteBuffer;
-import java.nio.channels.SelectableChannel;
-import java.nio.channels.SelectionKey;
-import java.nio.channels.Selector;
-import java.nio.channels.SocketChannel;
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * @ProjectName: chat-gwng
- * @FileName: ReadHandler
- * @Author: 杨逸
- * @Data:2025/9/22 19:20
- * @Description:
- */
-public class ReadHandler implements Runnable, Closeable {
-    private Selector readSelector;
-    private boolean isExit = false;
-
-    public ReadHandler(Selector readSelector) {
-        this.readSelector = readSelector;
-    }
-
-    @Override
-    public void close() throws IOException {
-        exit();
-        System.out.println("close readHandler");
-        if (Objects.nonNull(readSelector) && readSelector.isOpen()) {
-            readSelector.close();
-        }
-    }
-
-    public void exit() {
-        isExit = true;
-    }
-
-    @Override
-    public void run() {
-        System.out.println("readHandler starter");
-        while(true){
-            if (isExit)break;
-            int select = 0;
-            try {
-                select = readSelector.select(100);
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-            if (select > 0) {
-                Set<SelectionKey> selectionKeys = readSelector.selectedKeys();
-                for (SelectionKey selectionKey : selectionKeys) {
-                    if (selectionKey.isReadable() && selectionKey.isValid()) {
-                        //拿到channel
-                        SelectableChannel channel = selectionKey.channel();
-                        SocketChannel socketChannel = (SocketChannel) channel;
-                        //读取数据
-                        ByteBuffer byteBuffer = ByteBuffer.allocate(4);
-                        int len = 0;
-                        try {
-                            len = socketChannel.read(byteBuffer);
-                            if (len > 0) {
-                                byteBuffer.flip();
-                                int size = byteBuffer.getInt();
-                                byteBuffer = ByteBuffer.allocate(size);
-                                socketChannel.read(byteBuffer);
-                                //byteBuffer.flip();
-                                Message message = Encoder.decode(byteBuffer);
-                                if (Message.MessageType.SYSTEM == message.getType()){
-                                    //系统信息
-                                    int operate = message.getOperate();
-                                    switch(operate){
-                                        case Message.OperateType.LOGIN :
-                                            //登陆操作
-                                            //todo
-                                            break;
-                                        case Message.OperateType.LOGOUT :
-                                            //登出操作
-                                            //todo
-                                            break;
-                                        default:
-                                            System.err.println("未定义的操作"+message);
-                                            break;
-                                    }
-                                }else{
-                                    //用户信息
-                                    //todo
-                                    System.out.println(message.getSource() + ":" + message.getContent());
-                                }
-
-                            }
-                        } catch (SocketException e) {
-                            //连接意外中断导致异常
-                            //关闭客户端
-                            exit();
-                            e.printStackTrace();
-                            selectionKey.cancel();
-                            try {
-                                //关闭相关的channel
-                                socketChannel.close();
-                            } catch (IOException ex) {
-                                ex.printStackTrace();
-                            }
-                        } catch (IOException e) {
-                            e.printStackTrace();
-                        }
-                    }
-                    //处理完,移除selectionKey
-                    selectionKeys.remove(selectionKey);
-                }
-            }
-        }
-        //关闭资源
-        try {
-            close();
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-    }
-}

+ 172 - 0
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/core/Client.java

@@ -0,0 +1,172 @@
+package space.anyi.chatClient.core;
+
+import lombok.Data;
+import space.anyi.chatCommom.Encoder;
+import space.anyi.chatCommom.Message;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.*;
+import java.nio.ByteBuffer;
+import java.nio.channels.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: ClentServer
+ * @Author: 杨逸
+ * @Data:2025/9/26 18:59
+ * @Description:
+ */
+@Data
+public class Client implements Runnable, Closeable {
+    public static  HashMap<String, List<Message>> messageListMap = new HashMap<>();
+    private Selector readSelector;
+    private SocketChannel socketChannel;
+    private ReadHandler readHandler;
+    private WriterHandler writerHandler;
+    private InetSocketAddress inetSocketAddress;
+    private boolean isExit = false;
+    public static String name = "";
+    public static String hostNameUPD = "";
+    public static int portUPD = 8000;
+
+    public Client(InetSocketAddress inetSocketAddress) {
+        try {
+            this.readSelector = Selector.open();
+            this.inetSocketAddress = inetSocketAddress;
+            this.writerHandler = new WriterHandler();
+            this.readHandler = new ReadHandler(this.readSelector,this.writerHandler,this);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        logoutHandler();
+        readSelector.close();
+        socketChannel.close();
+    }
+
+    public void logoutHandler() {
+        Message message = Message.createSystemMessage("logout", Message.OperateType.LOGOUT, name, Message.MessageTarget.SERVER);
+        writerHandler.sendMessage(message);
+    }
+
+    @Override
+    public void run() {
+        //创建连接
+        try {
+            this.socketChannel = SocketChannel.open(inetSocketAddress);
+            socketChannel.configureBlocking(false);
+            socketChannel.register(readSelector, SelectionKey.OP_READ);
+            writerHandler.setChannel(socketChannel);
+            //tcp的处理
+            new Thread(readHandler).start();
+            Selector selector = Selector.open();
+            DatagramChannel datagramChannel = null;
+
+            //UDP使用的端口
+            int port = 8000;
+            boolean flag = false;
+            while(true){
+                if (flag)break;
+                try{
+                    datagramChannel = DatagramChannel.open().bind(new InetSocketAddress(port));
+                    datagramChannel.configureBlocking(false);
+                    datagramChannel.register(selector,SelectionKey.OP_READ);
+                    flag = true;
+                }catch (Exception e){
+                    //没有发生异常说明,端口没有被占用
+                    e.printStackTrace();
+                    port++;
+                    flag = false;
+                }
+            }
+            InetSocketAddress localAddress = (InetSocketAddress) datagramChannel.getLocalAddress();
+            hostNameUPD = localAddress.getAddress().getHostAddress();
+            portUPD = port;
+
+            //udp传输接受用户列表
+            while(!isExit){
+                if (selector.select(100) == 0) {
+                    continue;
+                }
+                Set<SelectionKey> selectionKeys = selector.selectedKeys();
+                for (SelectionKey selectionKey : selectionKeys) {
+                    DatagramChannel channel = (DatagramChannel) selectionKey.channel();
+                    if (selectionKey.isReadable()) {
+                        ByteBuffer buffer = ByteBuffer.allocate(4096);
+                        channel.receive(buffer);
+                        buffer.flip();
+                        Message message = decodeUPD(buffer);
+                        String[] userArray = message.getContent().split(",");
+                        readHandler.updateUserList(userArray);
+                    }
+                    selectionKeys.remove(selectionKey);
+                }
+            }
+        } catch (ClosedChannelException e) {
+            System.err.println(1);
+            e.printStackTrace();
+        } catch (IOException e) {
+            System.err.println(2);
+            e.printStackTrace();
+        }finally {
+            System.err.println(3);
+            exit();
+            try {
+                close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private Message decodeUPD(ByteBuffer buffer) {
+        int limit = buffer.limit();
+        byte[] bytes = buffer.array();
+        ByteBuffer allocate = ByteBuffer.allocate(limit - 4);
+        for (int i = 4; i < limit; i++) {
+            allocate.put(bytes[i]);
+        }
+        return Encoder.decode(allocate.array());
+    }
+
+    public void exit(){
+        isExit = true;
+        try {
+            close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void sendMessageWithGroup(Message message){
+        message.setSource(name);
+        writerHandler.sendMessageWithGroup(message);
+    }
+
+    public void sendMessageWithUser(Message message,String username){
+        message.setTarget(username);
+        writerHandler.sendMessage(message);
+    }
+
+    public void sendLoginMessage(String username) {
+        this.name = username;
+        Message message = Message.createSystemMessage("login", Message.OperateType.LOGIN, username, Message.MessageTarget.SERVER);
+        writerHandler.sendMessage(message);
+    }
+
+    public void sendUDPMessage() {
+        Message message = Message.createSystemMessage(hostNameUPD + ":" + portUPD, Message.OperateType.UDP, name, Message.MessageTarget.SERVER);
+        writerHandler.sendMessage(message);
+    }
+
+    public void revokeMessage(Message message) {
+        writerHandler.sendMessage(message);
+    }
+}

+ 216 - 0
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/core/ReadHandler.java

@@ -0,0 +1,216 @@
+package space.anyi.chatClient.core;
+
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.layout.Pane;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import lombok.Data;
+import space.anyi.chatClient.ui.ClientApplication;
+import space.anyi.chatClient.ui.controller.ClientController;
+import space.anyi.chatClient.ui.controller.LoginController;
+import space.anyi.chatClient.ui.controller.PrivateChatController;
+import space.anyi.chatCommom.Encoder;
+import space.anyi.chatCommom.Message;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectableChannel;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.*;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: ReadHandler
+ * @Author: 杨逸
+ * @Data:2025/9/22 19:20
+ * @Description:
+ */
+@Data
+public class ReadHandler implements Runnable, Closeable {
+    private Client clientServer;
+    private Selector readSelector;
+    private WriterHandler writerHandler;
+    private ClientController clientController;
+    private boolean isExit = false;
+
+    public ReadHandler(Selector readSelector, WriterHandler writerHandler, Client clientServer) {
+        this.readSelector = readSelector;
+        this.writerHandler = writerHandler;
+        this.clientServer = clientServer;
+    }
+
+    @Override
+    public void close() throws IOException {
+        exit();
+        System.out.println("close readHandler");
+        if (Objects.nonNull(readSelector) && readSelector.isOpen()) {
+            readSelector.close();
+        }
+    }
+
+    public void exit() {
+        isExit = true;
+    }
+
+    @Override
+    public void run() {
+        System.out.println("readHandler starter");
+        while(true){
+            if (isExit)break;
+            int select = 0;
+            try {
+                select = readSelector.select(100);
+                if (select > 0) {
+                    Set<SelectionKey> selectionKeys = readSelector.selectedKeys();
+                    for (SelectionKey selectionKey : selectionKeys) {
+                        if (selectionKey.isValid() && selectionKey.isReadable()) {
+                            //拿到channel
+                            SelectableChannel channel = selectionKey.channel();
+                            SocketChannel socketChannel = (SocketChannel) channel;
+                            //读取数据
+                            ByteBuffer byteBuffer = ByteBuffer.allocate(4);
+                            int len = 0;
+                            try {
+                                len = socketChannel.read(byteBuffer);
+                                if (len > 0) {
+                                    byteBuffer.flip();
+                                    int size = byteBuffer.getInt();
+                                    byteBuffer = ByteBuffer.allocate(size);
+                                    socketChannel.read(byteBuffer);
+                                    Message message = Encoder.decode(byteBuffer);
+                                    System.out.println("message = " + message);
+                                    if (Message.MessageType.SYSTEM == message.getType()){
+                                        //系统信息
+                                        int operate = message.getOperate();
+                                        switch(operate){
+                                            case Message.OperateType.LOGIN :
+                                                //登陆操作
+                                                System.out.println("登陆");
+                                                loginHandler(message);
+                                                break;
+                                            case Message.OperateType.LOGOUT :
+                                                //登出操作
+                                                System.out.println("登出");
+                                                clientServer.logoutHandler();
+                                                Platform.exit();
+                                                break;
+                                            case Message.OperateType.BROADCAST:
+                                                //广播消息
+                                                System.out.println("广播");
+                                                receiveBroadcastHandler(message);
+                                                break;
+                                            case Message.OperateType.REVOKE:
+                                                //撤回消息
+                                                System.out.println("撤回");
+                                                clientController.removeMessage(message);
+                                                break;
+                                            default:
+                                                System.err.println("未定义的操作"+message);
+                                                break;
+                                        }
+                                    }else{
+                                        //用户信息
+                                        String target = message.getTarget();
+                                        if (Message.MessageTarget.GROUP.equals(target)) {
+                                            //群聊
+                                            System.out.println("群聊");
+                                            clientController.addMessage(message);
+                                        }else{
+                                            //私聊
+                                            System.out.println("私聊");
+                                            List<Message> list = Client.messageListMap.getOrDefault(message.getSource(), new ArrayList<>());
+                                            list.add(message);
+                                            Client.messageListMap.put(message.getSource(), list);
+                                        }
+                                    }
+                                }
+                            } catch (SocketException e) {
+                                //连接意外中断导致异常
+                                //关闭客户端
+                                exit();
+                                e.printStackTrace();
+                                selectionKey.cancel();
+                                try {
+                                    //关闭相关的channel
+                                    socketChannel.close();
+                                } catch (IOException ex) {
+                                    ex.printStackTrace();
+                                }
+                            } catch (IOException e) {
+                                e.printStackTrace();
+                            }
+                        }
+                        //处理完,移除selectionKey
+                        selectionKeys.remove(selectionKey);
+                    }
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        //关闭资源
+        try {
+            close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void receiveBroadcastHandler(Message message) {
+        clientController.addMessage(message);
+    }
+
+    /**
+     * 弹出登陆窗口
+     * @description:
+     * @author: 杨逸
+     * @data:2025/09/28 16:32:12
+     * @since 1.0.0
+     */
+    public void loginHandler(Message message) {
+        Platform.runLater(()->{
+            try{
+                FXMLLoader fxmlLoader = new FXMLLoader();
+                fxmlLoader.setLocation(getClass().getResource("/fxml/loginScene.fxml"));
+                Pane loginNode = fxmlLoader.load();
+
+                Scene loginScene = new Scene(loginNode);
+                //注意这里的stage不要和主stage搞混了
+                Stage subStage = new Stage();
+                subStage.setResizable(false);
+                subStage.setScene(loginScene);
+                subStage.setTitle("登陆");
+                subStage.initOwner(ClientApplication.primaryStage);
+                subStage.initModality(Modality.APPLICATION_MODAL);
+
+                LoginController controller = fxmlLoader.getController();
+                controller.setClientServer(clientServer);
+                subStage.show();
+                //如果信息不为null,就是服务端要求重新登陆
+                if (Objects.nonNull(message)) {
+                    Alert alert = new Alert(Alert.AlertType.WARNING);
+                    alert.setTitle("警告");
+                    alert.setHeaderText(message.getContent());
+                    alert.setContentText("请重新登陆");
+                    alert.show();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        });
+    }
+
+    public void updateUserList(String[] userArray) {
+        clientController.updateUserList(userArray);
+    }
+}

+ 40 - 0
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/core/WriterHandler.java

@@ -0,0 +1,40 @@
+package space.anyi.chatClient.core;
+
+import lombok.Data;
+import space.anyi.chatCommom.Encoder;
+import space.anyi.chatCommom.Message;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: WriterHandler
+ * @Author: 杨逸
+ * @Data:2025/9/26 19:12
+ * @Description:
+ */
+@Data
+public class WriterHandler {
+    private SocketChannel channel;
+    public void sendMessageWithGroup(Message message) {
+       sendMessage(message);
+    }
+
+    /**
+     * @param message
+     * @description: 发送信息
+     * @author: 杨逸
+     * @data:2025/09/28 14:15:42
+     * @since 1.0.0
+     */
+    public void sendMessage(Message message){
+        try {
+            channel.write(ByteBuffer.wrap(Encoder.encode(message)));
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 60 - 0
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/ClientApplication.java

@@ -0,0 +1,60 @@
+package space.anyi.chatClient.ui;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.ButtonType;
+import javafx.scene.layout.Pane;
+import javafx.stage.Stage;
+import lombok.Data;
+import space.anyi.chatClient.ui.controller.StartController;
+
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: ClientApplication
+ * @Author: 杨逸
+ * @Data:2025/9/26 19:54
+ * @Description: 客户端启动类
+ */
+@Data
+public class ClientApplication extends Application{
+    public static Stage primaryStage;
+    @Override
+    public void start(Stage primaryStage) throws Exception {
+        ClientApplication.primaryStage = primaryStage;
+        //不能修改窗口大小
+        primaryStage.setResizable(false);
+        //加载fxml文件
+        FXMLLoader fxmlLoader = new FXMLLoader();
+        fxmlLoader.setLocation(getClass().getResource("/fxml/index.fxml"));
+        Pane pane = fxmlLoader.<Pane>load();
+        StartController startController = fxmlLoader.<StartController>getController();
+        //设置舞台
+        startController.setStage(primaryStage);
+        //创建场景
+        Scene startScene = new Scene(pane);
+        primaryStage.setScene(startScene);
+        primaryStage.setTitle("聊天室 Client");
+        primaryStage.setOnCloseRequest(event -> {
+            //消费事件,防止默认关闭
+            event.consume();
+            windowCloseHandler();
+        });
+        primaryStage.show();
+    }
+
+    private void windowCloseHandler() {
+        Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
+        alert.setTitle("警告");
+        alert.setHeaderText("确定要关闭吗?");
+        //用户操作的结果
+        ButtonType buttonType = alert.showAndWait().get();
+        if (ButtonType.OK.equals(buttonType)) {
+            //退出程序
+            Platform.exit();
+        }
+    }
+}

+ 259 - 0
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/controller/ClientController.java

@@ -0,0 +1,259 @@
+package space.anyi.chatClient.ui.controller;
+
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.fxml.Initializable;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import lombok.Data;
+import space.anyi.chatClient.core.Client;
+import space.anyi.chatClient.ui.ClientApplication;
+import space.anyi.chatCommom.Message;
+
+import java.io.IOException;
+import java.net.URL;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.ResourceBundle;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: ClientController
+ * @Author: 杨逸
+ * @Data:2025/9/26 20:09
+ * @Description:
+ */
+@Data
+public class ClientController implements Initializable {
+    private Stage stage;
+    private Client clientServer;
+
+    @Override
+    public void initialize(URL url, ResourceBundle resourceBundle) {
+
+    }
+    @FXML
+    private Button bu;
+
+    @FXML
+    private TextArea inputMessage;
+
+    @FXML
+    private ScrollPane messagePane;
+
+    @FXML
+    private Label onlineLabel;
+
+    @FXML
+    private VBox userList;
+
+    @FXML
+    void sendMessage(ActionEvent event) {
+        bu.setDisable(true);
+        String content = inputMessage.getText();
+        inputMessage.clear();
+        if (Objects.nonNull(content) && !"".equals(content)) {
+            Message message = new Message(content);
+            message.setType(Message.MessageType.USER);
+            message.setTarget(Message.MessageTarget.GROUP);
+            //message.setSource(name);
+            clientServer.sendMessageWithGroup(message);
+            addMessage(message);
+        }
+        bu.setDisable(false);
+    }
+
+    public void setStage(Stage stage) {
+        this.stage = stage;
+    }
+
+    public void setClientServer(Client clientServer) {
+        this.clientServer = clientServer;
+    }
+
+    public void addMessage(Message message) {
+        VBox messageUI = createMessageUI(message);
+        VBox content = (VBox) messagePane.getContent();
+        Platform.runLater(()->{
+            content.getChildren().add(messageUI);
+        });
+
+        //右键菜单
+        if (message.getType() == Message.MessageType.USER) {
+            messageUI.setOnContextMenuRequested(event->{
+                ContextMenu contextMenu = new ContextMenu();
+                //自己的可以触发撤回
+                //别人的可以私聊
+                MenuItem item = null;
+                if (message.getSource().equals(Client.name)) {
+                    item = new MenuItem("撤回");
+                    item.setOnAction(e->{
+                        Alert alert = new Alert(Alert.AlertType.WARNING);
+                        alert.setTitle("警告");
+                        alert.setHeaderText("撤回消息");
+                        alert.setContentText("确定撤回?");
+                        alert.showAndWait().ifPresent(buttonType -> {
+                            removeMessageUI(messageUI);
+                            message.setType(Message.MessageType.SYSTEM);
+                            message.setOperate(Message.OperateType.REVOKE);
+                            clientServer.revokeMessage(message);
+                        });
+
+                    });
+                }else{
+                    item = new MenuItem("私聊");
+                    item.setOnAction(actionEvent->{
+                        //创建私聊场景
+                        FXMLLoader fxmlLoader = new FXMLLoader();
+                        fxmlLoader.setLocation(getClass().getResource("/fxml/privateChatScene.fxml"));
+                        try {
+                            VBox privateChatPane = fxmlLoader.load();
+                            PrivateChatController controller = fxmlLoader.getController();
+                            controller.setWriterHandler(clientServer.getWriterHandler());
+                            List<Message> list = Client.messageListMap.getOrDefault(message.getSource(), new ArrayList<>());
+                            Client.messageListMap.put(message.getSource(),list);
+                            controller.setMessageList(list);
+                            controller.setName(message.getSource());
+                            controller.runTask();
+                            Scene scene = new Scene(privateChatPane);
+                            Stage stage = new Stage();
+                            stage.setTitle("私聊");
+                            stage.setScene(scene);
+                            stage.setResizable(false);
+                            stage.initOwner(ClientApplication.primaryStage);
+                            stage.initModality(Modality.WINDOW_MODAL);
+                            stage.show();
+                            stage.setOnCloseRequest(windowEvent -> {
+                                //取消私聊消息列表的更新任务
+                                controller.setFlag(false);
+                            });
+                        } catch (IOException e) {
+                            e.printStackTrace();
+                        }
+                    });
+                }
+                contextMenu.getItems().add(item);
+                contextMenu.show(messageUI,event.getScreenX(),event.getScreenY());
+            });
+        }
+    }
+
+    private VBox createMessageUI(Message message) {
+        VBox result = new VBox();
+        HBox head = new HBox();
+        Label name = new Label(message.getSource());
+        Label content = new Label(message.getContent());
+        String format = Instant.ofEpochMilli(message.getTimeStamp()).atZone(ZoneId.systemDefault()).toLocalDateTime().format(DateTimeFormatter.ISO_DATE_TIME);
+        Label time = new Label(format);
+        //样式
+        double width = 450;
+        double height = 50;
+        result.setPrefWidth(width);
+        result.setPrefHeight(height);
+        //用背景颜色区分自己的消息
+        if (Client.name.equals(message.getSource())){
+            //自己的
+            result.setStyle("-fx-background-radius: 100;-fx-background-color:  #3EB575");
+        }else{
+            //别人的
+            result.setStyle("-fx-background-radius: 100;-fx-background-color:  #2E2E2E");
+        }
+        head.setPrefWidth(width);
+        head.setPrefHeight(height-15);
+
+        name.setPrefWidth(100);
+        name.setPrefHeight(height - 15);
+        name.setTextFill(Color.WHITE);
+        name.setAlignment(Pos.CENTER);
+
+        content.setPrefWidth(width - 100);
+        content.setPrefHeight(height - 15);
+        content.setTextFill(Color.WHITE);
+
+        time.setPrefWidth(width);
+        time.setPrefHeight(15);
+        time.setTextFill(Color.GRAY);
+        time.setAlignment(Pos.CENTER);
+
+
+        //组装
+        head.getChildren().addAll(name, content);
+        result.getChildren().addAll(head,time);
+
+        return result;
+    }
+
+    private void removeMessageUI(VBox messageUI) {
+        VBox content = (VBox) messagePane.getContent();
+        ObservableList<Node> children = content.getChildren();
+        for (Node child : children) {
+            if (child.equals(messageUI)){
+                children.remove(child);
+                break;
+            }
+        }
+    }
+
+    public void updateUserList(String[] userArray) {
+        int size = userArray.length;
+        Platform.runLater(()->{
+            onlineLabel.setText("在线人数:"+size);
+            userList.getChildren().clear();
+            for (int i = 0; i < userArray.length; i++) {
+                userList.getChildren().add(createUserUI(userArray[i]));
+            }
+        });
+
+    }
+
+    private Label createUserUI(String username) {
+        Label label = new Label(username);
+        label.prefWidth(150);
+        label.setMinWidth(150);
+        label.setMinHeight(30);
+        label.prefHeight(30);
+        label.setAlignment(Pos.CENTER);
+        label.setStyle("-fx-background-color: #363636; -fx-background-radius: 50;");
+        label.setTextFill(Color.WHITE);
+        return label;
+    }
+
+    public void removeMessage(Message message) {
+        VBox content = (VBox) messagePane.getContent();
+        ObservableList<Node> children = content.getChildren();
+        for (Node child : children) {
+            VBox messageUI = (VBox) child;
+            HBox head = (HBox) messageUI.getChildren().get(0);
+            Label nameLabel = (Label) head.getChildren().get(0);
+            Label contentLabel = (Label) head.getChildren().get(1);
+            Label timeLabel = (Label) messageUI.getChildren().get(1);
+            String name1 = nameLabel.getText();
+            String content1 = contentLabel.getText();
+            String time1 = timeLabel.getText();
+            String name2 = message.getSource();
+            String content2 = message.getContent();
+            String time2 = Instant.ofEpochMilli(message.getTimeStamp()).atZone(ZoneId.systemDefault()).toLocalDateTime().format(DateTimeFormatter.ISO_DATE_TIME);
+            if (name1.equals(name2) && content1.equals(content2) && time1.equals(time2)){
+                Platform.runLater(()->{
+                    children.remove(child);
+                });
+                break;
+            }
+        }
+    }
+}

+ 77 - 0
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/controller/LoginController.java

@@ -0,0 +1,77 @@
+package space.anyi.chatClient.ui.controller;
+
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.Pane;
+import javafx.stage.Modality;
+import lombok.Data;
+import space.anyi.chatClient.core.Client;
+import space.anyi.chatClient.ui.ClientApplication;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: LoginController
+ * @Author: 杨逸
+ * @Data:2025/9/28 14:04
+ * @Description: 登录界面控制器
+ */
+@Data
+public class LoginController {
+    private Client clientServer;
+    //private Stage stage;
+
+    @FXML
+    private Button bu;
+
+    @FXML
+    private TextField name;
+
+    @FXML
+    void loginHandler(ActionEvent event) {
+        bu.setDisable(true);
+        String username = name.getText();
+        if (Objects.nonNull(username) && !username.trim().equals("")) {
+            //登陆
+            clientServer.sendLoginMessage(username);
+            //切换场景
+            FXMLLoader fxmlLoader = new FXMLLoader();
+            fxmlLoader.setLocation(getClass().getResource("/fxml/client.fxml"));
+            try {
+                Pane pane = fxmlLoader.<Pane>load();
+                ClientController controller = fxmlLoader.<ClientController>getController();
+                controller.setStage(ClientApplication.primaryStage);
+                clientServer.getReadHandler().setClientController(controller);
+                controller.setClientServer(clientServer);
+                Scene scene = new Scene(pane);
+                //获取当前的窗口,窗口隐藏
+                bu.getScene().getWindow().hide();
+                //切换到聊天窗口
+                ClientApplication.primaryStage.setScene(scene);
+                clientServer.sendUDPMessage();
+                //ClientApplication.primaryStage.show();
+            } catch (IOException e) {
+                e.printStackTrace();
+                Alert alert = new Alert(Alert.AlertType.ERROR);
+                alert.setTitle("出现错误");
+                alert.setHeaderText("切换场景失败");
+                alert.show();
+            }
+        }else{
+            Alert alert = new Alert(Alert.AlertType.WARNING);
+            alert.initOwner(ClientApplication.primaryStage);
+            alert.initModality(Modality.WINDOW_MODAL);
+            alert.setTitle("警告");
+            alert.setHeaderText("用户名不能为空");
+            alert.showAndWait();
+        }
+        bu.setDisable(false);
+    }
+}

+ 155 - 0
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/controller/PrivateChatController.java

@@ -0,0 +1,155 @@
+package space.anyi.chatClient.ui.controller;
+
+import javafx.application.Platform;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import lombok.Data;
+import space.anyi.chatClient.core.Client;
+import space.anyi.chatClient.core.WriterHandler;
+import space.anyi.chatCommom.Message;
+
+import java.net.URL;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Objects;
+import java.util.ResourceBundle;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: PrivateChatController
+ * @Author: 杨逸
+ * @Data:2025/9/29 11:40
+ * @Description: 私聊窗口控制器
+ */
+@Data
+public class PrivateChatController implements Initializable {
+    private WriterHandler writerHandler;
+    private List<Message> messageList;
+    private boolean flag = true;
+
+    @Override
+    public void initialize(URL url, ResourceBundle resourceBundle) {
+
+    }
+    @FXML
+    private Button bu;
+
+    @FXML
+    private TextArea inputMessage;
+
+    @FXML
+    private ScrollPane messagePane;
+
+    @FXML
+    private Label nameLabel;
+
+    @FXML
+    void sendMessage(ActionEvent event) {
+        bu.setDisable(true);
+        String text = inputMessage.getText();
+        String name = nameLabel.getText();
+        inputMessage.clear();
+        if (Objects.nonNull(text) && !"".equals(text.trim())) {
+            Message message = new Message(text);
+            message.setType(Message.MessageType.USER);
+            message.setOperate(Message.OperateType.CHAT);
+            message.setSource(Client.name);
+            message.setTarget(name);
+            writerHandler.sendMessage(message);
+            addMessage(message);
+        }
+        bu.setDisable(false);
+    }
+
+    private void addMessage(Message message) {
+        System.out.println("message = " + message);
+        messageList.add(message);
+        VBox messageUI = createMessageUI(message);
+        VBox content = (VBox) messagePane.getContent();
+        Platform.runLater(()->{
+            //添加messageUI
+            content.getChildren().add(messageUI);
+        });
+    }
+
+    public static VBox createMessageUI(Message message) {
+        VBox result = new VBox();
+        HBox head = new HBox();
+        Label name = new Label(message.getSource());
+        Label content = new Label(message.getContent());
+        String format = Instant.ofEpochMilli(message.getTimeStamp()).atZone(ZoneId.systemDefault()).toLocalDateTime().format(DateTimeFormatter.ISO_DATE_TIME);
+        Label time = new Label(format);
+        //样式
+        double width = 300;
+        double height = 50;
+        result.setPrefWidth(width);
+        result.setPrefHeight(height);
+        //用背景颜色区分自己的消息
+        if (Client.name.equals(message.getSource())){
+            //自己的
+            result.setStyle("-fx-background-radius: 100;-fx-background-color:  #3EB575");
+        }else{
+            //别人的
+            result.setStyle("-fx-background-radius: 100;-fx-background-color:  #2E2E2E");
+        }
+        head.setPrefWidth(width);
+        head.setPrefHeight(height-15);
+
+        name.setPrefWidth(100);
+        name.setPrefHeight(height - 15);
+        name.setTextFill(Color.WHITE);
+        name.setAlignment(Pos.CENTER);
+
+        content.setPrefWidth(width - 100);
+        content.setPrefHeight(height - 15);
+        content.setTextFill(Color.WHITE);
+
+        time.setPrefWidth(width);
+        time.setPrefHeight(15);
+        time.setTextFill(Color.GRAY);
+        time.setAlignment(Pos.CENTER);
+        //组装
+        head.getChildren().addAll(name, content);
+        result.getChildren().addAll(head,time);
+        return result;
+    }
+
+    public void setName(String source) {
+        nameLabel.setText(source);
+    }
+
+    public void runTask() {
+        //更新私聊信息的任务
+        new Thread(()->{
+            while (flag){
+                try {
+                    Thread.sleep(1000);
+                    VBox content = (VBox) messagePane.getContent();
+                    int size1 = content.getChildren().size();
+                    int size2 = messageList.size();
+                    System.out.println("size1 = " + size1);
+                    System.out.println("size2 = " + size2);
+                    for (int i = size1; i < size2; i++) {
+                        VBox messageUI = createMessageUI(messageList.get(i));
+                        Platform.runLater(()->{
+                            content.getChildren().add(messageUI);
+                        });
+                    }
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        }).start();
+    }
+}

+ 75 - 0
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ui/controller/StartController.java

@@ -0,0 +1,75 @@
+package space.anyi.chatClient.ui.controller;
+
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextField;
+import javafx.stage.Stage;
+import lombok.Data;
+import space.anyi.chatClient.core.Client;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.util.ResourceBundle;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: StartController
+ * @Author: 杨逸
+ * @Data:2025/9/26 19:47
+ * @Description:
+ */
+@Data
+public class StartController implements Initializable {
+    /**
+     * JavaFX场景控制器的初始化方法,在构造器后调用
+     * @param url
+     * @param resourceBundle
+     * @description: 初始化
+     * @author: 杨逸
+     * @data:2025/09/26 19:47:30
+     * @since 1.0.0
+     */
+    @Override
+    public void initialize(URL url, ResourceBundle resourceBundle) {
+
+    }
+
+    private Stage stage;
+
+    @FXML
+    private Button bu;
+
+    @FXML
+    private TextField hostField;
+
+    @FXML
+    private TextField portField;
+
+    @FXML
+    void startHandler(ActionEvent event) {
+        bu.setDisable(true);
+        //获取输入的host和port
+        String host = hostField.getText();
+        String port = portField.getText();
+        InetSocketAddress inetSocketAddress = new InetSocketAddress(host, Integer.parseInt(port));
+
+        Client clientServer = new Client(inetSocketAddress);
+        new Thread(clientServer).start();
+        //登陆操作,自发的
+        clientServer.getReadHandler().loginHandler(null);
+
+        //关闭窗口的回调
+        this.stage.setOnHiding(windowEvent->{
+            clientServer.exit();
+            try {
+                clientServer.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        });
+        bu.setDisable(false);
+    }
+}

+ 80 - 0
chat-gwng/chat-client/src/main/resources/fxml/client.fxml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.ScrollPane?>
+<?import javafx.scene.control.TextArea?>
+<?import javafx.scene.layout.ColumnConstraints?>
+<?import javafx.scene.layout.GridPane?>
+<?import javafx.scene.layout.RowConstraints?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.text.Font?>
+
+<GridPane alignment="CENTER" maxHeight="300.0" maxWidth="600.0" minHeight="400.0" minWidth="600.0" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="space.anyi.chatClient.ui.controller.ClientController">
+   <columnConstraints>
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+   </columnConstraints>
+   <rowConstraints>
+      <RowConstraints maxHeight="50.0" minHeight="50.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+   </rowConstraints>
+   <children>
+      <Label alignment="CENTER" maxHeight="80.0" maxWidth="600.0" minHeight="80.0" minWidth="600.0" prefHeight="80.0" prefWidth="600.0" text="聊天室 Client" GridPane.columnIndex="14" GridPane.columnSpan="12" GridPane.rowSpan="2">
+         <font>
+            <Font size="48.0" />
+         </font>
+      </Label>
+      <ScrollPane fx:id="messagePane" prefHeight="200.0" prefWidth="200.0" GridPane.columnIndex="14" GridPane.columnSpan="9" GridPane.rowIndex="2" GridPane.rowSpan="4">
+         <content>
+            <VBox prefHeight="200.0" prefWidth="450.0" />
+         </content>
+      </ScrollPane>
+      <ScrollPane prefHeight="200.0" prefWidth="200.0" GridPane.columnIndex="23" GridPane.columnSpan="3" GridPane.rowIndex="3" GridPane.rowSpan="5">
+         <content>
+            <VBox fx:id="userList" alignment="TOP_CENTER" prefHeight="250.0" prefWidth="150.0" />
+         </content>
+      </ScrollPane>
+      <TextArea fx:id="inputMessage" maxWidth="400.0" minHeight="100.0" minWidth="400.0" prefColumnCount="1" prefHeight="200.0" prefRowCount="1" prefWidth="200.0" promptText="输入信息" GridPane.columnIndex="14" GridPane.columnSpan="8" GridPane.rowIndex="6" GridPane.rowSpan="2">
+         <font>
+            <Font size="14.0" />
+         </font>
+      </TextArea>
+      <Button fx:id="bu" alignment="CENTER" maxHeight="100.0" maxWidth="50.0" minHeight="100.0" minWidth="50.0" mnemonicParsing="false" onAction="#sendMessage" prefHeight="100.0" prefWidth="50.0" text="发送" GridPane.columnIndex="22" GridPane.rowIndex="6" GridPane.rowSpan="2" />
+      <Label fx:id="onlineLabel" alignment="CENTER" maxHeight="50.0" maxWidth="150.0" minHeight="50.0" minWidth="150.0" prefHeight="50.0" prefWidth="150.0" style="-fx-border-color: #fff;" text="在线人数:0" GridPane.columnIndex="23" GridPane.columnSpan="3" GridPane.rowIndex="2">
+         <font>
+            <Font size="14.0" />
+         </font>
+      </Label>
+   </children>
+</GridPane>

+ 96 - 0
chat-gwng/chat-client/src/main/resources/fxml/index.fxml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.TextField?>
+<?import javafx.scene.layout.ColumnConstraints?>
+<?import javafx.scene.layout.GridPane?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.RowConstraints?>
+<?import javafx.scene.text.Font?>
+
+<GridPane alignment="CENTER" maxHeight="300.0" maxWidth="600.0" minHeight="400.0" minWidth="600.0" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="space.anyi.chatClient.ui.controller.StartController">
+   <columnConstraints>
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+      <ColumnConstraints hgrow="SOMETIMES" maxWidth="50.0" minWidth="50.0" prefWidth="50.0" />
+   </columnConstraints>
+   <rowConstraints>
+      <RowConstraints maxHeight="50.0" minHeight="50.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+      <RowConstraints maxHeight="50.0" minHeight="10.0" prefHeight="50.0" vgrow="SOMETIMES" />
+   </rowConstraints>
+   <children>
+      <Label alignment="CENTER" maxHeight="80.0" maxWidth="600.0" minHeight="80.0" minWidth="600.0" prefHeight="80.0" prefWidth="600.0" text="聊天室 Client" GridPane.columnIndex="14" GridPane.columnSpan="12" GridPane.rowSpan="2">
+         <font>
+            <Font size="48.0" />
+         </font>
+      </Label>
+      <Label alignment="BOTTOM_CENTER" prefHeight="50.0" prefWidth="600.0" text="25计科4(专升本)-2520601429-黄略" GridPane.columnIndex="14" GridPane.columnSpan="12" GridPane.rowIndex="7">
+         <font>
+            <Font size="14.0" />
+         </font>
+      </Label>
+      <HBox prefHeight="100.0" prefWidth="200.0" GridPane.columnIndex="18" GridPane.columnSpan="4" GridPane.rowIndex="2">
+         <children>
+            <Label alignment="CENTER" prefHeight="50.0" prefWidth="100.0" text="服务器端口:">
+               <font>
+                  <Font size="16.0" />
+               </font>
+            </Label>
+            <TextField fx:id="portField" prefHeight="50.0" prefWidth="100.0" promptText="端口" text="8000">
+               <font>
+                  <Font size="14.0" />
+               </font>
+            </TextField>
+         </children>
+      </HBox>
+      <HBox prefHeight="100.0" prefWidth="200.0" GridPane.columnIndex="18" GridPane.columnSpan="4" GridPane.rowIndex="3">
+         <children>
+            <Label alignment="CENTER" prefHeight="50.0" prefWidth="100.0" text="服务器HOST:">
+               <font>
+                  <Font size="16.0" />
+               </font>
+            </Label>
+            <TextField fx:id="hostField" prefHeight="50.0" prefWidth="100.0" promptText="IP" text="localhost">
+               <font>
+                  <Font size="14.0" />
+               </font>
+            </TextField>
+         </children>
+      </HBox>
+      <Button fx:id="bu" mnemonicParsing="false" onAction="#startHandler" prefHeight="50.0" prefWidth="100.0" text="连接" GridPane.columnIndex="19" GridPane.columnSpan="2" GridPane.rowIndex="5">
+         <font>
+            <Font size="24.0" />
+         </font>
+      </Button>
+   </children>
+</GridPane>

+ 20 - 0
chat-gwng/chat-client/src/main/resources/fxml/loginScene.fxml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.TextField?>
+<?import javafx.scene.layout.Pane?>
+<?import javafx.scene.text.Font?>
+
+
+<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="space.anyi.chatClient.ui.controller.LoginController">
+   <children>
+      <Label alignment="CENTER" prefHeight="100.0" prefWidth="300.0" text="设置名字">
+         <font>
+            <Font size="24.0" />
+         </font>
+      </Label>
+      <TextField fx:id="name" alignment="CENTER" layoutX="74.0" layoutY="187.0" promptText="名字" />
+      <Button fx:id="bu" layoutX="123.0" layoutY="265.0" mnemonicParsing="false" onAction="#loginHandler" text="确认" />
+   </children>
+</Pane>

+ 29 - 0
chat-gwng/chat-client/src/main/resources/fxml/privateChatScene.fxml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.ScrollPane?>
+<?import javafx.scene.control.TextArea?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.text.Font?>
+
+<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="space.anyi.chatClient.ui.controller.PrivateChatController">
+   <children>
+      <Label fx:id="nameLabel" alignment="CENTER" prefHeight="50.0" prefWidth="300.0" text="name">
+         <font>
+            <Font size="18.0" />
+         </font>
+      </Label>
+      <ScrollPane fx:id="messagePane" prefHeight="250.0" prefWidth="300.0">
+         <content>
+            <VBox prefHeight="250.0" prefWidth="300.0" />
+         </content></ScrollPane>
+      <HBox prefHeight="100.0" prefWidth="300.0">
+         <children>
+            <TextArea fx:id="inputMessage" prefHeight="70.0" prefWidth="250.0" promptText="信息" />
+            <Button fx:id="bu" alignment="CENTER" mnemonicParsing="false" onAction="#sendMessage" prefHeight="100.0" prefWidth="50.0" text="发送" />
+         </children>
+      </HBox>
+   </children>
+</VBox>

+ 6 - 0
chat-gwng/chat-commom/src/main/java/space/anyi/chatCommom/Message.java

@@ -80,12 +80,18 @@ public class Message implements Serializable {
         public static final int USER = 1;
     }
     public static class OperateType{
+        //聊天
+        public static final int CHAT = 0;
         //服务操作:有客户登陆;客户端操作:需要重新登陆
         public static final int LOGIN = 1;
         //服务操作:有客户下线;客户端操作:需要重新登陆
         public static final int LOGOUT = 2;
         //广播操作
         public static final int BROADCAST = 3;
+        //服务端:表示接收到客户端传输的UPD地址信息
+        public static final int UDP = 4;
+        //撤回消息操作
+        public static final int REVOKE = 5;
     }
 
     /**

+ 22 - 3
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/core/ChatServer.java

@@ -11,9 +11,7 @@ import java.nio.channels.SelectionKey;
 import java.nio.channels.Selector;
 import java.nio.channels.ServerSocketChannel;
 import java.nio.channels.SocketChannel;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
 
 /**
  * @ProjectName: chat-gwng
@@ -56,7 +54,10 @@ public class ChatServer implements Runnable, Closeable {
     @Override
     public void run() {
         System.out.println("server listen...");
+        //UDP
+        updHandler();
         try {
+            //TCP
             listen();
         } catch (IOException e) {
             e.printStackTrace();
@@ -69,6 +70,24 @@ public class ChatServer implements Runnable, Closeable {
         }
     }
 
+    private void updHandler() {
+        new Thread(()->{
+            while(true){
+                if (isExit)break;
+                //1秒钟推送一次
+                try {
+                    Thread.sleep(1000);
+                    String s = serverController.getUserNameString();
+                    if (s != null && !s.equals("")){
+                        writeHandler.pushUserList(s,readHandler.getUdpAddressSet());
+                    }
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        }).start();
+    }
+
     /**
      * @throws IOException
      * @description: 服务端监听客户端连接

+ 32 - 24
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/core/ReadHandler.java

@@ -13,10 +13,7 @@ import java.nio.channels.SelectableChannel;
 import java.nio.channels.SelectionKey;
 import java.nio.channels.Selector;
 import java.nio.channels.SocketChannel;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
 
 /**
  * @ProjectName: chat-gwng
@@ -32,6 +29,8 @@ public class ReadHandler implements Runnable, Closeable {
     private Map<String,SocketChannel> userChannels = new HashMap<>();
     private boolean isExit = false;
     private ServerController serverController;
+    //ip:port的格式
+    private HashSet<String> udpAddressSet = new HashSet<>();
 
     public ReadHandler(Selector readSelector,ServerController serverController) {
         this.readSelector = readSelector;
@@ -66,7 +65,7 @@ public class ReadHandler implements Runnable, Closeable {
             if (select > 0) {
                 Set<SelectionKey> selectionKeys = readSelector.selectedKeys();
                 for (SelectionKey selectionKey : selectionKeys) {
-                    if (selectionKey.isReadable() && selectionKey.isValid()) {
+                    if (selectionKey.isValid() && selectionKey.isReadable()) {
                         //拿到channel
                         SelectableChannel channel = selectionKey.channel();
                         SocketChannel socketChannel = (SocketChannel) channel;
@@ -81,6 +80,7 @@ public class ReadHandler implements Runnable, Closeable {
                                 byteBuffer = ByteBuffer.allocate(size);
                                 len = socketChannel.read(byteBuffer);
                                 Message message = Encoder.decode(byteBuffer);
+                                System.out.println("message = " + message);
                                 //信息处理
                                 if (message.getType() == Message.MessageType.USER) {
                                     //用户信息
@@ -88,18 +88,20 @@ public class ReadHandler implements Runnable, Closeable {
                                     switch (messageTarget){
                                         case Message.MessageTarget.SERVER:
                                             //发送给系统的信息
-                                            //todo
+                                            System.out.println("系统信息");
                                             break;
                                         case Message.MessageTarget.GROUP:
                                             //群聊
                                             //转发信息
+                                            System.out.println("群聊信息");
                                             chatWithGroupHandler(message,socketChannel);
                                             break;
                                         case Message.MessageTarget.CLIENT:
-                                            //todo
+                                            System.out.println("客户端信息");
                                             break;
                                         default:
                                             //私聊信息
+                                            System.out.println("私聊信息");
                                             chatWithPrivateHandler(message);
                                             break;
                                     }
@@ -108,12 +110,22 @@ public class ReadHandler implements Runnable, Closeable {
                                     switch (message.getOperate()){
                                         case Message.OperateType.LOGIN:
                                             //登录
+                                            System.out.println("登陆");
                                             loginHandler(message,selectionKey,socketChannel);
                                             break;
                                         case Message.OperateType.LOGOUT:
                                             //登出
+                                            System.out.println("登出");
                                             userLogoutHandler(message,selectionKey, socketChannel);
                                             break;
+                                        case Message.OperateType.UDP:
+                                            //上报的UDP地址
+                                            UDPAddressHandler(message);
+                                            break;
+                                        case Message.OperateType.REVOKE:
+                                            //撤回信息
+                                            writeHandler.sendMessageWithGroup(message,socketChannel);
+                                            break;
                                         default:
                                             System.err.println("未知操作");
                                             System.err.println(message);
@@ -124,9 +136,8 @@ public class ReadHandler implements Runnable, Closeable {
                             //连接意外中断导致异常
                             Object attachment = selectionKey.attachment();
                             String username = (String) attachment;
-                            //todo
-                            System.out.println(username+"掉线了");
-                            Message message = new Message(username+"掉线了");
+                            Message message = Message.createSystemBroadcastMessage(username+"掉线了");
+                            chatWithGroupHandler(message,socketChannel);
                             //更新UI
                             serverController.addMessage(message);
                             serverController.removeUser(username);
@@ -156,21 +167,21 @@ public class ReadHandler implements Runnable, Closeable {
         }
     }
 
+    private void UDPAddressHandler(Message message) {
+        String content = message.getContent();
+        if (!udpAddressSet.contains(content))
+            udpAddressSet.add(content);
+    }
+
     private void loginHandler(Message message,SelectionKey selectionKey,SocketChannel socketChannel) {
         String username = message.getSource();
-        if (userChannels.containsKey(username) && Message.MessageTarget.SERVER.equals(username)) {
+        System.out.println(message);
+        if (userChannels.containsKey(username) || Message.MessageTarget.SERVER.equals(username)) {
             //用户名重复
             message = Message.createSystemMessage("用户名重复:'%s'".formatted(username), Message.OperateType.LOGIN, Message.MessageTarget.SERVER, Message.MessageTarget.CLIENT);
             writeHandler.sendMessageWithPrivate(message, socketChannel);
+            //重新登陆
             serverController.addMessage(message);
-            //释放资源
-            selectionKey.cancel();
-            try {
-                if (Objects.nonNull(socketChannel) && socketChannel.isOpen())
-                socketChannel.close();
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
         }else{
             //用户名保存到对应的selectionKey上,之后需要可以从里面取出
             selectionKey.attach(username);
@@ -185,7 +196,7 @@ public class ReadHandler implements Runnable, Closeable {
     }
 
     private void chatWithPrivateHandler(Message message) {
-        String username = message.getSource();
+        String username = message.getTarget();
         SocketChannel socketChannel = userChannels.getOrDefault(username, null);
         if (Objects.nonNull(socketChannel)) {
             writeHandler.sendMessageWithPrivate(message, socketChannel);
@@ -202,10 +213,7 @@ public class ReadHandler implements Runnable, Closeable {
         if(userChannels.containsKey(username))
             userChannels.remove(username);
         //广播用户下线
-        message = new Message();
-        message.setSource(Message.MessageTarget.SERVER);
-        message.setTarget(Message.MessageTarget.GROUP);
-        message.setContent(username+"下线了");
+        message = Message.createSystemBroadcastMessage(username+"下线了");
         writeHandler.sendMessageWithGroup(message, socketChannel);
         //更新UI
         serverController.addMessage(message);

+ 24 - 0
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/core/WriteHandler.java

@@ -5,7 +5,9 @@ import space.anyi.chatCommom.Encoder;
 import space.anyi.chatCommom.Message;
 
 import java.io.IOException;
+import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
+import java.nio.channels.DatagramChannel;
 import java.nio.channels.SelectionKey;
 import java.nio.channels.Selector;
 import java.nio.channels.SocketChannel;
@@ -93,4 +95,26 @@ public class WriteHandler {
             e.printStackTrace();
         }
     }
+
+    public void pushUserList(String content,Set<String> updAddressSet){
+        Message message = Message.createSystemMessage(content, Message.OperateType.UDP, Message.MessageTarget.SERVER, Message.MessageTarget.CLIENT);
+        try {
+            DatagramChannel datagramChannel = DatagramChannel.open();
+            datagramChannel.configureBlocking(false);
+            for (String updAddress : updAddressSet) {
+                int index = updAddress.lastIndexOf(':');
+                if (index < 0)continue;
+                String host = updAddress.substring(0,index);
+                int port = Integer.parseInt(updAddress.substring(index + 1));
+                //连接
+                InetSocketAddress remote = new InetSocketAddress("127.0.0.1", port);
+                //发送
+                datagramChannel.send(ByteBuffer.wrap(Encoder.encode(message)),remote);
+                //datagramChannel.connect(remote);
+                //datagramChannel.write();
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
 }

+ 79 - 51
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/ui/controller/ServerController.java

@@ -4,10 +4,12 @@ import javafx.application.Platform;
 import javafx.collections.ObservableList;
 import javafx.fxml.FXML;
 import javafx.geometry.Pos;
+import javafx.scene.Node;
 import javafx.scene.control.*;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.HBox;
 import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import javafx.stage.Stage;
 import javafx.util.Callback;
@@ -19,6 +21,7 @@ import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.util.List;
 import java.util.Objects;
 
 
@@ -46,7 +49,7 @@ public class ServerController {
     private Label onlineLabel;
 
     @FXML
-    private ListView<String> userList;
+    private VBox userList;
     @FXML
     void sendMessage(MouseEvent event) {
         //禁用按钮
@@ -56,7 +59,9 @@ public class ServerController {
         inputMessage.clear();
         text.trim();
         if (!"".equals(text)) {
-            chatServer.broadcast(createMessage(text));
+            Message message = createMessage(text);
+            chatServer.broadcast(message);
+            addMessage(message);
         }
         bu.setDisable(false);
     }
@@ -70,11 +75,12 @@ public class ServerController {
      * @since 1.0.0
      */
     private Message createMessage(String text) {
-        Message message = new Message();
-        message.setContent(text);
-        message.setType(Message.MessageType.SYSTEM);
-        message.setTarget(Message.MessageTarget.CLIENT);
-        return message;
+        return Message.createSystemMessage(text,Message.OperateType.BROADCAST,Message.MessageTarget.SERVER,Message.MessageTarget.CLIENT);
+        //Message message = new Message();
+        //message.setContent(text);
+        //message.setType(Message.MessageType.SYSTEM);
+        //message.setTarget(Message.MessageTarget.CLIENT);
+        //return message;
     }
 
     /**
@@ -161,49 +167,53 @@ public class ServerController {
      * @since 1.0.0
      */
     public void addUser(String username){
-        ObservableList<String> items = userList.getItems();
         Platform.runLater(()->{
             //UI更新的操作需要在JavaFX线程中
-            items.add(username);
-            onlineLabel.setText("在线人数:"+items.size());
+            userList.getChildren().add(createUserUI(username));
+            onlineLabel.setText("在线人数:"+userList.getChildren().size());
         });
-        userList.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
-            @Override
-            public ListCell<String> call(ListView<String> stringListView) {
-                //创建一个ListView中单元,使用匿名类实现自定义
-                return new ListCell<>(){
-                    @Override
-                    protected void updateItem(String name, boolean b) {
-                        super.updateItem(name, b);
-                        if (Objects.nonNull(name)) {
-                            setText(name);
-                        }
-                        //给单元格添加右键菜单
-                        setOnContextMenuRequested(event->{
-                            ContextMenu contextMenu = new ContextMenu();
-                            MenuItem item = new MenuItem("下线");
-                            contextMenu.getItems().add(item);
-                            contextMenu.show(this,event.getScreenX(),event.getScreenY());
-                            item.setOnAction(event1->{
-                                //下线提示
-                                Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
-                                alert.setTitle("提示");
-                                alert.setHeaderText("确定要下线该用户吗?");
-                                alert.setContentText("该操作不可逆");
-                                alert.showAndWait().ifPresent(buttonType -> {
-                                    if (ButtonType.OK.equals(buttonType)) {
-                                        //下线用户
-                                        chatServer.logoutUser(name);
-                                        //删除用户信息
-                                        removeUser(name);
-                                    }
-                                });
-                            });
-                        });
+    }
+
+    /**
+     * @param username
+     * @return {@code Label }
+     * @description: 创建用户列表的用户UI
+     * @author: 杨逸
+     * @data:2025/09/28 19:02:33
+     * @since 1.0.0
+     */
+    private Label createUserUI(String username) {
+        Label label = new Label(username);
+        label.prefWidth(150);
+        label.setMinWidth(150);
+        label.setMinHeight(30);
+        label.prefHeight(30);
+        label.setAlignment(Pos.CENTER);
+        label.setStyle("-fx-background-color: #363636; -fx-background-radius: 50;");
+        label.setTextFill(Color.WHITE);
+        //给单元格添加右键菜单
+        label.setOnContextMenuRequested(event->{
+            ContextMenu contextMenu = new ContextMenu();
+            MenuItem item = new MenuItem("下线");
+            contextMenu.getItems().add(item);
+            contextMenu.show(label,event.getScreenX(),event.getScreenY());
+            item.setOnAction(event1->{
+                //下线提示
+                Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
+                alert.setTitle("提示");
+                alert.setHeaderText("确定要下线用户:%s吗?".formatted(username));
+                alert.setContentText("该操作不可逆");
+                alert.showAndWait().ifPresent(buttonType -> {
+                    if (ButtonType.OK.equals(buttonType)) {
+                        //下线用户
+                        chatServer.logoutUser(username);
+                        //删除用户信息
+                        removeUser(username);
                     }
-                };
-            }
+                });
+            });
         });
+        return label;
     }
 
     /**
@@ -215,12 +225,30 @@ public class ServerController {
      * @since 1.0.0
      */
     public void removeUser(String username){
-        ObservableList<String> items = userList.getItems();
-        if (items.contains(username)){
-            Platform.runLater((()->{
-                items.remove(username);
-                onlineLabel.setText("在线人数:"+items.size());
-            }));
+        ObservableList<Node> children = userList.getChildren();
+        for (Node child : children) {
+            Label label = (Label) child;
+            if (label.getText().equals(username)){
+                Platform.runLater(()->{
+                    userList.getChildren().remove(label);
+                    onlineLabel.setText("在线人数:"+userList.getChildren().size());
+                });
+                break;
+            }
         }
     }
+
+    public String getUserNameString() {
+        String result = "";
+        ObservableList<Node> children = userList.getChildren();
+        for (Node child : children) {
+            Label label = (Label) child;
+            String name = label.getText();
+            result += "," + name;
+        }
+        if (result.length() != 0){
+            result = result.substring(1);
+        }
+        return result;
+    }
 }

+ 7 - 15
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/ui/controller/StartController.java

@@ -60,28 +60,20 @@ public class StartController {
             //启动服务器
             new Thread(chatServer).start();
             serverController.setChatServer(chatServer);
+            stage.setScene(mainScene);
+            chatServer.setServerController(serverController);
+            ChatServer finalChatServer = chatServer;
+            stage.setOnHiding(windowEvent->{
+                System.out.println("窗口关闭");
+                finalChatServer.exit();
+            });
         } catch (IOException e) {
             e.printStackTrace();
-            if (Objects.nonNull(chatServer)) {
-                chatServer.exit();
-                chatServer.close();
-            }
             Alert alert = new Alert(Alert.AlertType.WARNING);
             alert.setTitle("信息");
             alert.setHeaderText("服务端启动失败,请联系开发者");
             alert.show();
         }
-
-        stage.setScene(mainScene);
-        chatServer.setServerController(serverController);
-        ChatServer finalChatServer = chatServer;
-        stage.setOnHiding(windowEvent->{
-            System.out.println("窗口关闭");
-            finalChatServer.exit();
-        });
-        stage.setOnHidden(windowEvent -> {
-            System.out.println("窗口隐藏后");
-        });
     }
 
 }

+ 2 - 3
chat-gwng/chat-server/src/main/resources/fxml/server.fxml

@@ -2,7 +2,6 @@
 
 <?import javafx.scene.control.Button?>
 <?import javafx.scene.control.Label?>
-<?import javafx.scene.control.ListView?>
 <?import javafx.scene.control.ScrollPane?>
 <?import javafx.scene.control.TextArea?>
 <?import javafx.scene.layout.ColumnConstraints?>
@@ -11,7 +10,7 @@
 <?import javafx.scene.layout.VBox?>
 <?import javafx.scene.text.Font?>
 
-<GridPane alignment="CENTER" maxHeight="300.0" maxWidth="600.0" minHeight="400.0" minWidth="600.0" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/24.0.1" fx:controller="space.anyi.chatServer.ui.controller.ServerController">
+<GridPane alignment="CENTER" maxHeight="300.0" maxWidth="600.0" minHeight="400.0" minWidth="600.0" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="space.anyi.chatServer.ui.controller.ServerController">
   <columnConstraints>
       <ColumnConstraints />
       <ColumnConstraints />
@@ -63,7 +62,7 @@
       </ScrollPane>
       <ScrollPane prefHeight="200.0" prefWidth="200.0" GridPane.columnIndex="23" GridPane.columnSpan="3" GridPane.rowIndex="3" GridPane.rowSpan="5">
          <content>
-            <ListView fx:id="userList" maxWidth="150.0" minHeight="250.0" minWidth="100.0" prefHeight="250.0" prefWidth="150.0" />
+            <VBox fx:id="userList" alignment="TOP_CENTER" prefHeight="250.0" prefWidth="150.0" />
          </content>
       </ScrollPane>
       <TextArea fx:id="inputMessage" maxWidth="400.0" minHeight="100.0" minWidth="400.0" prefColumnCount="1" prefHeight="200.0" prefRowCount="1" prefWidth="200.0" promptText="服务器广播信息" GridPane.columnIndex="14" GridPane.columnSpan="8" GridPane.rowIndex="6" GridPane.rowSpan="2">