Explorar o código

feat:服务端实现UI,初始化页面,运行页面,日志信息显示,用户列表显示,在线用户信息

yang yi hai 2 meses
pai
achega
156fefff8b

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

@@ -51,6 +51,7 @@ public class Client {
                 byte[] bytes = Encoder.encode(message);
                 socketChannel.write(ByteBuffer.wrap(bytes));
                 username = name;
+                nameFlag = true;
             }
             String line = scanner.nextLine();
             if ("exit".equals(line)){
@@ -60,9 +61,18 @@ public class Client {
             message.setSource(username);
             message.setContent(line);
             message.setTarget(Message.MessageTarget.GROUP);
+            message.setType(Message.MessageType.USER);
             byte[] bytes = Encoder.encode(message);
             socketChannel.write(ByteBuffer.wrap(bytes));
         }
+        //断开连接请求
+        Message message = new Message();
+        message.setType(Message.MessageType.SYSTEM);
+        message.setOperate(Message.OperateType.LOGOUT);
+        message.setSource(username);
+        message.setTarget(Message.MessageTarget.SERVER);
+        byte[] bytes = Encoder.encode(message);
+        socketChannel.write(ByteBuffer.wrap(bytes));
         //释放资源
         readHandler.exit();
         socketChannel.close();

+ 21 - 2
chat-gwng/chat-client/src/main/java/space/anyi/chatClient/ReadHandler.java

@@ -72,9 +72,28 @@ public class ReadHandler implements Runnable, Closeable {
                                 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());
+                                }
 
-                                //String msg = new String(byteBuffer.array(), 0, len);
-                                System.out.println(message.getSource() + ":" + message.getContent());
                             }
                         } catch (SocketException e) {
                             //连接意外中断导致异常

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

@@ -35,6 +35,46 @@ public class Message implements Serializable {
     //时间戳
     private Long timeStamp = System.currentTimeMillis();
 
+    public Message() {
+    }
+
+    public Message(String content) {
+        this.content = content;
+    }
+
+    /**
+     *
+     * @param content 信息内容
+     * @param operateType 操作类型
+     * @param source 信息来源
+     * @param target 信息目标
+     * @return {@code Message }
+     * @description: 创建系统消息
+     * @author: 杨逸
+     * @data:2025/09/26 17:07:49
+     * @since 1.0.0
+     */
+    public static Message createSystemMessage(String content, int operateType, String source, String target) {
+        Message message = new Message(content);
+        message.setType(Message.MessageType.SYSTEM);
+        message.setOperate(operateType);
+        message.setSource(source);
+        message.setTarget(target);
+        return message;
+    }
+
+    /**
+     * @param content
+     * @return {@code Message }
+     * @description: 创建系统广播信息
+     * @author: 杨逸
+     * @data:2025/09/26 18:32:46
+     * @since 1.0.0
+     */
+    public static Message createSystemBroadcastMessage(String content) {
+        return createSystemMessage(content, OperateType.BROADCAST, MessageTarget.SERVER, MessageTarget.GROUP);
+    }
+
     public static class MessageType{
         public static final int SYSTEM = 0;
         public static final int USER = 1;
@@ -42,6 +82,10 @@ public class Message implements Serializable {
     public static class OperateType{
         //服务操作:有客户登陆;客户端操作:需要重新登陆
         public static final int LOGIN = 1;
+        //服务操作:有客户下线;客户端操作:需要重新登陆
+        public static final int LOGOUT = 2;
+        //广播操作
+        public static final int BROADCAST = 3;
     }
 
     /**

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

@@ -2,14 +2,17 @@ package space.anyi.chatServer;
 
 import lombok.Data;
 import space.anyi.chatCommom.Message;
+import space.anyi.chatServer.ui.controller.ServerController;
 
 import java.io.Closeable;
 import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.net.SocketOptions;
 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;
 
@@ -29,9 +32,12 @@ public class ChatServer implements Runnable, Closeable {
     private ReadHandler readHandler;
     private WriteHandler writeHandler;
     private boolean isExit = false;
+    //JavaFX的场景控制器
+    private ServerController serverController;
 
-    public ChatServer(int port) throws IOException {
+    public ChatServer(int port,ServerController serverController) throws IOException {
         System.out.println("server init");
+        this.serverController = serverController;
         //配置服务监听的端口
         this.inetSocketAddress = new InetSocketAddress(port);
         this.serverSocketChannel = ServerSocketChannel.open().bind(inetSocketAddress);
@@ -41,7 +47,7 @@ public class ChatServer implements Runnable, Closeable {
         //创建一个监听读事件的选择器
         this.readSelector = Selector.open();
         //自定义读事件处理器,用处理客户端发送的消息
-        this.readHandler = new ReadHandler(readSelector);
+        this.readHandler = new ReadHandler(readSelector,serverController);
         this.writeHandler = new WriteHandler(readSelector);
         //读取到数据后需要,写数据的能力
         readHandler.setWriteHandler(writeHandler);
@@ -117,7 +123,7 @@ public class ChatServer implements Runnable, Closeable {
      * @data:2025/09/22 19:31:09
      * @since 1.0.0
      */
-    public void sendMessageWithPrivate(String msg,SocketChannel target){
+    public void sendMessageWithPrivate(Message msg,SocketChannel target){
         writeHandler.sendMessageWithPrivate(msg,target);
     }
 
@@ -154,4 +160,36 @@ public class ChatServer implements Runnable, Closeable {
     public void exit(){
         this.isExit = true;
     }
+
+    /**
+     * @param name
+     * @description: 注销用户操作
+     * @author: 杨逸
+     * @data:2025/09/26 17:15:07
+     * @since 1.0.0
+     */
+    public void logoutUser(String name) {
+        Selector selector = readHandler.getReadSelector();
+        Map<String, SocketChannel> userChannels = readHandler.getUserChannels();
+        SocketChannel channel = userChannels.getOrDefault(name, null);
+        //发送下线信息
+        if (Objects.nonNull(channel)) {
+            Message message = Message.createSystemMessage("下线",Message.OperateType.LOGOUT,Message.MessageTarget.SERVER,name);
+            writeHandler.sendMessageWithPrivate(message,channel);
+        }
+        //释放对应的连接
+        userChannels.remove(name);
+        if (Objects.nonNull(channel) && channel.isOpen()){
+            for (SelectionKey key : selector.keys()) {
+                if (key.channel().equals(channel)) {
+                    key.cancel();
+                }
+            }
+            try {
+                channel.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
 }

+ 100 - 19
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/ReadHandler.java

@@ -3,6 +3,7 @@ package space.anyi.chatServer;
 import lombok.Data;
 import space.anyi.chatCommom.Encoder;
 import space.anyi.chatCommom.Message;
+import space.anyi.chatServer.ui.controller.ServerController;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -30,9 +31,11 @@ public class ReadHandler implements Runnable, Closeable {
     private WriteHandler writeHandler;
     private Map<String,SocketChannel> userChannels = new HashMap<>();
     private boolean isExit = false;
+    private ServerController serverController;
 
-    public ReadHandler(Selector readSelector) {
+    public ReadHandler(Selector readSelector,ServerController serverController) {
         this.readSelector = readSelector;
+        this.serverController = serverController;
     }
 
     @Override
@@ -81,30 +84,35 @@ public class ReadHandler implements Runnable, Closeable {
                                 //信息处理
                                 if (message.getType() == Message.MessageType.USER) {
                                     //用户信息
-                                    {
-                                        //转发信息
-                                        writeHandler.sendMessageWithGroup(message, socketChannel);
+                                    String messageTarget = message.getTarget();
+                                    switch (messageTarget){
+                                        case Message.MessageTarget.SERVER:
+                                            //发送给系统的信息
+                                            //todo
+                                            break;
+                                        case Message.MessageTarget.GROUP:
+                                            //群聊
+                                            //转发信息
+                                            chatWithGroupHandler(message,socketChannel);
+                                            break;
+                                        case Message.MessageTarget.CLIENT:
+                                            //todo
+                                            break;
+                                        default:
+                                            //私聊信息
+                                            chatWithPrivateHandler(message);
+                                            break;
                                     }
                                 }else{
                                     //系统消息
                                     switch (message.getOperate()){
                                         case Message.OperateType.LOGIN:
                                             //登录
-                                            String username = message.getSource();
-                                            if (userChannels.containsKey(username) && Message.MessageTarget.SERVER.equals(username)) {
-                                                //用户名重复
-                                            }else{
-                                                //用户名保存到对应的selectionKey上,之后需要可以从里面取出
-                                                selectionKey.attach(username);
-                                                //保存用户名到连接的映射
-                                                userChannels.put(username, socketChannel);
-                                                //广播用户上线
-                                                message = new Message();
-                                                message.setSource(Message.MessageTarget.SERVER);
-                                                message.setTarget(Message.MessageTarget.GROUP);
-                                                message.setContent(username+"上线了");
-                                                writeHandler.sendMessageWithGroup(message, socketChannel);
-                                            }
+                                            loginHandler(message,selectionKey,socketChannel);
+                                            break;
+                                        case Message.OperateType.LOGOUT:
+                                            //登出
+                                            userLogoutHandler(message,selectionKey, socketChannel);
                                             break;
                                         default:
                                             System.err.println("未知操作");
@@ -114,6 +122,14 @@ public class ReadHandler implements Runnable, Closeable {
                             }
                         } catch (SocketException e) {
                             //连接意外中断导致异常
+                            Object attachment = selectionKey.attachment();
+                            String username = (String) attachment;
+                            //todo
+                            System.out.println(username+"掉线了");
+                            Message message = new Message(username+"掉线了");
+                            //更新UI
+                            serverController.addMessage(message);
+                            serverController.removeUser(username);
                             e.printStackTrace();
                             selectionKey.cancel();
                             try {
@@ -139,4 +155,69 @@ public class ReadHandler implements Runnable, Closeable {
             e.printStackTrace();
         }
     }
+
+    private void loginHandler(Message message,SelectionKey selectionKey,SocketChannel socketChannel) {
+        String username = message.getSource();
+        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);
+            //保存用户名到连接的映射
+            userChannels.put(username, socketChannel);
+            //广播用户上线
+            message = Message.createSystemBroadcastMessage(username+"上线了");
+            serverController.addUser(username);
+            serverController.addMessage(message);
+            writeHandler.sendMessageWithGroup(message, socketChannel);
+        }
+    }
+
+    private void chatWithPrivateHandler(Message message) {
+        String username = message.getSource();
+        SocketChannel socketChannel = userChannels.getOrDefault(username, null);
+        if (Objects.nonNull(socketChannel)) {
+            writeHandler.sendMessageWithPrivate(message, socketChannel);
+        }
+    }
+
+    private void chatWithGroupHandler(Message message,SocketChannel socketChannel) {
+        writeHandler.sendMessageWithGroup(message, socketChannel);
+    }
+
+    private void userLogoutHandler(Message message,SelectionKey selectionKey,SocketChannel socketChannel){
+        String username = message.getSource();
+        //删除映射channel关系
+        if(userChannels.containsKey(username))
+            userChannels.remove(username);
+        //广播用户下线
+        message = new Message();
+        message.setSource(Message.MessageTarget.SERVER);
+        message.setTarget(Message.MessageTarget.GROUP);
+        message.setContent(username+"下线了");
+        writeHandler.sendMessageWithGroup(message, socketChannel);
+        //更新UI
+        serverController.addMessage(message);
+        serverController.removeUser(username);
+        //关闭连接
+        selectionKey.cancel();
+        try {
+            if (Objects.nonNull(socketChannel) && socketChannel.isOpen()) {
+                socketChannel.close();
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
 }

+ 2 - 1
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/Server.java

@@ -19,7 +19,8 @@ import java.util.Set;
  */
 public class Server {
     public static void main(String[] args) throws IOException, InterruptedException {
-        ChatServer chatServer = new ChatServer(8000);
+        //ChatServer chatServer = new ChatServer(8000);
+        ChatServer chatServer = new ChatServer(8000,null);
         Thread thread = new Thread(chatServer);
         thread.start();
         Scanner scanner = new Scanner(System.in);

+ 1 - 0
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/TestJAVAFX.java

@@ -110,6 +110,7 @@ public class TestJAVAFX extends Application {
         //});
     }
 
+    //无法在Application的子类里启动JavaFX程序
     public static void main(String[] args) {
         launch();
     }

+ 3 - 3
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/WriteHandler.java

@@ -9,7 +9,6 @@ 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.Set;
 
 /**
@@ -86,9 +85,10 @@ public class WriteHandler {
      * @since 1.0.0
      */
     //私聊
-    public void sendMessageWithPrivate(String msg,SocketChannel to){
+    public void sendMessageWithPrivate(Message msg,SocketChannel to){
+        byte[] bytes = Encoder.encode(msg);
         try {
-            to.write(ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8)));
+            to.write(ByteBuffer.wrap(bytes));
         } catch (IOException e) {
             e.printStackTrace();
         }

+ 15 - 0
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/ui/Application.java

@@ -0,0 +1,15 @@
+package space.anyi.chatServer.ui;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: Application
+ * @Author: 杨逸
+ * @Data:2025/9/25 9:27
+ * @Description:
+ */
+public class Application {
+    public static void main(String[] args) {
+        //启动一个JavaFX的程序
+        ServerApplication.launch(ServerApplication.class);
+    }
+}

+ 64 - 0
chat-gwng/chat-server/src/main/java/space/anyi/chatServer/ui/ServerApplication.java

@@ -0,0 +1,64 @@
+package space.anyi.chatServer.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 space.anyi.chatServer.ui.controller.StartController;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: ServerApplication
+ * @Author: 杨逸
+ * @Data:2025/9/25 9:06
+ * @Description: 启动聊天室UI的启动类
+ */
+public class ServerApplication extends Application {
+    public FXMLLoader fxmlLoader;
+    public Stage stage;
+    @Override
+    public void start(Stage primaryStage) throws Exception {
+        this.stage = primaryStage;
+        //不能修改窗口大小
+        primaryStage.setResizable(false);
+        //加载fxml文件
+        fxmlLoader.setLocation(getClass().getResource("/fxml/index.fxml"));
+        Pane pane = fxmlLoader.<Pane>load();
+        StartController startController = fxmlLoader.<StartController>getController();
+        //设置舞台
+        startController.setStage(primaryStage);
+        //设置加载器
+        startController.setLoader(fxmlLoader);
+        //创建场景
+        Scene startScene = new Scene(pane);
+        primaryStage.setScene(startScene);
+        primaryStage.setTitle("聊天室 Server");
+        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();
+        }
+
+    }
+
+    public ServerApplication() {
+        this.fxmlLoader = new FXMLLoader();
+    }
+}

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

@@ -0,0 +1,228 @@
+package space.anyi.chatServer.ui.controller;
+
+import com.sun.source.tree.IfTree;
+import javafx.application.Platform;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.geometry.Pos;
+import javafx.scene.control.*;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Paint;
+import javafx.stage.Stage;
+import javafx.util.Callback;
+import lombok.Data;
+import space.anyi.chatCommom.Message;
+import space.anyi.chatServer.ChatServer;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: ServerController
+ * @Author: 杨逸
+ * @Data:2025/9/25 17:56
+ * @Description:
+ */
+@Data
+public class ServerController {
+    private ChatServer chatServer;
+    private Stage stage;
+    @FXML
+    private Button bu;
+
+    @FXML
+    private ScrollPane messagePane;
+
+    @FXML
+    private TextArea inputMessage;
+
+    @FXML
+    private Label onlineLabel;
+
+    @FXML
+    private ListView<String> userList;
+    @FXML
+    void sendMessage(MouseEvent event) {
+        //禁用按钮
+        bu.setDisable(true);
+        System.out.println("发送消息");
+        String text = inputMessage.getText();
+        inputMessage.clear();
+        text.trim();
+        if (!"".equals(text)) {
+            chatServer.broadcast(createMessage(text));
+        }
+        bu.setDisable(false);
+    }
+
+    /**
+     * @param text
+     * @return {@code Message }
+     * @description: 创建消息
+     * @author: 杨逸
+     * @data:2025/09/26 17:19:55
+     * @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;
+    }
+
+    /**
+     * @param message
+     * @description: 向信息列表添加一条信息
+     * @author: 杨逸
+     * @data:2025/09/26 17:19:23
+     * @since 1.0.0
+     */
+    public void addMessage(Message message) {
+        VBox content = (VBox) messagePane.getContent();
+        VBox msgUI = createMessageUI(message);
+        Platform.runLater(()->{
+            content.getChildren().add(msgUI);
+        });
+    }
+
+    /**
+     * @param message
+     * @return {@code VBox }
+     * @description: 创建一个信息UI
+     * @author: 杨逸
+     * @data:2025/09/26 17:22:59
+     * @since 1.0.0
+     */
+    private VBox createMessageUI(Message message) {
+        VBox result = new VBox();
+        HBox head = new HBox();
+        Label username = new Label(message.getSource());
+        Label mes = new Label(message.getContent());
+
+        //右键菜单
+        //result.setOnContextMenuRequested(event->{
+        //    ContextMenu contextMenu = new ContextMenu();
+        //    MenuItem delete = new MenuItem("删除");
+        //    delete.setOnAction(event1->{
+        //        //删除消息
+        //        VBox content = (VBox) messagePane.getContent();
+        //        content.getChildren().remove(result);
+        //    });
+        //    contextMenu.getItems().add(delete);
+        //    contextMenu.show(result, event.getScreenX(), event.getScreenY());
+        //});
+        //内容
+        Long timeStamp = message.getTimeStamp();
+        Instant instant = Instant.ofEpochMilli(timeStamp);
+        LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
+        Label time = new Label(localDateTime.format(DateTimeFormatter.ISO_DATE_TIME));
+
+        //布局,样式
+        double width = 400;
+        double height = 50;
+        result.setPrefWidth(width);
+        result.setPrefHeight(height);
+        result.setStyle("-fx-background-color: #3C3F41;-fx-background-radius: 100;");
+        head.setPrefWidth(width);
+        head.setPrefHeight(height-15);
+        username.setPrefWidth(100);
+        username.setPrefHeight(height-35);
+        username.setAlignment(Pos.CENTER);
+        username.setTextFill(Paint.valueOf("#FFFFFF"));
+        mes.setPrefWidth(width-100);
+        mes.setPrefHeight(height-35);
+        mes.setAlignment(Pos.CENTER_LEFT);
+        mes.setTextFill(Paint.valueOf("#FFFFFF"));
+        time.setPrefWidth(width);
+        time.setPrefHeight(15);
+        time.setAlignment(Pos.CENTER);
+        time.setTextFill(Paint.valueOf("#FFFFFF"));
+        //组装
+        head.getChildren().add(username);
+        head.getChildren().add(mes);
+        result.getChildren().add(head);
+        result.getChildren().add(time);
+        return result;
+    }
+
+    /**
+     * 向用户列表添加用户
+     * @param username
+     * @description:
+     * @author: 杨逸
+     * @data:2025/09/25 20:11:22
+     * @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.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
+     * @description:
+     * @author: 杨逸
+     * @data:2025/09/25 20:12:51
+     * @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());
+            }));
+        }
+    }
+}

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

@@ -0,0 +1,87 @@
+package space.anyi.chatServer.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.Stage;
+import lombok.Data;
+import space.anyi.chatServer.ChatServer;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * @ProjectName: chat-gwng
+ * @FileName: StartController
+ * @Author: 杨逸
+ * @Data:2025/9/25 9:45
+ * @Description:
+ */
+@Data
+public class StartController {
+    //窗口对象
+    private Stage stage;
+    //场景加载器
+    private FXMLLoader loader;
+    @FXML
+    private TextField serverPort;
+
+    @FXML
+    private Button startBu;
+
+    @FXML
+    private Pane startPane;
+
+    @FXML
+    void startServerHandler(ActionEvent event) throws IOException {
+        //切换页面
+        FXMLLoader fxmlLoader = new FXMLLoader();
+        fxmlLoader.setLocation(getClass().getResource("/fxml/server.fxml"));
+        Pane pane = fxmlLoader.load();
+        ServerController serverController = fxmlLoader.<ServerController>getController();
+        serverController.setStage(stage);
+        Scene mainScene = new Scene(pane);
+        //准备启动chatServer
+        String port = serverPort.getCharacters().toString();
+        serverPort.clear();
+        int p = -1;
+        if (Objects.nonNull(port) && !port.equals("")) {
+            p = Integer.valueOf(port);
+        }
+        ChatServer chatServer = null;
+        try {
+            chatServer = new ChatServer(p,serverController);
+            //启动服务器
+            new Thread(chatServer).start();
+            serverController.setChatServer(chatServer);
+        } 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("窗口隐藏后");
+        });
+    }
+
+}

+ 25 - 0
chat-gwng/chat-server/src/main/resources/fxml/index.fxml

@@ -0,0 +1,25 @@
+<?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 fx:id="startPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" 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.StartController">
+   <children>
+      <Label alignment="CENTER" contentDisplay="CENTER" layoutX="-1.0" prefHeight="79.0" prefWidth="600.0" text="聊天室 Server" textAlignment="CENTER" textOverrun="CENTER_ELLIPSIS">
+         <font>
+            <Font size="28.0" />
+         </font>
+      </Label>
+      <TextField fx:id="serverPort" alignment="CENTER" layoutX="284.0" layoutY="187.0" prefHeight="30.0" prefWidth="81.0" promptText="设置服务端监听的端口" text="8000" />
+      <Button fx:id="startBu" layoutX="218.0" layoutY="233.0" mnemonicParsing="false" onAction="#startServerHandler" prefHeight="71.0" prefWidth="162.0" text="启动服务器" />
+      <Label alignment="BOTTOM_CENTER" layoutX="-1.0" layoutY="304.0" prefHeight="96.0" prefWidth="600.0" text="25计科4(专升本)-2520601429-黄略">
+         <font>
+            <Font size="14.0" />
+         </font>
+      </Label>
+      <Label layoutX="245.0" layoutY="187.0" prefHeight="30.0" prefWidth="39.0" text="端口:" />
+   </children>
+</Pane>

+ 81 - 0
chat-gwng/chat-server/src/main/resources/fxml/server.fxml

@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?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?>
+<?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:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/24.0.1" fx:controller="space.anyi.chatServer.ui.controller.ServerController">
+  <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="聊天室 Server" 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 alignment="TOP_CENTER" maxWidth="450.0" minHeight="200.0" minWidth="430.0" prefHeight="200.0" prefWidth="400.0" />
+         </content>
+      </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" />
+         </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" onMouseClicked="#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>

+ 1 - 1
chat-gwng/pom.xml

@@ -21,7 +21,7 @@
         <maven.compiler.target>17</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <lombok.version>1.18.32</lombok.version>
-        <javafx.version>22.0.1</javafx.version>
+        <javafx.version>21.0.1</javafx.version>
     </properties>
     <dependencyManagement>
         <dependencies>