[JavaFX]基于socket实现的简单聊天室之UI与前后端交互

前言

本文所指向的项目为本人大一的课程设计作业,由本人和@JinYu_LZ 以及@眠れない的荔枝冻 合作开发,由于经验不足,代码严谨性可能存在纰漏(尤其是UI画的跟史一样,这个后续可能会改一下),欢迎各位大佬批评指正。

本项目源码均已上传github:Torchman005/Simple-Chat-Room

项目简介

本项目采用Java Socket实现,支持多用户注册、登录、群聊、私聊等功能。前端采用JavaFX实现图形界面,后端通过多线程处理客户端请求,实现消息的实时转发。

UI界面设计

聊天室客户端采用JavaFX实现,主要界面包括登录/注册界面和主聊天窗口。用户登录后可看到在线用户列表,支持选择用户进行私聊或发送群消息。

主要UI类:

  • LoginUI:登录/注册界面
  • ChatUI:主聊天窗口,显示消息和在线用户

登录界面的代码示例:

前后端交互流程

前端通过Socket与服务端通信,所有消息均通过对象流(ObjectInputStream/ObjectOutputStream)传递。用户操作(如登录、发送消息)会封装为Message对象发送到服务端,服务端解析后进行相应处理,并将结果返回客户端。

交互流程示意:

        1.客户端发起登录/注册请求,服务端校验后返回结果

        2.登录成功后,客户端进入主界面,服务端推送在线用户列表

        3.客户端发送消息(群聊/私聊),服务端转发到目标用户

        4.用户下线,服务端通知所有客户端更新在线列表

关键类与源代码

User

User类用于封装用户的基本信息,客户端和服务端均需保持一致

Message

Message类用于封装所有前后端交互的数据,包括消息类型、内容、发送者、接收者等。

ChatUI(客户端主界面)

ChatUI负责显示聊天内容和在线用户列表,用户可选择私聊或群聊以及发送文件。

package com.jinyu.ui;

import java.util.Queue;
import java.util.Map;

import com.jinyu.chatclient.service.ClientMessageService;
import com.jinyu.chatclient.service.ToUserFunction;
import com.jinyu.chatcommon.Message;
import com.jinyu.chatcommon.User;
import com.jinyu.chatcommon.MessageType;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.scene.image.Image;

import java.io.File;
import java.io.ObjectOutputStream;
import com.jinyu.chatclient.service.ClientConnServerThreadsManage;

public class ChatUI extends Application {
    private User user;
    private TextArea chatArea;
    private ListView<String> onlineUsersList;
    private TextField messageInput;
    private ComboBox<String> groupComboBox;
    private VBox mainLayout;
    private Stage primaryStage;
    private ObservableList<String> onlineUsers = FXCollections.observableArrayList();
    private Label currentChatTarget;
    private String currentChatUserId;
    private ToUserFunction toUserFunction;
    private HBox toolbar;
    private Map<String, java.lang.StringBuilder> sessionHistories = new java.util.HashMap<>();

    public void setUser(User user) {
        this.user = user;
        if (primaryStage != null) {
            primaryStage.setTitle("聊天系统 - " + user.getUserId());
        }
    }

    @Override
    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;
        createMainUI();
    }

    private void createMainUI() {
        primaryStage.setTitle("聊天系统");

        // 创建主布局
        mainLayout = new VBox(10);
        mainLayout.setPadding(new Insets(15));
        // 渐变背景:左上到右下浅蓝到深蓝
        mainLayout.setStyle(
                "-fx-background-color: linear-gradient(from 0% 0% to 100% 100%, #e3f0ff, #2196f3 80%, #1565c0); " +
                        "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif;");

        // 创建顶部工具栏
        toolbar = createToolbar();
        // 玻璃模糊效果(已移除)
        // toolbar.setEffect(new javafx.scene.effect.GaussianBlur(18));

        // 创建聊天区域
        chatArea = new TextArea();
        chatArea.setEditable(false);
        chatArea.setWrapText(true);
        chatArea.setPrefHeight(400);
        chatArea.setStyle("-fx-font-size: 14px; " +
                "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                "-fx-background-color: rgba(255, 255, 255, 0.45); " +
                "-fx-text-fill: black; " +
                "-fx-background-radius: 10; " +
                "-fx-border-radius: 10; " +
                "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                "-fx-border-width: 2; " +
                "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");
        // chatArea.setEffect(new javafx.scene.effect.GaussianBlur(10));

        // 创建在线用户列表
        onlineUsersList = new ListView<>(onlineUsers);
        onlineUsersList.setPrefWidth(200);
        onlineUsersList.setPrefHeight(400);
        onlineUsersList.setMinHeight(200);
        onlineUsersList.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        // 美化:自定义CellFactory(淡蓝色卡片,透明度高,背景可见)
        onlineUsersList.setCellFactory(listView -> new javafx.scene.control.ListCell<String>() {
            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                if (empty || item == null) {
                    setText(null);
                    setGraphic(null);
                    setStyle("");
                } else {
                    setText(item);
                    boolean isSelf = user != null && item.equals(user.getUserId());
                    // 主色为淡蓝色,透明度高
                    String baseBg = "rgba(173,216,230,0.35)"; // #ADD8E6, 0.35透明度
                    String selfBg = "rgba(129,212,250,0.45)"; // #81d4fa, 0.45透明度
                    String selectedBg = "linear-gradient(to right, #42a5f5, #1976d2)";
                    String hoverBg = "rgba(144,202,249,0.55)"; // #90caf9, 0.55透明度
                    setStyle("-fx-background-color: " +
                            (isSelf ? selfBg : baseBg) + ";" +
                            "-fx-background-radius: 14;" +
                            "-fx-border-radius: 14;" +
                            "-fx-border-color: rgba(33,150,243,0.10);" +
                            "-fx-border-width: 1.2;" +
                            "-fx-padding: 10 18;" +
                            "-fx-font-size: 15px;" +
                            "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif;" +
                            (isSelf ? "-fx-text-fill: #1976d2; font-weight:bold;" : "-fx-text-fill: #1565c0;") +
                            "-fx-effect: dropshadow(gaussian, rgba(33,150,243,0.06), 6, 0, 0, 1);");
                    // 选中高亮为深蓝色渐变
                    if (isSelected()) {
                        setStyle(getStyle() + "-fx-background-color: " + selectedBg
                                + ";-fx-text-fill: white;font-weight:bold;");
                    }
                    // 悬浮高亮为更深的淡蓝色
                    setOnMouseEntered(e -> setStyle(
                            getStyle() + "-fx-background-color: " + hoverBg + ";-fx-text-fill: #1976d2;"));
                    setOnMouseExited(e -> {
                        setStyle("-fx-background-color: " +
                                (isSelf ? selfBg : baseBg) + ";" +
                                "-fx-background-radius: 14;" +
                                "-fx-border-radius: 14;" +
                                "-fx-border-color: rgba(33,150,243,0.10);" +
                                "-fx-border-width: 1.2;" +
                                "-fx-padding: 10 18;" +
                                "-fx-font-size: 15px;" +
                                "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif;" +
                                (isSelf ? "-fx-text-fill: #1976d2; font-weight:bold;" : "-fx-text-fill: #1565c0;") +
                                "-fx-effect: dropshadow(gaussian, rgba(33,150,243,0.06), 6, 0, 0, 1);");
                        if (isSelected()) {
                            setStyle(getStyle() + "-fx-background-color: " + selectedBg
                                    + ";-fx-text-fill: white;font-weight:bold;");
                        }
                    });
                }
            }
        });
        // onlineUsersList.setEffect(new javafx.scene.effect.GaussianBlur(10));

        // 添加当前聊天对象显示
        currentChatTarget = new Label("当前聊天对象: 未选择");
        currentChatTarget.setStyle("-fx-text-fill: black; -fx-font-size: 14px; -fx-font-weight: bold; " +
                "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                "-fx-background-color: rgba(255, 255, 255, 0.5); " +
                "-fx-padding: 5; " +
                "-fx-background-radius: 5; " +
                "-fx-border-radius: 5; " +
                "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                "-fx-border-width: 2; " +
                "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");

        // 创建消息输入区域
        HBox inputArea = createInputArea();
        // inputArea.setEffect(new javafx.scene.effect.GaussianBlur(8));

        // 创建群组选择区域
        groupComboBox = new ComboBox<>();
        groupComboBox.setPromptText("选择群组");
        groupComboBox.setPrefWidth(200);
        groupComboBox.setVisible(false);
        groupComboBox.setStyle("-fx-font-family: 'Microsoft YaHei', 'SimHei', sans-serif; " +
                "-fx-background-color: rgba(255, 255, 255, 0.5); " +
                "-fx-text-fill: black; " +
                "-fx-background-radius: 10; -fx-border-radius: 10; " +
                "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                "-fx-border-width: 2; " +
                "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");

        // 布局设置
        HBox contentArea = new HBox(15);
        VBox chatInfo = new VBox(10);
        chatInfo.getChildren().addAll(currentChatTarget, chatArea);
        VBox.setVgrow(chatArea, Priority.ALWAYS);

        contentArea.getChildren().addAll(chatInfo, onlineUsersList);
        HBox.setHgrow(chatInfo, Priority.ALWAYS);

        mainLayout.getChildren().addAll(toolbar, contentArea, groupComboBox, inputArea);

        // 在mainLayout顶部叠加一层高光矩形,模拟玻璃反光
        javafx.scene.shape.Rectangle highlight = new javafx.scene.shape.Rectangle(800, 80);
        highlight.setFill(
                new javafx.scene.paint.LinearGradient(0, 0, 1, 0, true, javafx.scene.paint.CycleMethod.NO_CYCLE,
                        new javafx.scene.paint.Stop(0, javafx.scene.paint.Color.rgb(255, 255, 255, 0.45)),
                        new javafx.scene.paint.Stop(1, javafx.scene.paint.Color.rgb(255, 255, 255, 0.05))));
        highlight.setArcHeight(40);
        highlight.setArcWidth(40);
        highlight.setMouseTransparent(true);
        mainLayout.getChildren().add(0, highlight);

        Scene scene = new Scene(mainLayout, 1000, 600);
        primaryStage.setScene(scene);
        // 设置默认背景
        try {
            java.net.URL bgUrl = getClass().getResource("/images/default_bg.jpg");
            if (bgUrl != null) {
                File bgFile = new File(bgUrl.toURI());
                setBackground(bgFile, 0.7); // 0.7为透明度
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        primaryStage.show();

        // 实现点击在线用户昵称直接私聊
        onlineUsersList.setOnMouseClicked(event -> {
            String selectedUser = onlineUsersList.getSelectionModel().getSelectedItem();
            if (selectedUser != null && !selectedUser.equals(user.getUserId())) {
                setCurrentChatTarget(selectedUser);
            }
        });
    }

    private HBox createToolbar() {
        HBox toolbar = new HBox(15);
        toolbar.setAlignment(Pos.CENTER_LEFT);
        toolbar.setPadding(new Insets(10));
        toolbar.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); " +
                "-fx-background-radius: 10; -fx-border-radius: 10; " +
                "-fx-border-color: rgba(255, 255, 255, 0.1); -fx-border-width: 1;");

        Button refreshButton = createStyledButton("刷新用户列表", this::reqOnlineUserList);
        Button privateChatButton = createStyledButton("私聊", () -> {
            // 弹出私聊选择框
            PrivateChatDialog dialog = new PrivateChatDialog(this);
            dialog.show();
        });
        Button groupChatButton = createStyledButton("群聊", () -> {
            // 弹出群聊列表对话框
            GroupChatDialog dialog = new GroupChatDialog(this);
            dialog.show();
        });
        Button createGroupButton = createStyledButton("创建群组", () -> {
            // 弹出创建群聊对话框
            CreateGroupDialog dialog = new CreateGroupDialog(this);
            dialog.show();
        });
        Button fileButton = createStyledButton("发送文件", () -> {
            FileSendDialog dialog = new FileSendDialog(this);
            dialog.show();
        });
        Button backgroundButton = createStyledButton("选择背景", () -> {
        });
        Button logoutButton = createStyledButton("退出", () -> {
            if (toUserFunction != null) {
                toUserFunction.logout();
            } else {
                // 兜底直接退出
                System.exit(0);
            }
            if (primaryStage != null) {
                primaryStage.close();
            }
        });

        toolbar.getChildren().addAll(refreshButton, privateChatButton, groupChatButton, createGroupButton, fileButton,
                backgroundButton, logoutButton);

        return toolbar;
    }

    private HBox createInputArea() {
        HBox inputArea = new HBox(15);
        inputArea.setAlignment(Pos.CENTER);
        inputArea.setPadding(new Insets(10));
        inputArea.setStyle("-fx-background-color: rgba(255, 255, 255, 0.5); " +
                "-fx-background-radius: 10; -fx-border-radius: 10; " +
                "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                "-fx-border-width: 2; " +
                "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");

        messageInput = new TextField();
        messageInput.setPromptText("输入消息...");
        messageInput.setPrefWidth(500);
        messageInput.setStyle("-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                "-fx-background-color: rgba(255, 255, 255, 0.5); " +
                "-fx-text-fill: black; " +
                "-fx-prompt-text-fill: rgba(0, 0, 0, 0.7); " +
                "-fx-background-radius: 20; -fx-border-radius: 20; " +
                "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                "-fx-border-width: 2; " +
                "-fx-padding: 8 15; " +
                "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");

        Button sendButton = createStyledButton("发送", () -> {
            String content = messageInput.getText().trim();
            if (content.isEmpty())
                return;
            if (currentChatUserId != null) {
                // 私聊
                new ClientMessageService().sendMessageToOne(content, user.getUserId(), currentChatUserId);
                // 本地显示自己发的消息
                Message selfMsg = new Message(MessageType.MESSAGE_COMM_MES);
                selfMsg.setSender(user.getUserId());
                selfMsg.setGetter(currentChatUserId);
                selfMsg.setContent(content);
                selfMsg.setSendTime(new java.util.Date().toString());
                displayMessage(selfMsg);
            } else if (groupComboBox.isVisible() && groupComboBox.getValue() != null) {
                // 群聊
                new ClientMessageService().sendMessageToGroup(content, user.getUserId(), groupComboBox.getValue());
                // 本地显示自己发的群消息
                Message selfMsg = new Message(MessageType.MESSAGE_TO_GROUP_MES);
                selfMsg.setSender(user.getUserId());
                selfMsg.setGroupName(groupComboBox.getValue());
                selfMsg.setContent(content);
                selfMsg.setSendTime(new java.util.Date().toString());
                displayMessage(selfMsg);
            } else {
                displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "请选择聊天对象或群组"));
                return;
            }
            messageInput.clear();
        });

        inputArea.getChildren().addAll(messageInput, sendButton);
        return inputArea;
    }

    private Button createStyledButton(String text, Runnable action) {
        Button button = new Button(text);
        button.setStyle("-fx-background-color: linear-gradient(to right, #2196f3, #1976d2); " +
                "-fx-text-fill: white; -fx-font-weight: bold; " +
                "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                "-fx-background-radius: 20; -fx-border-radius: 20; " +
                "-fx-padding: 8 20; " +
                "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");

        button.setOnAction(e -> action.run());

        // 添加悬停效果
        button.setOnMouseEntered(
                e -> button.setStyle("-fx-background-color: linear-gradient(to right, #42a5f5, #1e88e5); " +
                        "-fx-text-fill: white; -fx-font-weight: bold; " +
                        "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                        "-fx-background-radius: 20; -fx-border-radius: 20; " +
                        "-fx-padding: 8 20; " +
                        "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 15, 0, 0, 3);"));

        button.setOnMouseExited(
                e -> button.setStyle("-fx-background-color: linear-gradient(to right, #2196f3, #1976d2); " +
                        "-fx-text-fill: white; -fx-font-weight: bold; " +
                        "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                        "-fx-background-radius: 20; -fx-border-radius: 20; " +
                        "-fx-padding: 8 20; " +
                        "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);"));

        return button;
    }

    public void updateOnlineUsers(Queue<String> users) {
        System.out.println("客户端刷新在线用户:" + users);
        Platform.runLater(() -> {
            onlineUsers.clear();
            if (users != null)
                onlineUsers.addAll(users);
            onlineUsersList.setItems(null); // 彻底解绑
            onlineUsersList.setItems(onlineUsers); // 重新绑定
            onlineUsersList.refresh();
            System.out.println("ListView实际显示项数:" + onlineUsersList.getItems().size());
        });
    }

    public void displayMessage(Message message) {
        if (chatArea == null)
            return;
        String sessionKey = getSessionKey(message);
        java.lang.StringBuilder history = sessionHistories.computeIfAbsent(sessionKey,
                k -> new java.lang.StringBuilder());
        String displayText;
        if (MessageType.MESSAGE_SYSTEM.equals(message.getMesType())) {
            displayText = "[系统] " + message.getContent();
        } else if (MessageType.MESSAGE_TO_GROUP_MES.equals(message.getMesType())) {
            displayText = String.format("[%s] %s@%s: %s", message.getSendTime(), message.getSender(),
                    message.getGroupName(), message.getContent());
        } else if (MessageType.MESSAGE_FILE_MES.equals(message.getMesType())) {
            String fileName = new java.io.File(message.getSrc()).getName();
            displayText = String.format("[%s] %s 发送文件: %s (%d bytes)", message.getSendTime(), message.getSender(),
                    fileName, message.getFileLen());
        } else {
            displayText = String.format("[%s] %s: %s", message.getSendTime(), message.getSender(),
                    message.getContent());
        }
        // 根据发送者决定左右显示
        String formatted = message.getSender() != null && message.getSender().equals(user.getUserId())
                ? rightAlign(displayText)
                : displayText;
        history.append("\n").append(formatted);
        // 判断当前会话是否匹配
        boolean shouldShow = false;
        if (MessageType.MESSAGE_TO_GROUP_MES.equals(message.getMesType())) {
            shouldShow = groupComboBox.isVisible() && message.getGroupName().equals(groupComboBox.getValue());
        } else if (MessageType.MESSAGE_COMM_MES.equals(message.getMesType())) {
            shouldShow = message.getSender().equals(currentChatUserId) || message.getGetter().equals(currentChatUserId);
        } else if (MessageType.MESSAGE_SYSTEM.equals(message.getMesType())) {
            shouldShow = true;
        }
        if (shouldShow) {
            javafx.application.Platform.runLater(() -> chatArea.appendText("\n" + formatted));
        }
        if (MessageType.MESSAGE_FILE_MES.equals(message.getMesType()) && message.getFileBytes() != null) {
            javafx.application.Platform.runLater(() -> saveFile(message));
        }
    }

    public void setCurrentChatTarget(String userId) {
        this.currentChatUserId = userId;
        Platform.runLater(() -> {
            currentChatTarget.setText("当前聊天对象: " + userId);
            chatArea.clear();
            String key = userId == null ? "SYS"
                    : (userId.compareTo(user.getUserId()) < 0 ? userId + "#" + user.getUserId()
                            : user.getUserId() + "#" + userId);
            chatArea.appendText(sessionHistories.getOrDefault(key, new java.lang.StringBuilder()).toString());
        });
    }

    public void setBackground(File imageFile, double opacity) {
        try {
            Image image = new Image(imageFile.toURI().toString());
            BackgroundImage backgroundImage = new BackgroundImage(
                    image,
                    BackgroundRepeat.NO_REPEAT,
                    BackgroundRepeat.NO_REPEAT,
                    BackgroundPosition.CENTER,
                    new BackgroundSize(100, 100, true, true, true, true));

            // 创建一个半透明的背景
            Background background = new Background(backgroundImage);

            // 设置主布局的背景图片
            mainLayout.setBackground(background);

            // 设置主布局的样式(移除之前的背景色)
            mainLayout.setStyle("-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif;");

            // 设置聊天区域的背景透明度
            chatArea.setStyle("-fx-font-size: 14px; " +
                    "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                    "-fx-background-color: rgba(255, 255, 255, " + opacity + "); " +
                    "-fx-text-fill: black; " +
                    "-fx-background-radius: 10; " +
                    "-fx-border-radius: 10; " +
                    "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                    "-fx-border-width: 2; " +
                    "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");

            // 设置用户列表的背景透明度(玻璃效果)
            onlineUsersList.setStyle("-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                    "-fx-background-color: rgba(255, 255, 255, " + opacity + "); " +
                    "-fx-text-fill: black; " +
                    "-fx-background-radius: 10; " +
                    "-fx-border-radius: 10; " +
                    "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                    "-fx-border-width: 2; " +
                    "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");

            // 设置工具栏的背景透明度
            if (toolbar != null) {
                toolbar.setStyle("-fx-background-color: rgba(255, 255, 255, " + opacity + "); " +
                        "-fx-background-radius: 10; -fx-border-radius: 10; " +
                        "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                        "-fx-border-width: 2; " +
                        "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");
            }

            // 设置输入区域的背景透明度
            HBox inputArea = null;
            for (javafx.scene.Node node : mainLayout.getChildren()) {
                if (node instanceof HBox && node != toolbar) {
                    inputArea = (HBox) node;
                }
            }
            if (inputArea != null) {
                inputArea.setStyle("-fx-background-color: rgba(255, 255, 255, " + opacity + "); " +
                        "-fx-background-radius: 10; -fx-border-radius: 10; " +
                        "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                        "-fx-border-width: 2; " +
                        "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");

                // 设置消息输入框的背景透明度
                for (javafx.scene.Node child : inputArea.getChildren()) {
                    if (child instanceof TextField) {
                        TextField messageInput = (TextField) child;
                        messageInput
                                .setStyle("-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                                        "-fx-background-color: rgba(255, 255, 255, " + opacity + "); " +
                                        "-fx-text-fill: black; " +
                                        "-fx-prompt-text-fill: rgba(0, 0, 0, 0.7); " +
                                        "-fx-background-radius: 20; -fx-border-radius: 20; " +
                                        "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                                        "-fx-border-width: 2; " +
                                        "-fx-padding: 8 15; " +
                                        "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");
                    }
                }
            }

            // 设置当前聊天对象标签的样式
            currentChatTarget.setStyle("-fx-text-fill: black; -fx-font-size: 14px; -fx-font-weight: bold; " +
                    "-fx-font-family: 'Microsoft YaHei', 'SimHei', 'PingFang SC', sans-serif; " +
                    "-fx-background-color: rgba(255, 255, 255, " + opacity + "); " +
                    "-fx-padding: 5; " +
                    "-fx-background-radius: 5; " +
                    "-fx-border-radius: 5; " +
                    "-fx-border-color: rgba(255, 255, 255, 0.3); " +
                    "-fx-border-width: 2; " +
                    "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 10, 0, 0, 2);");
            System.out.println(currentChatTarget.getText());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void reqOnlineUserList() {
        if (user == null)
            return;
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_REQ_ONLINE_USERS);
        message.setSender(user.getUserId());
        try {
            ObjectOutputStream oos = new ObjectOutputStream(
                    ClientConnServerThreadsManage.getClientConnectServerThread(user.getUserId()).getSocket()
                            .getOutputStream());
            oos.writeObject(message);
        } catch (Exception e) {
            displayMessage(new Message(MessageType.MESSAGE_SYSTEM, "获取在线用户列表失败: " + e.getMessage()));
        }
    }

    public void setToUserFunction(ToUserFunction toUserFunction) {
        this.toUserFunction = toUserFunction;
    }

    public User getUser() {
        return this.user;
    }

    public ToUserFunction getToUserFunction() {
        return this.toUserFunction;
    }

    /**
     * 创建群聊(被CreateGroupDialog调用)
     * 
     * @param groupName 群聊名称
     * @param members   群成员列表
     */
    public void createGroup(String groupName, java.util.List<String> members) {
        // 构造消息并发送到服务端
        com.jinyu.chatclient.service.Group groupService = new com.jinyu.chatclient.service.Group();
        java.util.Queue<String> groupMembers = new java.util.LinkedList<>();
        groupMembers.add(user.getUserId()); // 把自己加进去
        groupMembers.addAll(members);
        com.jinyu.chatcommon.Message mes = new com.jinyu.chatcommon.Message();
        mes.setMesType(com.jinyu.chatcommon.MessageType.MESSAGE_PULL_GROUP_MES);
        mes.setSender(user.getUserId());
        mes.setGroupName(groupName);
        mes.setGroupMembers(groupMembers);
        try {
            java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(
                    com.jinyu.chatclient.service.ClientConnServerThreadsManage
                            .getClientConnectServerThread(user.getUserId()).getSocket().getOutputStream());
            oos.writeObject(mes);
            displayMessage(new com.jinyu.chatcommon.Message(com.jinyu.chatcommon.MessageType.MESSAGE_SYSTEM,
                    "已发送创建群聊请求: " + groupName));
        } catch (Exception e) {
            displayMessage(new com.jinyu.chatcommon.Message(com.jinyu.chatcommon.MessageType.MESSAGE_SYSTEM,
                    "创建群聊失败: " + e.getMessage()));
        }
    }

    /**
     * 切换到群聊聊天界面
     * 
     * @param groupName 群聊名称
     */
    public void joinGroup(String groupName) {
        this.currentChatUserId = null;
        Platform.runLater(() -> {
            currentChatTarget.setText("当前群聊: " + groupName);
            chatArea.clear();
            groupComboBox.setVisible(true);
            if (!groupComboBox.getItems().contains(groupName)) {
                groupComboBox.getItems().add(groupName);
            }
            groupComboBox.setValue(groupName);
            String key = "G:" + groupName;
            chatArea.appendText(sessionHistories.getOrDefault(key, new java.lang.StringBuilder()).toString());
        });
    }

    // 主动请求群聊列表
    public void requestGroupList() {
        try {
            com.jinyu.chatcommon.Message mes = new com.jinyu.chatcommon.Message();
            mes.setMesType(com.jinyu.chatcommon.MessageType.MESSAGE_REQ_GROUP_LIST);
            mes.setSender(user.getUserId());
            java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(
                    com.jinyu.chatclient.service.ClientConnServerThreadsManage
                            .getClientConnectServerThread(user.getUserId()).getSocket().getOutputStream());
            oos.writeObject(mes);
        } catch (Exception e) {
            displayMessage(new com.jinyu.chatcommon.Message(com.jinyu.chatcommon.MessageType.MESSAGE_SYSTEM,
                    "请求群聊列表失败: " + e.getMessage()));
        }
    }

    // 处理服务端返回的群聊列表
    public void handleGroupListMsg(String groupListStr) {
        java.util.List<String> groupNames = new java.util.ArrayList<>();
        if (groupListStr != null && !groupListStr.isEmpty()) {
            for (String name : groupListStr.split(",")) {
                if (!name.trim().isEmpty())
                    groupNames.add(name.trim());
            }
        }
        // 分发给所有GroupChatDialog实例(这里只处理当前弹窗)
        if (com.jinyu.ui.GroupChatDialog.currentInstance != null) {
            com.jinyu.ui.GroupChatDialog.currentInstance.updateGroupList(groupNames);
        }
    }

    private String getSessionKey(Message message) {
        if (MessageType.MESSAGE_TO_GROUP_MES.equals(message.getMesType())) {
            return "G:" + message.getGroupName();
        } else if (MessageType.MESSAGE_COMM_MES.equals(message.getMesType())) {
            // 私聊:会话键统一为两人中较小-较大的拼接,确保双方一致
            String a = message.getSender();
            String b = message.getGetter();
            if (a == null || b == null)
                return "SYS";
            return a.compareTo(b) < 0 ? a + "#" + b : b + "#" + a;
        } else {
            return "SYS";
        }
    }

    // 简单右对齐:在前面补足空格到固定宽度
    private String rightAlign(String text) {
        int width = 80; // 依据字体大致列宽,可调整
        int len = text.length();
        if (len >= width)
            return text; // 太长不处理
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < width - len; i++)
            sb.append(' ');
        sb.append(text);
        return sb.toString();
    }

    private void saveFile(Message message) {
        javafx.stage.FileChooser chooser = new javafx.stage.FileChooser();
        chooser.setTitle("保存文件 - " + message.getSrc());
        chooser.setInitialFileName(new java.io.File(message.getSrc()).getName());
        java.io.File dest = chooser.showSaveDialog(primaryStage);
        if (dest == null)
            return; // 取消
        try (java.io.FileOutputStream fos = new java.io.FileOutputStream(dest)) {
            fos.write(message.getFileBytes(), 0, message.getFileLen());
            displayMessage(new com.jinyu.chatcommon.Message(com.jinyu.chatcommon.MessageType.MESSAGE_SYSTEM,
                    "文件已保存到 " + dest.getAbsolutePath()));
        } catch (Exception e) {
            displayMessage(new com.jinyu.chatcommon.Message(com.jinyu.chatcommon.MessageType.MESSAGE_SYSTEM,
                    "保存文件失败: " + e.getMessage()));
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

ChatServer(服务端主线程)

服务端主线程负责监听客户端连接,并为每个客户端分配一个通信线程。

package com.jinyu.chatserver.service;

import com.jinyu.chatcommon.Message;
import com.jinyu.chatcommon.MessageType;
import com.jinyu.chatcommon.User;
import com.jinyu.chatcommon.UserType;
import com.jinyu.dbexecute.SQLs;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class ChatServer {
    ServerSocket ss = null;
    SQLs sqls = new SQLs();
    // 将用户信息统一存放到项目根目录 Simple-Chat-Room-main/users.txt
    private static final String USER_FILE = "." + File.separator + "users.txt";
    private static Map<String, String> userMap = new HashMap<>();

    // 加载用户信息
    private void loadUsers() {
        try (BufferedReader br = new BufferedReader(new FileReader(USER_FILE))) {
            String line;
            while ((line = br.readLine()) != null) {
                String[] arr = line.split(",");
                if (arr.length == 2)
                    userMap.put(arr[0], arr[1]);
            }
        } catch (Exception e) {
            /* 文件不存在时忽略 */ }
    }

    public ChatServer() throws Exception {
        try {
            // 启动推送新闻的线程
            new Thread(new SendNewsToAllService()).start();

            // 读取配置文件
            ClassLoader classLoader = ChatServer.class.getClassLoader();
            InputStream input = classLoader.getResourceAsStream("config.properties");
            Properties prop = new Properties();
            prop.load(input);
            String sport = prop.getProperty("port");
            int port = Integer.parseInt(sport);
            System.out.println("服务端在" + port + "端口监听");
            ss = new ServerSocket(port);

            // 加载用户信息
            loadUsers();

            while (true) {
                Socket socket = ss.accept();// 监听客户端的连接,若没有则阻塞

                // 对象输入输出流读取用户对象
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                User user = (User) ois.readObject();
                // 准备一个Message对象,用来回复客户端
                Message message = new Message();

                if (user.getUserType().equals(UserType.USER_LOGIN)) {
                    // 登录校验
                    if (sqls.login(user.getUserId(), user.getPwd())) {
                        message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
                        // 加入线程
                        ServerConnectClientThread thread = new ServerConnectClientThread(socket, user.getUserId());
                        thread.start();
                        ClientThreadsManage.addServerConnectClientThread(user.getUserId(), thread);
                        // 将用户Id加入在线用户队列
                        OnlineUsers.addOnlineUsers(user.getUserId());
                        // 然后把message传给客户端
                        oos.writeObject(message);
                    } else {
                        // 登录失败
                        System.out.println("用户账号或密码不正确");
                        message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                        oos.writeObject(message);
                        socket.close();
                    }
                } else if (user.getUserType().equals(UserType.USER_REGISTER)) {
                    // 用来判断是否是用户名已存在错误
                    boolean ex = false;
                    // 判断是否登陆成功
                    boolean rg = false;
                    rg = sqls.register(ex,user.getUserId(), user.getPwd());
                    // 注册校验
                    if (rg == true && ex == false) {
                        message.setMesType(MessageType.MESSAGE_REGISTER_SUCCEED);
                    } else if(rg == false && ex == true){
                        message.setMesType(MessageType.MESSAGE_USERID_EXISTS);
                    } else {
                        message.setMesType(MessageType.MESSAGE_REGISTER_FAIL);
                    }
                    oos.writeObject(message);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (ss != null)
                ss.close();
        }
    }
}

ServerConnectClientThread(服务端消息转发)

该类负责维护与服务端的Socket连接,负责消息的收发。

public class ServerConnectClientThread extends Thread {
    private Socket socket;
    private ObjectInputStream ois;
    private ObjectOutputStream oos;
    private ServerMain serverMain;
    private String userId;

    public ServerConnectClientThread(Socket socket, ServerMain serverMain) {
        this.socket = socket;
        this.serverMain = serverMain;
        try {
            oos = new ObjectOutputStream(socket.getOutputStream());
            ois = new ObjectInputStream(socket.getInputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void run() {
        while (true) {
            try {
                Message msg = (Message) ois.readObject();
                switch (msg.getMsgType()) {
                    case "LOGIN":
                        // 校验用户,返回结果
                        break;
                    case "CHAT":
                        // 转发消息到目标用户
                        serverMain.sendToUser(msg.getReceiver(), msg);
                        break;
                    // ... 其他类型 ...
                }
            } catch (Exception e) {
                break;
            }
        }
    }

    public void sendMessage(Message msg) {
        try {
            oos.writeObject(msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

结语

本项目通过Java Socket实现了一个基础的聊天室系统,前端采用JavaFX和Swing实现用户界面,后端通过多线程处理客户端请求。前后端通过User和Message类的调用进行数据交互,所有操作均通过Socket对象流传递。希望本文能帮助大家理解Socket聊天室项目的前后端交互实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值