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 通道)与业务处理模块(用户认证、消息转发、数据库交互)

聊天室系统功能结构图
0f93a091c132bb1edfa6abf06c2cb0e4_1768502450029-28dc44fd-4c16-46c3-99de-ee166fb6f508_x-oss-process=image%2Fformat%2Cwebp

2.3 包结构图

3b4bc27b344d6ee5a984595bb12157b6_1768522167737-5937a779-4536-438b-9654-dba2de849387_x-oss-process=image%2Fformat%2Cwebp

三、个人任务简述

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 字节流的跨终端文件传输,核心流程如下:

  1. 客户端选择本地文件,向服务器发送文件元信息(文件名、文件大小、接收方 ID);
  2. 服务器将元信息转发至接收方,接收方确认后,发送方通过专用 Socket 字节流传输文件内容;
  3. 接收方启动独立 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)数据库结构

贴合业务需求设计用户表、好友关系表、聊天记录表等核心表结构。例如:

  • 用户表usernamepasswordpasswordUser create_time 等;
  • 聊天记录表sender_namefriend_namecontentsend_time 等。
  • 朋友关系表: usernamefriend_nameadd_time
  • 在线表:usernameuser_ip

(2)JDBC 连接与配置优化

  • 单例模式:通过静态属性与静态方法实现 JDBC 连接复用(或简易连接池),避免重复加载驱动与创建连接;
  • 资源管理:全面采用 try-with-resources 语法自动关闭 ConnectionStatementResultSet 等资源,有效防止内存泄漏;
  • 异常处理:将 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@731731

4.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("服务器资源已释放。");
}
}

五、成果展示

登陆界面:

627b71017b2a4ed6b521973cc19a353c_1768526552052-9f5b0c60-e4c1-4a77-b1a0-40049e27261b_x-oss-process=image%2Fformat%2Cwebp

注册界面:

abbf8c6720d43dbc022a140510bd3242_1768526587107-2a7414da-2420-44cd-b4d1-814b072fdb6c_x-oss-process=image%2Fformat%2Cwebp

聊天界面:
32d232520cb3dadfd2f7e01dc808fde7_1768526624343-13323508-6ad3-4f4b-a8a2-13c50304e0e3_x-oss-process=image%2Fformat%2Cwebp

文件下载界面:
32d232520cb3dadfd2f7e01dc808fde7_1768526624343-13323508-6ad3-4f4b-a8a2-13c50304e0e3_x-oss-process=image%2Fformat%2Cwebp

六、总结及展望

6.1 总结

核心问题与解决方案

问题 解决方案
Socket 长连接管理 基于 ConcurrentHashMap 实现在线 Socket 连接的线程安全管理,登录时添加、退出时移除
文件下载阻塞 为文件流读取创建独立 I/O 线程,与文本消息处理线程隔离,彻底避免界面卡顿
消息实时更新 单客户端独立监听线程 + EDT(事件分发线程)刷新 UI,结合全局 Socket 映射实现精准推送

功能亮点

  1. 高实时通信:采用 TCP 长连接 + 多线程并发监听架构,通过独立线程阻塞式监听输入流,结合 EDT 线程安全更新 UI,实现消息“近零延迟”收发;
  2. 高效文件传输:基于 C-S-C 中继转发模式,通过独立 Socket 通道与专用 I/O 线程处理文件传输,有效避免与文本消息竞争资源,保障传输稳定性与用户体验。

6.2 展望

在现有稳定通信基础上,未来可从安全性、跨平台性、性能与交互体验四个维度进行迭代升级:

  1. 安全增强:引入 SSL/TLS 加密通信链路,实现传输层加密;支持文件传输加密与断点续传,防止数据泄露或篡改;
  2. 跨平台适配:基于 JavaFX 重构客户端界面,拓展至 Android/iOS 移动端,实现多端无缝互通;
  3. 性能优化:针对高并发消息与大文件传输场景开展压力测试,优化线程池参数,引入消息队列(如 RabbitMQ)解耦核心逻辑,进一步提升系统响应速度与稳定性;
  4. 功能拓展:增加多级权限管理、超大型群聊(>100 人)、临时群组、私密聊天(阅后即焚)等高级功能,适配更复杂的社交与协作场景。

七、课程设计感想与问题

本次 QO 聊天室课程设计是一次理论与实践深度融合的宝贵经历。我主要负责客户端界面开发、网络通信逻辑、文件传输及会话管理模块,在技术攻坚与团队协作中收获颇丰:

(1)技术认知升级

深入掌握了 Java Swing GUI 开发、Socket 网络编程、多线程并发处理及 JDBC 数据库操作的核心原理。尤其对“长连接 vs 短连接”“线程隔离 vs 资源共享”等抽象概念有了具象化理解。初期曾因采用 HTTP 短连接模式导致实时通信失效,后通过重构为 TCP 长连接 + ConcurrentHashMap 管理在线连接,最终实现稳定可靠的消息推送,深刻体会到架构设计对系统功能的决定性影响。

(2)问题解决能力提升

在文件传输模块初期,因复用聊天线程导致 I/O 竞争与界面卡顿,通过“独立 Socket 连接 + 专用 I/O 线程”的隔离策略彻底解决;消息实时更新问题则通过“单客户端独立监听线程 + EDT 刷新 UI”的方案,兼顾了并发安全性与界面流畅性。这些问题的攻克,让我学会从“线程模型、资源分配、系统架构”多维度分析并解决复杂工程问题。

(3)团队协作成长

在与队友的分工协作中,掌握了模块化开发、接口规范定义、代码集成与需求对齐的关键技巧,深刻理解了“高内聚、低耦合”在团队项目中的重要性。未来将持续优化代码的健壮性、可读性与可维护性,并持续关注即时通讯领域的前沿技术(如 WebSocket、WebRTC、端到端加密等),为构建更安全、高效、智能的通信系统积累经验。


参考文献

  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/
posted @ 2026-01-16 09:43  穗和  阅读(62)  评论(0)    收藏  举报