QO聊天室
QO聊天室——C/S架构
一、项目简介
QO聊天室是一款基于 Java Swing(客户端 GUI)与 Socket 网络编程实现的即时通讯系统,采用经典的客户端-服务器(C/S)架构,旨在满足多人实时通信的核心需求。系统支持用户注册与登录、好友管理、实时文本聊天以及跨终端文件传输等关键功能。客户端通过图形化界面提供直观友好的交互体验,服务器端则负责消息路由、用户状态维护及全量数据的持久化存储,整体架构在功能性与易用性之间取得了良好平衡。
| 项目 | 内容 |
|---|---|
| 系统简介 | 基于 Java Swing + Socket 的 C/S 架构即时通讯系统,支持登录、好友管理、实时聊天、文件传输 |
| 参考资料 | 1. Java Swing 官方文档:https://docs.oracle.com/javase 2. Java Socket 编程指南:https://www.oracle.com/java/ 3. MySQL JDBC 教程:https://dev.mysql.com/doc/connector-j/8.0/en/ |
二、系统功能简介
2.1 系统功能列表及概述
| 功能 | 概述 |
|---|---|
| 客户端 UI 与交互逻辑 | 实现登录/注册界面、好友列表展示、聊天窗口交互、消息输入与实时显示、好友增删等可视化操作 |
| 客户端网络通信 | 与服务器建立长连接,封装并发送用户指令,监听并解析服务器响应 |
| 服务器端逻辑 | 完成用户身份认证、好友关系管理、消息存储与转发、用户在线状态实时维护 |
| 数据持久化 | 基于 MySQL 实现用户信息、好友关系、聊天记录的持久化存储与高效查询 |
2.2 功能结构图
基于 Socket 的聊天室系统分为前端(客户端)与后端(服务器端)两大模块:
- 前端:核心为 GUI 界面层,包含用户界面模块(登录/聊天/好友列表等视图)与交互控制模块(响应用户操作、更新界面状态);
- 后端:核心为网络与数据层,包含网络监听模块(监听客户端连接、管理 Socket 通道)与业务处理模块(用户认证、消息转发、数据库交互)
聊天室系统功能结构图

2.3 包结构图

三、个人任务简述
3.1 负责的任务与功能
| 序号 | 完成功能与任务 | 描述 |
|---|---|---|
| 1 | 网络通信与消息处理 | 实现客户端与服务器 Socket 长连接、自定义协议解析、消息实时收发与界面刷新 |
| 2 | 用户状态与会话管理 | 设计当前聊天对象切换机制,实现历史消息加载与聊天界面同步更新 |
| 3 | 好友管理与会话切换 | 完成好友列表动态加载、好友增删、聊天会话切换及历史消息拉取 |
| 4 | 文件传输功能 | 实现文件选择、Socket 流传输、文件下载与本地保存功能 |
四、个人负责功能详解
4.1 网络通信与消息处理
核心实现客户端与服务器的 Socket 长连接通信逻辑,通过自定义协议前缀(如 MSG: 表示文本消息、FRIEND_LIST: 表示好友列表、FILE: 表示文件传输)区分指令类型,保障数据传输的规范性与可解析性。
关键实现要点:
- 启动独立的消息监听线程,以阻塞方式读取服务器输入流,确保消息的实时接收;
- 解析服务器返回的不同类型响应,分发至对应业务逻辑(如更新好友列表、渲染聊天消息);
- 封装用户操作指令(登录、注册、添加好友、发送消息等),通过 Socket 输出流可靠发送至服务器。
ServerHandler.java
package chat.service;import chat.dao.UserInfoDaoImpl;
import chat.entity.Friend;
import chat.entity.User;import java.io.*;
import java.net.Socket;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class ServerHandler implements Runnable {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
public static final Map<String,Socket> ONLINE_USER_MAP = new ConcurrentHashMap<>();
private String currentLoginUser = null;
private UserInfoDaoImpl UserInfoDaoImpl=new UserInfoDaoImpl();
private User user=new User();public ServerHandler(Socket clientSocket) { this.clientSocket = clientSocket; } @Override public void run() { try { in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); out = new PrintWriter(clientSocket.getOutputStream(), true); String inputLine; while ((inputLine = in.readLine()) != null) { System.out.println("收到指令: " + inputLine); processCommand(inputLine); } } catch (IOException e) { System.out.println("连接异常断开: " + e.getMessage()); } finally { // 用户下线处理 if (currentLoginUser != null) { ONLINE_USER_MAP.remove(currentLoginUser); System.out.println("用户 " + currentLoginUser + " 已下线"); } try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } public void processCommand(String inputLine) { // 分割指令 String[] parts = inputLine.split(" @####@ "); if (parts.length == 0) return; boolean result = false; String friendName; try { int commandType = Integer.parseInt(parts[0]); switch (commandType) { case 1://保存数据 result = UserInfoDaoImpl.saveMessage(parts); if (result) { sendResponse("成功发送信息"); } break; case 2://添加朋友 user.setUsername(parts[1]); friendName = parts[2]; String passwordUser = parts[3]; result = UserInfoDaoImpl.addFriend(user.getUsername(), friendName, passwordUser); if (result) { Socket receiverSocket = ONLINE_USER_MAP.get(parts[2]); if (ONLINE_USER_MAP.containsKey(parts[2])) { PrintWriter receiverOut = new PrintWriter(receiverSocket.getOutputStream(), true); receiverOut.println("CMD_SUCCESS:成功添加朋友"+parts[1]); receiverOut.flush(); } sendResponse("CMD_SUCCESS:成功添加朋友"+parts[2]); } break; case 3://删除朋友 user.setUsername(parts[1]); friendName = parts[2]; result = UserInfoDaoImpl.deleteFriend(friendName, user.getUsername()); if (result) { Socket receiverSocket = ONLINE_USER_MAP.get(parts[2]); if (ONLINE_USER_MAP.containsKey(parts[2])) { PrintWriter receiverOut = new PrintWriter(receiverSocket.getOutputStream(), true); receiverOut.println("CMD_SUCCESS:成功删除"+parts[1]); receiverOut.flush(); } sendResponse("CMD_SUCCESS:成功删除"+parts[2]); } break; case 4://注册 user.setUsername(parts[1]); user.setPassword(parts[2]); user.setPasswordUser(parts[3]); try { result = UserInfoDaoImpl.register(user); out.println("成功注册"); } catch (RuntimeException e) { if (e.getMessage().contains("用户名已存在")) { out.println("注册失败:用户名已存在"); } else { out.println("注册失败:系统异常"); } } break; case 5: // 登录 boolean success = UserInfoDaoImpl.login(parts[1], parts[2], parts[3]); if (success) { this.currentLoginUser =parts[1]; ONLINE_USER_MAP.put(parts[1], clientSocket); sendResponse("登录成功"); } else { out.println("登录失败"); } break; case 6://获取朋友列表 List<String> friendList = UserInfoDaoImpl.getFriends(parts[1]); for (String friend : friendList) { out.println("FRIEND_LIST:"+friend); } out.println("CMD_END_LIST"); break; case 7: // 发送消息 String sender = parts[1]; String receiver = parts[2]; String msg = parts[3]; boolean saved = UserInfoDaoImpl.saveMessage(parts); if (saved) { Socket receiverSocket = ONLINE_USER_MAP.get(receiver); if (ONLINE_USER_MAP.containsKey(receiver)) { PrintWriter receiverOut = new PrintWriter(receiverSocket.getOutputStream(), true); receiverOut.println("MSG:" + sender + ": " + msg); receiverOut.flush(); sendResponse("CMD_SUCCESS:实时消息发送成功"); } else { sendResponse("CMD_SUCCESS:信息保存成功(对方离线)"); } } else { sendResponse("CMD_ERROR:消息保存失败"); } break; case 8: // 获取历史消息 List<Friend> historyList = UserInfoDaoImpl.sendMessage(parts[1], parts[2]); StringBuilder sb = new StringBuilder(); for (Friend msg1 : historyList) { // 发送格式:MSG_HISTORY:发送者:内容 (时间) sb.append("MSG_HISTORY:").append(msg1.getSendname()).append(":").append(msg1.getMessage()).append(" (").append(msg1.getSendtime()).append(")"); sendResponse(sb.toString()); sb.setLength(0); } sendResponse("CMD_END_HISTORY"); break; case 9: // 退出 UserInfoDaoImpl.logout(parts[1]); ONLINE_USER_MAP.remove(parts[1]); this.currentLoginUser = null; sendResponse("CMD_SUCCESS:退出成功"); break; case 11: File dir = new File("server_files"); if (!dir.exists()) dir.mkdirs(); File serverFile = new File(dir,parts[3]); InputStream socketIn = clientSocket.getInputStream(); FileOutputStream fileOut = new FileOutputStream(serverFile); byte[] buffer = new byte[1024]; long totalRead = 0; int len; while (totalRead < Long.parseLong(parts[4])) { len = socketIn.read(buffer); if (len == -1) break; fileOut.write(buffer, 0, len); totalRead += len; } fileOut.close(); boolean saved2 = UserInfoDaoImpl.saveMessage(parts); if (saved2) { Socket receiverSocket = ONLINE_USER_MAP.get(parts[2]); if (ONLINE_USER_MAP.containsKey(parts[2])) { PrintWriter receiverOut = new PrintWriter(receiverSocket.getOutputStream(), true); receiverOut.println("FILE_MSG:" + parts[1] + ":" + parts[3]); receiverOut.flush(); } sendResponse("FILE_MSG:文件上传成功"); } break; case 12: boolean saved3 = UserInfoDaoImpl.Filesearch(parts[2], parts[1], parts[3]); if(saved3) {// 假设 receiverOut 是 PrintWriter if (clientSocket != null) { System.out.println("hello"); try { File serverFile1 = new File("server_files", parts[3]); FileInputStream fis = new FileInputStream(serverFile1); long realFileSize = serverFile1.length(); OutputStream socketOut = clientSocket.getOutputStream(); String header = "CMD_FILE_START:" + parts[3] + ":" + realFileSize + "\n"; socketOut.write(header.getBytes("UTF-8")); socketOut.flush(); byte[] buffer1 = new byte[4096]; // 使用更大的缓冲区 int len1; while ((len1 = fis.read(buffer1)) != -1) { System.out.println(len1); socketOut.write(buffer1, 0, len1); } fis.close(); socketOut.flush(); System.out.println("文件 " + parts[3] + " 发送完毕"); } catch (FileNotFoundException e) { PrintWriter errorOut = new PrintWriter(clientSocket.getOutputStream(), true); errorOut.println("CMD_ERROR:文件不存在"); errorOut.flush(); } catch (IOException e) { e.printStackTrace(); } } } break; default: sendResponse("CMD_ERROR:未知指令"); break; } } catch (Exception e) { sendResponse("CMD_ERROR:服务器处理异常: " + e.getMessage()); e.printStackTrace(); } } private void sendResponse(String msg) { if (currentLoginUser != null) { Socket userSocket = ONLINE_USER_MAP.get(currentLoginUser); if (userSocket != null) { try { PrintWriter userOut = new PrintWriter(userSocket.getOutputStream(), true); userOut.println(msg); userOut.flush(); } catch (IOException e) { throw new RuntimeException(e); } } } }
}
ChatFrame.java(数据接收)
private void startListenThread() { new Thread(() -> { try { String line; // 只要 socket 没有关闭,就一直读 while ((line =clientSocket.readMessage()) != null) { if (line.startsWith("FRIEND_LIST:")) { // 处理好友列表 String friendName = line.substring(12); // 去掉 "FRIEND_LIST:" if (!friendListModel.contains(friendName)) { friendListModel.addElement(friendName); user.addFriend(friendName); } }if (line.startsWith("MSG:")) { // 实时消息 String content = line.substring(4); appendMessage(content); } else if (line.startsWith("MSG_HISTORY:")) { // 历史消息 String content = line.substring(12); appendMessage(content); } else if (line.startsWith("CMD_SUCCESS:")) { // 服务器反馈成功 String msg = line.substring(12); if (msg.contains("成功添加朋友")) { String friendName1 = msg.substring(6); friendListModel.addElement(friendName1); user.addFriend(friendName1); JOptionPane.showMessageDialog(this, "添加好友成功"); } else if (msg.contains("成功删除")) { String friendName1 = msg.substring(4); friendListModel.removeElement(friendName1); user.removeFriend(friendName1); JOptionPane.showMessageDialog(this, "删除好友成功"); } } else if (line.startsWith("CMD_ERROR:")) { // 服务器反馈失败 String errorMsg = line.substring(10); JOptionPane.showMessageDialog(this, "错误: " + errorMsg); } else if (line.startsWith("FRIEND_LIST:")) { // 好友列表数据 String friendName = line.substring(12); // 简单去重 if (!friendListModel.contains(friendName)) { friendListModel.addElement(friendName); user.addFriend(friendName); } }else if (line.startsWith("FILE_MSG:")) { // 格式: FILE_MSG:林:photo.jpg String[] parts = line.split(":"); String senderName = parts[1]; String fileName = parts[2]; // 显示提示 appendMessage("系统: " + senderName + " 给你发了一个文件 [" + fileName + "]"); appendMessage("系统: 请输入文件名并点击【下载文件】按钮保存。"); } } } catch (Exception e) { // 连接断开时不打印堆栈,正常退出 } }).start();
}
4.2 用户状态与会话管理
设计轻量高效的会话切换机制,确保多好友聊天场景下界面与数据的一致性:
- 定义
currentChatTarget变量记录当前聊天好友 ID,作为会话唯一标识; - 切换好友时清空聊天窗口,并通过服务器指令拉取该好友的历史聊天记录进行渲染;
- 监听服务器推送的实时消息,仅当消息接收方为
currentChatTarget时才更新当前聊天界面,避免消息错位。
ChatFrame.java
package chat.Frame;import chat.client.ClientSocket;
import chat.entity.User;import javax.swing.;
import javax.swing.border.EmptyBorder;
import java.awt.;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;public class ChatFrame extends JFrame {
private User user;
private JTextArea taDisplay;
private JTextField tfInput;
private JButton btnSend;
ClientSocket clientSocket;
String newFriend = null;// 好友列表相关组件 private DefaultListModel<String> friendListModel; private JList<String> friendList; private String currentChatTarget; // 当前正在聊天的对象 private JLabel lblCurrent; public ChatFrame(User user,ClientSocket clientSocket) { this.user=user; this.clientSocket = clientSocket; friendListModel = new DefaultListModel<>(); startListenThread(); initUI(); } private void initUI() { setTitle("Chatroom - " + user.getUsername()); setSize(800, 600); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { logOut(); dispose(); } }); // 使用 JSplitPane 分割窗口 JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); splitPane.setDividerLocation(200); // 左侧宽度200 splitPane.setOneTouchExpandable(true); //左侧面板:好友列表 JPanel leftPanel = createLeftPanel(); // 右侧面板:聊天区域 JPanel rightPanel = createRightPanel(); splitPane.setLeftComponent(leftPanel); splitPane.setRightComponent(rightPanel); add(splitPane, BorderLayout.CENTER); // 欢迎语 taDisplay.append("系统: 欢迎你," + user.getUsername() + "!连接服务器成功\n"); taDisplay.append("系统: 请从左侧列表选择好友开始聊天。\n"); } private JPanel createLeftPanel() { JPanel leftPanel = new JPanel(new BorderLayout()); leftPanel.setBackground(new Color(240, 242, 245)); leftPanel.setBorder(new EmptyBorder(10, 10, 10, 0)); // 顶部标题 JLabel lblFriends = new JLabel("好友列表", SwingConstants.CENTER); lblFriends.setFont(new Font("微软雅黑", Font.BOLD, 16)); lblFriends.setBorder(new EmptyBorder(0, 0, 10, 0)); leftPanel.add(lblFriends, BorderLayout.NORTH); // 中间好友列表 loadFriendListFromResponse(); friendList = new JList<>(friendListModel); friendList.setFont(new Font("微软雅黑", Font.PLAIN, 14)); friendList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); friendList.setBorder(new EmptyBorder(5, 5, 5, 5)); JScrollPane scrollPane = new JScrollPane(friendList); scrollPane.setBorder(null); leftPanel.add(scrollPane, BorderLayout.CENTER); // 底部按钮区 JPanel btnPanel = new JPanel(new GridLayout(1, 2, 5, 5)); btnPanel.setBorder(new EmptyBorder(10, 0, 0, 0)); btnPanel.setBackground(new Color(240, 242, 245)); JButton btnAddFriend = createSmallButton("添加", new Color(52, 199, 89)); JButton btnDelFriend = createSmallButton("删除", new Color(255, 59, 48)); JButton logOut=createSmallButton("登出",new Color(244, 226, 21, 221)); btnPanel.add(btnAddFriend); btnPanel.add(btnDelFriend); btnPanel.add(logOut); leftPanel.add(btnPanel, BorderLayout.SOUTH); //左侧面板事件监听 // 点击好友切换聊天对象 friendList.addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) { // 防止触发两次 String selected = friendList.getSelectedValue(); switchChatTarget(selected); } }); // 添加好友 btnAddFriend.addActionListener(e -> addFriend()); // 删除好友 btnDelFriend.addActionListener(e -> deleteFriend()); //登出 logOut.addActionListener(e->logOut()); return leftPanel; } private JPanel createRightPanel() { JPanel rightPanel = new JPanel(new BorderLayout()); rightPanel.setBorder(new EmptyBorder(10, 0, 10, 10)); // 顶部当前聊天对象显示 lblCurrent = new JLabel("当前聊天: 未选择", SwingConstants.CENTER); lblCurrent.setFont(new Font("微软雅黑", Font.BOLD, 14)); lblCurrent.setBorder(new EmptyBorder(0, 0, 10, 0)); rightPanel.add(lblCurrent, BorderLayout.NORTH); // 消息显示区域 taDisplay = new JTextArea(); taDisplay.setEditable(false); taDisplay.setFont(new Font("微软雅黑", Font.PLAIN, 14)); taDisplay.setLineWrap(true); taDisplay.setWrapStyleWord(true); JScrollPane scrollPane = new JScrollPane(taDisplay); rightPanel.add(scrollPane, BorderLayout.CENTER); // 底部输入区域 JPanel bottomPanel = new JPanel(new BorderLayout(5, 5)); tfInput = new JTextField(); tfInput.setFont(new Font("微软雅黑", Font.PLAIN, 14)); tfInput.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(new Color(200, 200, 200)), BorderFactory.createEmptyBorder(8, 10, 8, 10) )); btnSend = new JButton("发送"); styleButton(btnSend, new Color(0, 122, 204)); btnSend.setPreferredSize(new Dimension(80, 35)); JButton btnSendFile=new JButton("发送文件"); styleButton(btnSendFile, new Color(106, 90, 205)); btnSendFile.setPreferredSize(new Dimension(100, 35)); JButton btnDownload = new JButton("下载文件"); styleButton(btnDownload, new Color(255, 140, 0)); // 橙色按钮 btnDownload.setPreferredSize(new Dimension(100, 35)); JPanel sendPanel=new JPanel(new FlowLayout(FlowLayout.RIGHT,5,0)); sendPanel.add(btnSend); sendPanel.add(btnSendFile); sendPanel.add(btnDownload); bottomPanel.add(tfInput, BorderLayout.CENTER); bottomPanel.add(sendPanel, BorderLayout.EAST); rightPanel.add(bottomPanel, BorderLayout.SOUTH); // 右侧面板事件监听 ActionListener sendAction = e -> sendMessage(); btnSend.addActionListener(sendAction); tfInput.addActionListener(sendAction); btnSendFile.addActionListener(e->sendFile()); btnDownload.addActionListener(e -> downloadFileFromServer()); return rightPanel; } //逻辑方法 private void startListenThread() { new Thread(() -> { try { String line; // 只要 socket 没有关闭,就一直读 while ((line =clientSocket.readMessage()) != null) { if (line.startsWith("FRIEND_LIST:")) { // 处理好友列表 String friendName = line.substring(12); // 去掉 "FRIEND_LIST:" if (!friendListModel.contains(friendName)) { friendListModel.addElement(friendName); user.addFriend(friendName); } } if (line.startsWith("MSG:")) { // 实时消息 String content = line.substring(4); appendMessage(content); } else if (line.startsWith("MSG_HISTORY:")) { // 历史消息 String content = line.substring(12); appendMessage(content); } else if (line.startsWith("CMD_SUCCESS:")) { // 服务器反馈成功 String msg = line.substring(12); if (msg.contains("成功添加朋友")) { String friendName1 = msg.substring(6); friendListModel.addElement(friendName1); user.addFriend(friendName1); JOptionPane.showMessageDialog(this, "添加好友成功"); } else if (msg.contains("成功删除")) { String friendName1 = msg.substring(4); friendListModel.removeElement(friendName1); user.removeFriend(friendName1); JOptionPane.showMessageDialog(this, "删除好友成功"); } } else if (line.startsWith("CMD_ERROR:")) { // 服务器反馈失败 String errorMsg = line.substring(10); JOptionPane.showMessageDialog(this, "错误: " + errorMsg); } else if (line.startsWith("FRIEND_LIST:")) { // 好友列表数据 String friendName = line.substring(12); // 简单去重 if (!friendListModel.contains(friendName)) { friendListModel.addElement(friendName); user.addFriend(friendName); } }else if (line.startsWith("FILE_MSG:")) { // 格式: FILE_MSG:林:photo.jpg String[] parts = line.split(":"); String senderName = parts[1]; String fileName = parts[2]; // 显示提示 appendMessage("系统: " + senderName + " 给你发了一个文件 [" + fileName + "]"); appendMessage("系统: 请输入文件名并点击【下载文件】按钮保存。"); } } } catch (Exception e) { // 连接断开时不打印堆栈,正常退出 } }).start(); } private void loadFriendListFromResponse() { if (clientSocket == null) { JOptionPane.showMessageDialog(this, "连接已断开,请重新登录!"); return; } String cmd = "6 @####@ " + user.getUsername(); clientSocket.sendCommand(cmd); } private void switchChatTarget(String target) { this.currentChatTarget = target; taDisplay.setText(""); // 清空显示区域 lblCurrent.setText("当前聊天:" + target); taDisplay.append("系统: 正在与 " + target + " 聊天\n"); String command = "8 @####@ " + user.getUsername() + " @####@ " + target; clientSocket.sendCommand(command); } private void sendMessage() { String message = tfInput.getText().trim(); if (message.isEmpty()) { JOptionPane.showMessageDialog(this, "请输入要发送的消息内容!"); return; } if (currentChatTarget == null || currentChatTarget.isEmpty()) { JOptionPane.showMessageDialog(this, "请先从左侧列表选择聊天好友!"); return; } String command = "7 @####@ " + user.getUsername() + " @####@ " + currentChatTarget + " @####@ " + message; clientSocket.sendCommand(command); appendMessage(user.getUsername()+": " + message); tfInput.setText(""); } private void sendFile() { if (currentChatTarget == null || currentChatTarget.isEmpty()) { JOptionPane.showMessageDialog(this, "请先从左侧列表选择要发送的好友!"); return; } JFileChooser fileChooser = new JFileChooser(); fileChooser.setDialogTitle("选择要发送的文件"); int result = fileChooser.showOpenDialog(this); if (result != JFileChooser.APPROVE_OPTION) return; File file = fileChooser.getSelectedFile(); if (!file.exists() || !file.isFile()) { JOptionPane.showMessageDialog(this, "无效的文件!"); return; } try { String fileName = file.getName(); long fileSize = file.length(); System.out.println(currentChatTarget); String header = "11 @####@ " + user.getUsername() + " @####@ " + currentChatTarget + " @####@ " + fileName+ " @####@ " +fileSize; clientSocket.sendCommand(header); // 发送文件内容 FileInputStream fis = new FileInputStream(file); OutputStream socketOut = clientSocket.getOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { socketOut.write(buffer, 0, len); } fis.close(); socketOut.flush(); appendMessage("系统: 文件 [" + fileName + "] 发送完成"); } catch (Exception e) { e.printStackTrace(); JOptionPane.showMessageDialog(this, "文件发送失败: " + e.getMessage()); } } private void addFriend() { JPanel panel = new JPanel(); panel.setLayout(new GridLayout(2, 2, 10, 10)); // 2行2列布局,行列间距10px,排版美观 panel.setBackground(new Color(240, 242, 245)); JTextField newFriendField = new JTextField(); // 好友账号输入框 JTextField newFriendPassField = new JTextField(); panel.add(new JLabel("请输入要添加的好友昵称名:")); panel.add(newFriendField); panel.add(new JLabel("请输入要添加的好友交友码:")); panel.add(newFriendPassField); int result = JOptionPane.showConfirmDialog(this, panel, "添加好友", JOptionPane.OK_CANCEL_OPTION); String newFriendPass = null; if (result != JOptionPane.OK_OPTION) { return; } newFriend = newFriendField.getText().trim(); // 获取好友账号 newFriendPass = newFriendPassField.getText().trim(); // 获取好友交友码(密文框正确取值) if (newFriend == null || newFriend.isEmpty() || newFriendPass == null || newFriendPass.isEmpty()) { JOptionPane.showMessageDialog(this, "好友账号和交友码不能为空!"); return; } if (newFriend.equals(user.getUsername())) { JOptionPane.showMessageDialog(this, "不能添加自己为好友!"); return; } if(user.getFriendList().contains(newFriend)) { JOptionPane.showMessageDialog(this, "该好友已在列表中!"); return; } String command="2 @####@ "+user.getUsername()+" @####@ "+newFriend+" @####@ "+newFriendPass; clientSocket.sendCommand(command); } private void deleteFriend() { String selected = friendList.getSelectedValue(); if (selected == null) { JOptionPane.showMessageDialog(this, "请先选择要删除的好友!"); return; } int confirm = JOptionPane.showConfirmDialog(this, "确定要删除好友 " + selected + " 吗?", "确认删除", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { String command="3 @####@ "+user.getUsername()+" @####@ "+selected; clientSocket.sendCommand(command); } } private void logOut() { int confirm=JOptionPane.showConfirmDialog( this, "确定要登出账号"+user.getUsername()+"吗?", "确认登出", JOptionPane.YES_NO_OPTION ); //用户取消登出 if (confirm!=JOptionPane.YES_OPTION) { return; } String command="9 @####@ "+user.getUsername(); clientSocket.sendCommand(command); clientSocket.closeConnection(); this.dispose(); SwingUtilities.invokeLater(() -> { new LoginFrame().setVisible(true); }); } private void appendMessage(String msg) { SwingUtilities.invokeLater(() -> { LocalDateTime now = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String timeStr = now.format(formatter); // 避免重复添加时间戳(如果服务器发来的带时间戳,这里就不加了) if (!msg.contains("(") && !msg.startsWith("系统")) { taDisplay.append(msg + " (" + timeStr + ")\n"); } else { taDisplay.append(msg + "\n"); } taDisplay.setCaretPosition(taDisplay.getDocument().getLength()); }); } private void downloadFileFromServer() { String fileName = JOptionPane.showInputDialog(this, "请输入聊天记录中显示的文件名:"); if (fileName == null || fileName.trim().isEmpty()) return; try { // 选择保存路径 JFileChooser chooser = new JFileChooser(); chooser.setDialogTitle("选择保存位置"); chooser.setSelectedFile(new File(fileName)); int userSelection = chooser.showSaveDialog(this); if (userSelection != JFileChooser.APPROVE_OPTION) { return; } File fileToSave = chooser.getSelectedFile(); System.out.println("准备保存到: " + fileToSave.getAbsolutePath()); ClientSocket clientSocket2 = new ClientSocket(); clientSocket2.start("172.19.76.83"); String cmd = "12 @####@ " +user.getUsername()+" @####@ "+currentChatTarget+" @####@ "+ fileName; clientSocket2.sendCommand(cmd); InputStream rawIn = clientSocket2.getInputStream(); ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream(); int b = -1; while ((b = rawIn.read()) != -1) { System.out.println("b="+b); if (b == '\n') break; // 遇到换行符,指令结束 lineBuffer.write(b); } System.out.println("out"); String startLine = lineBuffer.toString("UTF-8"); System.out.println("收到响应: " + startLine); if (!startLine.startsWith("CMD_FILE_START:")) { JOptionPane.showMessageDialog(this, "服务器响应错误"); return; } String[] parts = startLine.split(":"); long fileSize = Long.parseLong(parts[2]); System.out.println("准备接收文件,大小: " + fileSize); FileOutputStream fileOut = new FileOutputStream(fileToSave); byte[] buffer = new byte[4096]; long totalRead = 0; int len; while (totalRead < fileSize && (len = rawIn.read(buffer)) != -1) { // 防止多读(读到下一条指令的数据) if (totalRead + len > fileSize) { int need = (int)(fileSize - totalRead); fileOut.write(buffer, 0, need); totalRead += need; } else { fileOut.write(buffer, 0, len); totalRead += len; } } fileOut.close(); JOptionPane.showMessageDialog(this, "文件下载成功!"); appendMessage("系统: 文件已保存到 " + fileToSave.getAbsolutePath()); clientSocket2.closeConnection(); } catch (Exception e) { e.printStackTrace(); JOptionPane.showMessageDialog(this, "下载失败: " + e.getMessage()); } } private JButton createSmallButton(String text, Color color) { JButton btn = new JButton(text); btn.setBackground(color); btn.setForeground(Color.WHITE); btn.setFont(new Font("微软雅黑", Font.PLAIN, 12)); btn.setFocusPainted(false); btn.setBorderPainted(false); btn.setOpaque(true); btn.setCursor(new Cursor(Cursor.HAND_CURSOR)); return btn; } private void styleButton(JButton button, Color color) { button.setBackground(color); button.setForeground(Color.WHITE); button.setFont(new Font("微软雅黑", Font.BOLD, 14)); button.setFocusPainted(false); button.setBorderPainted(false); button.setOpaque(true); button.setCursor(new Cursor(Cursor.HAND_CURSOR)); }
}
4.3 文件传输功能
实现基于 Socket 字节流的跨终端文件传输,核心流程如下:
- 客户端选择本地文件,向服务器发送文件元信息(文件名、文件大小、接收方 ID);
- 服务器将元信息转发至接收方,接收方确认后,发送方通过专用 Socket 字节流传输文件内容;
- 接收方启动独立 I/O 线程读取文件流,选择本地路径完成文件保存,并可选支持下载进度可视化。
ServerHandler.java(文件传输)
case 11: File dir = new File("server_files"); if (!dir.exists()) dir.mkdirs(); File serverFile = new File(dir,parts[3]); InputStream socketIn = clientSocket.getInputStream(); FileOutputStream fileOut = new FileOutputStream(serverFile); byte[] buffer = new byte[1024]; long totalRead = 0; int len; while (totalRead < Long.parseLong(parts[4])) { len = socketIn.read(buffer); if (len == -1) break; fileOut.write(buffer, 0, len); totalRead += len; } fileOut.close(); boolean saved2 = UserInfoDaoImpl.saveMessage(parts); if (saved2) { Socket receiverSocket = ONLINE_USER_MAP.get(parts[2]); if (ONLINE_USER_MAP.containsKey(parts[2])) { PrintWriter receiverOut = new PrintWriter(receiverSocket.getOutputStream(), true); receiverOut.println("FILE_MSG:" + parts[1] + ":" + parts[3]); receiverOut.flush(); } sendResponse("FILE_MSG:文件上传成功"); } break; case 12: boolean saved3 = UserInfoDaoImpl.Filesearch(parts[2], parts[1], parts[3]); if(saved3) {// 假设 receiverOut 是 PrintWriter if (clientSocket != null) { System.out.println("hello"); try { File serverFile1 = new File("server_files", parts[3]); FileInputStream fis = new FileInputStream(serverFile1); long realFileSize = serverFile1.length();OutputStream socketOut = clientSocket.getOutputStream(); String header = "CMD_FILE_START:" + parts[3] + ":" + realFileSize + "\n"; socketOut.write(header.getBytes("UTF-8")); socketOut.flush(); byte[] buffer1 = new byte[4096]; // 使用更大的缓冲区 int len1; while ((len1 = fis.read(buffer1)) != -1) { System.out.println(len1); socketOut.write(buffer1, 0, len1); } fis.close(); socketOut.flush(); System.out.println("文件 " + parts[3] + " 发送完毕"); } catch (FileNotFoundException e) { PrintWriter errorOut = new PrintWriter(clientSocket.getOutputStream(), true); errorOut.println("CMD_ERROR:文件不存在"); errorOut.flush(); } catch (IOException e) { e.printStackTrace(); } } } break;
4.4 数据库设计与 JDBC 实现
(1)数据库结构
贴合业务需求设计用户表、好友关系表、聊天记录表等核心表结构。例如:
- 用户表:
username、password、passwordUser、 create_time 等; - 聊天记录表:
sender_name、friend_name、content、send_time等。 - 朋友关系表: username、friend_name、add_time 等
- 在线表:username 、user_ip
(2)JDBC 连接与配置优化
- 单例模式:通过静态属性与静态方法实现 JDBC 连接复用(或简易连接池),避免重复加载驱动与创建连接;
- 资源管理:全面采用
try-with-resources语法自动关闭Connection、Statement、ResultSet等资源,有效防止内存泄漏; - 异常处理:将 JDBC 的检查型异常封装为自定义运行时异常,简化上层业务逻辑的异常处理流程;
- 配置解耦:通过
JDBC.properties文件集中管理数据库连接参数(驱动类、URL、用户名、密码),提升系统可维护性与可移植性。
JdbcUtil.java
package chat.util;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import java.io.InputStream;public class JdbcUtil {
private static Properties props = new Properties();// 静态代码块加载配置 static { try (InputStream is = JdbcUtil.class.getClassLoader().getResourceAsStream("jdbc.properties")) { props.load(is); Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { throw new RuntimeException("JDBC配置加载失败", e); } } // 获取数据库连接 public static Connection getConnection() { try { return DriverManager.getConnection( props.getProperty("jdbc.url"), props.getProperty("jdbc.username"), props.getProperty("jdbc.password") ); } catch (SQLException e) { throw new RuntimeException("数据库连接失败", e); } } // 关闭资源 public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs != null) rs.close(); if (ps != null) ps.close(); if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } // 重载:关闭连接和PreparedStatement public static void close(Connection conn, PreparedStatement ps) { close(conn, ps, null); }
}
jdbc.properties
# ?????
jdbc.driver=com.mysql.cj.jdbc.Driver
# ?????????? ???? ??????????????3306???
jdbc.url=jdbc:mysql://localhost:3306/teststu?useSSL=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=Root@7317314.5 网络通讯核心设计
- C/S 架构:服务器端通过
ServerSocket监听指定端口,采用线程池管理客户端连接,避免单线程阻塞;客户端主动发起 Socket 连接,维持长连接以支撑实时通信; - 线程池优化:服务器端使用
ThreadPoolExecutor复用线程处理客户端请求,有效支持多用户并发在线,显著提升系统吞吐能力; - 长连接管理:借助
ConcurrentHashMap(键为用户 ID,值为对应 Socket 对象)安全地管理在线连接,用户登录时注册、退出或异常断开时移除,确保连接管理的线程安全性与一致性。
ChatServer .java
package chat.service;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class ChatServer {
private static final int PORT = 8080;
private ExecutorService threadPool;
private volatile boolean isRunning = true;public static void main(String[] args) { System.out.println("聊天服务器启动"); ChatServer server = new ChatServer(); server.start(); } public void start() { threadPool = Executors.newCachedThreadPool(); try (ServerSocket serverSocket = new ServerSocket(PORT)) { System.out.println("服务器已启动,监听端口: " + PORT); System.out.println("等待客户端连接..."); while (isRunning) { Socket clientSocket = serverSocket.accept(); System.out.println("检测到新客户端连接: " + clientSocket.getInetAddress().getHostAddress()); threadPool.submit(new ServerHandler(clientSocket)); } } catch (IOException e) { if (isRunning) { System.err.println("服务器异常: " + e.getMessage()); } else { System.out.println("服务器已停止。"); } } finally { shutdown(); } } //关闭服务器 public void shutdown() { isRunning = false; if (threadPool != null) { threadPool.shutdown(); // 停止接受新任务 try { // 等待现有任务完成(最多给5秒) if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { threadPool.shutdownNow(); } } catch (InterruptedException e) { threadPool.shutdownNow(); Thread.currentThread().interrupt(); } } System.out.println("服务器资源已释放。"); }
}
五、成果展示
登陆界面:

注册界面:

聊天界面:

文件下载界面:

六、总结及展望
6.1 总结
核心问题与解决方案
| 问题 | 解决方案 |
|---|---|
| Socket 长连接管理 | 基于 ConcurrentHashMap 实现在线 Socket 连接的线程安全管理,登录时添加、退出时移除 |
| 文件下载阻塞 | 为文件流读取创建独立 I/O 线程,与文本消息处理线程隔离,彻底避免界面卡顿 |
| 消息实时更新 | 单客户端独立监听线程 + EDT(事件分发线程)刷新 UI,结合全局 Socket 映射实现精准推送 |
功能亮点
- 高实时通信:采用 TCP 长连接 + 多线程并发监听架构,通过独立线程阻塞式监听输入流,结合 EDT 线程安全更新 UI,实现消息“近零延迟”收发;
- 高效文件传输:基于 C-S-C 中继转发模式,通过独立 Socket 通道与专用 I/O 线程处理文件传输,有效避免与文本消息竞争资源,保障传输稳定性与用户体验。
6.2 展望
在现有稳定通信基础上,未来可从安全性、跨平台性、性能与交互体验四个维度进行迭代升级:
- 安全增强:引入 SSL/TLS 加密通信链路,实现传输层加密;支持文件传输加密与断点续传,防止数据泄露或篡改;
- 跨平台适配:基于 JavaFX 重构客户端界面,拓展至 Android/iOS 移动端,实现多端无缝互通;
- 性能优化:针对高并发消息与大文件传输场景开展压力测试,优化线程池参数,引入消息队列(如 RabbitMQ)解耦核心逻辑,进一步提升系统响应速度与稳定性;
- 功能拓展:增加多级权限管理、超大型群聊(>100 人)、临时群组、私密聊天(阅后即焚)等高级功能,适配更复杂的社交与协作场景。
七、课程设计感想与问题
本次 QO 聊天室课程设计是一次理论与实践深度融合的宝贵经历。我主要负责客户端界面开发、网络通信逻辑、文件传输及会话管理模块,在技术攻坚与团队协作中收获颇丰:
(1)技术认知升级
深入掌握了 Java Swing GUI 开发、Socket 网络编程、多线程并发处理及 JDBC 数据库操作的核心原理。尤其对“长连接 vs 短连接”“线程隔离 vs 资源共享”等抽象概念有了具象化理解。初期曾因采用 HTTP 短连接模式导致实时通信失效,后通过重构为 TCP 长连接 + ConcurrentHashMap 管理在线连接,最终实现稳定可靠的消息推送,深刻体会到架构设计对系统功能的决定性影响。
(2)问题解决能力提升
在文件传输模块初期,因复用聊天线程导致 I/O 竞争与界面卡顿,通过“独立 Socket 连接 + 专用 I/O 线程”的隔离策略彻底解决;消息实时更新问题则通过“单客户端独立监听线程 + EDT 刷新 UI”的方案,兼顾了并发安全性与界面流畅性。这些问题的攻克,让我学会从“线程模型、资源分配、系统架构”多维度分析并解决复杂工程问题。
(3)团队协作成长
在与队友的分工协作中,掌握了模块化开发、接口规范定义、代码集成与需求对齐的关键技巧,深刻理解了“高内聚、低耦合”在团队项目中的重要性。未来将持续优化代码的健壮性、可读性与可维护性,并持续关注即时通讯领域的前沿技术(如 WebSocket、WebRTC、端到端加密等),为构建更安全、高效、智能的通信系统积累经验。
参考文献
- Java Swing 官方文档:https://docs.oracle.com/javase
- Java Socket 编程指南:https://www.oracle.com/java/
- MySQL JDBC 教程:https://dev.mysql.com/doc/connector-j/8.0/en/

浙公网安备 33010602011771号