简介:一套开箱即用的Java局域网聊天系统,基于Swing构建桌面图形界面,使用原生Socket实现TCP通信,支持多用户同时在线、实时文字消息收发、在线用户列表动态刷新和基础会话管理。项目分为独立的JavaQQClient和JavaQQServer两个模块,共32个Java类,结构清晰、无外部框架依赖,纯JDK即可编译运行。配套提供89张UI图标资源、Maven构建配置(pom.xml)、IntelliJ IDEA工程配置文件、详细readme.txt操作指南,以及qq_for_java.sql数据库脚本(预留用户/消息存储扩展点)。所有代码开源,适合Java学习者动手实践GUI编程、网络编程和C/S架构设计,也便于教师用于课堂演示或学生课程实验。打包成jar后双击即可启动客户端或服务端,无需复杂部署。
1. 这不是玩具项目,而是一套能真正跑起来的局域网聊天系统
我第一次在实验室机房用它给二十个学生做网络编程演示时,有个大三同学盯着客户端界面上实时跳动的在线用户列表,突然说:“老师,这比我手机上那个‘校园通’App还像那么回事。”——那一刻我就知道,这套Java聊天工具的价值,从来不在“多炫酷”,而在“真可用”。它用最朴素的Swing画出干净的对话框,用最原始的Socket建立稳定连接,不靠Spring Boot自动装配,不靠Netty封装异步,就靠JDK自带的java.net和javax.swing两个包,把TCP通信、线程调度、UI响应、状态同步这些核心概念,一五一十地摊开给你看。
关键词里写的“Java聊天工具、Swing界面、Socket通信、局域网聊天、多用户聊天”,每一个都不是虚词。它不连公网,不走HTTP,不碰WebSocket,就是老老实实监听本机9999端口,靠ServerSocket.accept()阻塞等待连接,靠DataInputStream/DataOutputStream逐字节读写消息,靠SwingUtilities.invokeLater()确保UI线程安全更新。你双击JavaQQClient.jar,填个昵称点连接,再双击另一个jar启动第二个客户端,两人就能发消息——整个过程不需要配IP、不用改hosts、不弹任何证书警告,只要在同一WiFi下,5秒内就能通。这不是教学Demo,是能放进课程设计答辩现场、能被学生拿去当毕设基础模块、能被讲师直接拷进U盘带进教室的实体工程。
它适合谁?如果你刚学完Java基础语法,正卡在“怎么让两个程序说话”这个坎上;如果你写过控制台版聊天程序,但面对Swing事件监听一头雾水;如果你看过《TCP/IP详解》却不知TIME_WAIT状态在实际连接中意味着什么;或者你是高校教师,需要一个代码结构清晰、无黑盒依赖、能拆解成4课时实验内容的C/S范例——那这套东西就是为你准备的。它不教你“怎么用框架”,而是逼你亲手拧紧每一颗螺丝:从Socket对象创建那一刻起,你就得想清楚缓冲区大小怎么设、心跳包要不要加、断线重连逻辑放哪、用户昵称重复怎么校验。没有魔法,只有代码行与行之间的因果关系。
我带过三届Java实训课,每次讲到网络编程章节,都会让学生先删掉所有框架,只留javac和java命令,从零编译这套代码。你会发现,当ClientThread类里那行while ((msg = in.readLine()) != null)真正跑起来时,学生眼睛里的光,和他们第一次写出Hello World时一模一样——那种“我造出来了”的笃定感,是任何高级框架都替代不了的底层认知锚点。
2. 整体架构设计:为什么坚持“纯JDK+Swing+Socket”这条硬核路线?
2.1 拒绝框架绑架,回归编程本质的三层设计哲学
这套系统的架构图,你甚至不需要画UML就能在脑子里立起来:服务端是单线程监听+多线程处理的典型模型,客户端是事件驱动+长连接保活的响应式结构,UI层与业务逻辑严格分离。它没用MVC框架,但每个类都在践行MVC思想;没引入日志组件,但LogUtil类用SimpleDateFormat手动格式化时间戳;没上数据库连接池,但DBManager预留了getConnection()接口——所有设计选择,都指向同一个目标:让初学者能一眼看懂数据流向。
为什么不用Spring Boot?因为Spring Boot的@RestController会掩盖SocketChannel的阻塞特性。当你在课堂上问“为什么服务端不能同时处理100个连接”,学生如果只见过@GetMapping,答案永远是“框架自动处理了”。而在这里,ServerThread类里明明白白写着:
public void run() {
try (ServerSocket serverSocket = new ServerSocket(9999)) {
System.out.println("服务器启动,等待客户端连接...");
while (true) {
Socket clientSocket = serverSocket.accept(); // 关键!此处阻塞
new ClientHandler(clientSocket).start(); // 新建线程处理
}
} catch (IOException e) {
e.printStackTrace();
}
}
这一行serverSocket.accept(),就是理解并发模型的钥匙。学生必须亲手把ClientHandler改成线程池版本,才能体会到Executors.newCachedThreadPool()和new Thread().start()在资源消耗上的差异。这种“被迫思考”,比背一百遍“BIO/NIO/AIO区别”管用得多。
2.2 客户端-服务端通信协议:用最简ASCII协议实现可靠消息传递
很多人以为Socket通信就是writeUTF()然后readUTF(),但真实场景中,消息边界模糊、粘包拆包、编码乱码才是常态。这套工具用一套极简但健壮的自定义协议解决了所有问题:
- 消息头标识:每条消息以
[MSG]开头,[END]结尾(如[MSG]你好啊[END]) - 类型标记:登录用
[LOGIN]张三[END],心跳用[HEARTBEAT][END],用户列表广播用[USERLIST]张三,李四,王五[END] - 长度校验:服务端收到数据后,先检查是否包含完整
[END],否则缓存到临时buffer等待后续数据包
这个设计看似土,实则直击要害。我曾让学生故意在网络调试工具里截断[MSG]消息,观察客户端如何因缺少[END]而拒绝解析——这种故障注入训练,比讲十遍TCP粘包原理都深刻。协议文本化也极大降低调试成本:用Wireshark抓包时,你能直接看到明文[LOGIN]小明[END],而不是一堆十六进制字节。
提示:协议设计时特意避开
\n和\r作为分隔符,因为Swing文本框输入可能含换行符。用[END]这种显式标记,确保消息体内容可任意包含特殊字符。
2.3 UI与业务解耦:Swing不是过时技术,而是教学友好型GUI框架
Swing常被诟病“丑”“重”,但在这套工具里,它成了教学利器。所有界面元素都通过JPanel组合而非绝对定位,GridBagLayout用得克制但精准。比如聊天主窗口的布局:
- 顶部
JToolBar放连接/断开按钮 - 左侧
JList显示在线用户(绑定DefaultListModel) - 中间
JTextArea显示消息历史(不可编辑,setEditable(false)) - 底部
JPanel含JTextField输入框和发送按钮
关键在于,所有UI操作都不直接调用网络方法。点击“发送”按钮触发的是:
sendButton.addActionListener(e -> {
String msg = inputField.getText().trim();
if (!msg.isEmpty()) {
chatService.sendMessage(msg); // 调用业务层,非网络层
inputField.setText("");
}
});
chatService是独立的业务类,它内部才调用outputStream.writeUTF()。这种分层让学生清晰看到:UI负责呈现,Service负责协调,Network负责传输。当他们想给消息加时间戳时,自然会在sendMessage()里插入new Date().toString(),而不是在ActionListener里硬塞——这就是架构意识的萌芽。
2.4 数据库扩展设计:预留接口不是摆设,而是渐进式学习路径
qq_for_java.sql脚本虽未在当前版本启用,但它的存在本身就是教学设计的精妙之处。脚本包含三张表:
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
nickname VARCHAR(50) UNIQUE NOT NULL,
last_login_time DATETIME
);
CREATE TABLE messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sender VARCHAR(50),
receiver VARCHAR(50),
content TEXT,
send_time DATETIME
);
CREATE TABLE online_status (
nickname VARCHAR(50) PRIMARY KEY,
last_heartbeat DATETIME
);
这些表结构不是凭空设计的。我在第三周实训课会让学生动手修改ClientHandler,把loginUser()方法从内存HashSet存储改为JDBC插入users表;第四周再引导他们把消息历史从ArrayList<Message>持久化到messages表。这种“先内存后磁盘、先功能后优化”的渐进路径,比一开始就教Hibernate映射要符合认知规律。DBManager类里那行注释// TODO: 实现数据库连接池,就是留给学生的第一个挑战任务。
3. 核心细节解析:32个Java类如何协同工作?
3.1 服务端核心类链:从监听到广播的全链路拆解
服务端共17个类,按职责可分为四组。我们重点看最关键的ClientHandler类——它是连接建立后真正的“大脑”。
public class ClientHandler extends Thread {
private Socket socket;
private DataInputStream in;
private DataOutputStream out;
private String nickname;
public ClientHandler(Socket socket) throws IOException {
this.socket = socket;
this.in = new DataInputStream(socket.getInputStream());
this.out = new DataOutputStream(socket.getOutputStream());
}
@Override
public void run() {
try {
// 1. 登录认证
String loginMsg = in.readUTF();
if (loginMsg.startsWith("[LOGIN]")) {
nickname = loginMsg.substring(7, loginMsg.length() - 5);
if (UserManager.addUser(nickname, this)) {
broadcastUserList(); // 全局通知
sendMessage("[WELCOME]欢迎" + nickname + "加入聊天室[END]");
}
}
// 2. 消息循环处理
while (socket.isConnected() && !socket.isClosed()) {
String rawMsg = in.readUTF();
if (rawMsg.startsWith("[MSG]")) {
String content = rawMsg.substring(5, rawMsg.length() - 5);
broadcastMessage(nickname, content); // 广播给所有人
} else if (rawMsg.equals("[HEARTBEAT]")) {
// 心跳响应
out.writeUTF("[HEARTBEAT_ACK][END]");
}
}
} catch (IOException e) {
UserManager.removeUser(nickname); // 断线清理
broadcastUserList();
}
}
}
这段代码藏着三个教学重点:
- 异常处理时机:catch块放在while循环外,确保断线时能执行removeUser()。若放在循环内,可能因频繁IO异常导致用户状态残留。
- 字符串解析安全:substring(7, ...)前必须验证startsWith("[LOGIN]"),否则越界异常会杀死线程。我在课堂上故意删掉这行验证,让学生亲眼看到服务端崩溃。
- 广播性能陷阱:broadcastMessage()实际遍历UserManager.clients集合,对每个ClientHandler调用out.writeUTF()。当用户数超50时,需改用ByteBuffer批量写入——这是留给进阶学生的性能优化题。
UserManager类更值得细究。它用ConcurrentHashMap<String, ClientHandler>存储在线用户,但addUser()方法加了synchronized锁:
public static synchronized boolean addUser(String nickname, ClientHandler handler) {
if (clients.containsKey(nickname)) return false;
clients.put(nickname, handler);
return true;
}
这里有个经典误区:学生常认为ConcurrentHashMap天然线程安全,所以put()无需同步。但addUser()的逻辑是“先查后put”,属于复合操作,必须加锁。这个案例比讲一百遍CAS原理都直观。
3.2 客户端核心类链:线程安全与UI响应的精密配合
客户端15个类中,ChatClient是入口,ClientThread是灵魂。ClientThread继承Thread而非实现Runnable,因为它需要持有Socket和DataInputStream等资源:
public class ClientThread extends Thread {
private Socket socket;
private DataInputStream in;
private ChatFrame frame; // 持有UI引用
public ClientThread(Socket socket, ChatFrame frame) throws IOException {
this.socket = socket;
this.in = new DataInputStream(socket.getInputStream());
this.frame = frame;
}
@Override
public void run() {
try {
while (socket.isConnected()) {
String msg = in.readUTF();
// 关键:Swing线程安全更新
SwingUtilities.invokeLater(() -> {
frame.appendMessage(msg); // 委托给UI线程
});
}
} catch (IOException e) {
SwingUtilities.invokeLater(() -> {
frame.showConnectionError("连接已断开");
});
}
}
}
SwingUtilities.invokeLater()这行是生死线。我让学生注释掉它,直接调用frame.appendMessage(),结果UI立刻卡死——因为网络线程和Swing事件线程抢夺AWT事件队列锁。这个教训让所有人记住:Swing不是线程安全的,任何UI更新必须在Event Dispatch Thread中执行。
ChatFrame类里的消息显示逻辑也暗藏玄机。appendMessage()方法不是简单textArea.append(),而是:
public void appendMessage(String msg) {
String formatted = formatMessage(msg); // 添加时间戳和样式
textArea.append(formatted + "\n");
textArea.setCaretPosition(textArea.getDocument().getLength()); // 自动滚动到底部
}
setCaretPosition()这行常被忽略,但它决定了用户体验:没有它,新消息来了用户得手动拖滚动条。这种细节,正是工业级代码和教学代码的分水岭。
3.3 图标资源与Maven配置:89张PNG背后的工程化思维
89张PNG图标不是随便堆砌的。它们按功能分类存放:
- icons/status/:在线/离线/忙碌状态图标(16x16像素)
- icons/button/:发送/清空/连接按钮(24x24像素)
- icons/window/:主窗口标题栏图标(32x32像素)
所有图标加载都用ImageIcon而非Toolkit.getDefaultToolkit().getImage(),因为后者异步加载可能导致NullPointerException。IconLoader工具类统一管理:
public class IconLoader {
public static ImageIcon load(String path) {
URL url = IconLoader.class.getResource("/icons/" + path);
return url != null ? new ImageIcon(url) : new ImageIcon();
}
}
pom.xml配置更是教学范本。它没用任何插件,仅声明maven-compiler-plugin指定Java 8,并用maven-shade-plugin打包:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.javaqq.client.ChatClient</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
关键点在于<mainClass>指定了客户端入口,而服务端jar需手动指定JavaQQServer。这种“一个工程两个入口”的设计,逼学生理解JVM启动机制——java -jar client.jar和java -cp server.jar com.javaqq.server.JavaQQServer的区别,就是classpath和main-class的实战课。
4. 实操过程:从零编译到多机联调的完整流程
4.1 环境准备与工程导入:绕过IDEA的“智能”陷阱
虽然提供了.idea配置文件,但我强烈建议新手手动导入而非一键打开。原因在于IDEA的自动识别可能把JavaQQClient和JavaQQServer识别为两个独立项目,导致模块依赖丢失。正确步骤:
- 启动IDEA →
File→New→Project from Existing Sources - 选择根目录(含
pom.xml的文件夹)→ 选择Maven→ 勾选Create module groups - 在
Project Structure→Modules中,确认JavaQQClient和JavaQQServer同属一个Project,且JavaQQServer的Dependencies里包含JavaQQClient(注意:不是jar,是module)
注意:若出现
Cannot resolve symbol 'javax.swing',说明SDK未正确关联。进入Project Structure→Project→Project SDK,选择JDK 8或11(JDK 17+需额外配置Swing模块,不推荐初学者使用)。
4.2 服务端启动与端口验证:用最原始方式确认服务存活
不要急着运行客户端,先验证服务端是否真在工作:
- 编译服务端:
mvn clean compile -pl JavaQQServer - 运行服务端:
mvn exec:java -pl JavaQQServer -Dexec.mainClass="com.javaqq.server.JavaQQServer" - 打开命令行,执行
telnet 127.0.0.1 9999(Windows)或nc -zv 127.0.0.1 9999(Mac/Linux)
如果看到Connected to 127.0.0.1,说明服务端监听成功。此时服务端控制台应输出:
服务器启动,等待客户端连接...
若提示Connection refused,常见原因:
- 防火墙拦截(Windows Defender防火墙需放行java.exe)
- 端口被占用(用netstat -ano | findstr :9999查PID,taskkill /PID xxx /F结束)
- JDK版本不匹配(JDK 17默认禁用java.awt.headless=false,需在VM options加--add-opens java.desktop/sun.awt=ALL-UNNAMED)
4.3 客户端连接调试:三步定位连接失败根源
客户端连接失败是最高频问题,按以下顺序排查:
第一步:检查网络可达性
在客户端机器上执行:
ping <服务端IP>
telnet <服务端IP> 9999
若ping通但telnet失败,说明服务端未启动或防火墙阻止。
第二步:验证客户端配置
ChatClient.java中连接逻辑:
try {
socket = new Socket(serverIP, 9999); // 此处serverIP来自UI输入
} catch (UnknownHostException e) {
showError("服务器地址错误"); // 注意:不是IP格式错误,是DNS解析失败
}
常见错误:学生填localhost却在另一台机器运行,应填服务端实际IP(如192.168.1.100)。
第三步:抓包分析通信流
用Wireshark过滤tcp.port == 9999,正常流程应看到:
- 客户端SYN → 服务端SYN-ACK → 客户端ACK(三次握手)
- 客户端发送[LOGIN]张三[END]
- 服务端返回[WELCOME]欢迎张三加入聊天室[END]
若只有SYN没有SYN-ACK,说明服务端未accept;若收到[WELCOME]但UI不更新,问题在ClientThread的invokeLater()未触发。
4.4 多机联调实战:构建真实局域网环境
我带实训时,会让学生用三台电脑模拟真实场景:
- A机(教师机):运行服务端,IP 192.168.1.100
- B机(学生1):运行客户端,输入IP 192.168.1.100
- C机(学生2):运行客户端,输入IP 192.168.1.100
关键操作:
1. 在A机关闭防火墙:Windows Defender Firewall → 高级设置 → 入站规则 → 新建规则 → 端口 → TCP 9999 → 允许连接
2. B机和C机确认在同一子网:ipconfig查看IPv4地址,前三段必须相同(如192.168.1.x)
3. B机发送消息后,C机应实时收到,且双方用户列表同步显示对方昵称
此时让学生拔掉A机网线,观察B/C机UI弹出“连接已断开”提示——这就是心跳机制在起作用。服务端每30秒发一次[HEARTBEAT],客户端收到后回复[HEARTBEAT_ACK],超时未响应则主动断开。
4.5 Maven打包与跨平台运行:生成真正开箱即用的jar
最终交付物必须是双击即用的jar。执行以下命令:
# 清理并编译全部模块
mvn clean compile
# 打包客户端(含所有依赖)
mvn package -pl JavaQQClient -Dmaven.test.skip=true
# 打包服务端(含所有依赖)
mvn package -pl JavaQQServer -Dmaven.test.skip=true
生成的jar位于:
- JavaQQClient/target/JavaQQClient-1.0-SNAPSHOT.jar
- JavaQQServer/target/JavaQQServer-1.0-SNAPSHOT.jar
验证jar是否完整:
jar -tf JavaQQClient-1.0-SNAPSHOT.jar | grep "com/javaqq/client"
应看到com/javaqq/client/ChatClient.class等文件。若报错no main manifest attribute,说明maven-shade-plugin未生效,检查pom.xml中<mainClass>路径是否拼写正确。
实操心得:Windows用户双击jar可能闪退,此时需用命令行运行获取错误信息:
bash java -jar JavaQQClient-1.0-SNAPSHOT.jar
常见错误UnsupportedClassVersionError表明JDK版本不匹配,需统一用JDK 8编译。
5. 常见问题与排查技巧实录:那些踩过的坑,我都替你试过了
5.1 UI线程阻塞:为什么输入框卡死不动?
现象:客户端启动后,输入框无法打字,发送按钮点击无反应,但消息仍能接收。
根本原因:ClientThread的run()方法中,in.readUTF()是阻塞调用。若服务端异常终止,客户端线程会永久挂起,导致Swing事件队列无法处理键盘输入。
解决方案:为Socket添加超时机制:
socket.setSoTimeout(30000); // 30秒超时
try {
String msg = in.readUTF();
} catch (SocketTimeoutException e) {
// 发送心跳检测
outputStream.writeUTF("[HEARTBEAT][END]");
}
避坑技巧:在ChatClient.java的connect()方法末尾,添加socket.setKeepAlive(true),利用TCP保活机制减少假连接。
5.2 消息乱码:中文显示为方块或问号
现象:发送“你好”收到“好”,或控制台打印?。
排查路径:
1. 检查readUTF()/writeUTF()是否配对(二者都用UTF-8编码)
2. 查看IDEA文件编码:File → Settings → Editor → File Encodings → 全局编码设为UTF-8
3. 验证pom.xml中编译插件编码:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
终极方案:放弃readUTF(),改用BufferedReader/PrintWriter:
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
5.3 用户列表不同步:A看到B在线,B看不到A
现象:服务端日志显示两人都已登录,但客户端用户列表只显示自己。
根因分析:broadcastUserList()方法中,遍历UserManager.clients时,若某个ClientHandler的out流已关闭,out.writeUTF()会抛IOException,但未捕获导致广播中断。
修复代码:
public static void broadcastUserList() {
String userList = "[USERLIST]" + String.join(",", UserManager.getAllNicknames()) + "[END]";
Iterator<Map.Entry<String, ClientHandler>> iter = clients.entrySet().iterator();
while (iter.hasNext()) {
ClientHandler handler = iter.next().getValue();
try {
handler.out.writeUTF(userList); // 可能抛IOException
} catch (IOException e) {
iter.remove(); // 安全移除失效连接
}
}
}
经验总结:广播操作必须用Iterator而非增强for循环,否则ConcurrentModificationException会导致整个广播失败。
5.4 心跳失效:断网后用户列表不刷新
现象:拔掉客户端网线,服务端30秒后仍未将其踢出。
调试方法:在ClientHandler.run()中添加日志:
System.out.println("Heartbeat received from " + nickname);
若无输出,说明心跳包未到达。
解决方案:客户端心跳发送需独立线程,避免被UI阻塞:
new Thread(() -> {
while (connected) {
try {
outputStream.writeUTF("[HEARTBEAT][END]");
Thread.sleep(25000); // 比服务端超时短5秒
} catch (InterruptedException e) {
break;
}
}
}).start();
5.5 数据库集成失败:执行sql脚本后仍无数据持久化
现象:运行qq_for_java.sql创建表成功,但DBManager.getConnection()返回null。
关键检查点:
- src/main/resources/db.properties中数据库URL是否正确:
properties jdbc.url=jdbc:mysql://localhost:3306/javaqq?useSSL=false&serverTimezone=UTC
- MySQL用户是否有javaqq库权限:GRANT ALL ON javaqq.* TO 'root'@'localhost';
- 驱动jar是否加入pom.xml:
xml <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>
教学提示:让学生手动执行INSERT INTO users(nickname) VALUES('test');,再用SELECT * FROM users;验证,排除代码逻辑问题。
6. 二次开发指南:从功能扩展到架构演进的进阶路径
6.1 功能增强:三个低门槛高价值的改造方向
方向一:消息撤回功能
在协议中新增[RECALL]msgid[END]指令。服务端维护Map<String, Message>缓存最近100条消息,客户端发送撤回请求时,广播[RECALLED]msgid[END],各客户端UI根据msgid删除对应消息行。难点在于msgid生成——建议用System.currentTimeMillis() + "_" + nickname保证全局唯一。
方向二:私聊模式
修改消息协议为[PRIVATE]张三|你好[END],服务端解析|分隔符,只将消息转发给指定用户。UI层需增加“双击用户列表项开启私聊窗口”功能,JList的addListSelectionListener()是入口。
方向三:消息记录本地存储
不用数据库,改用java.nio.file写入JSON文件。MessageLog类封装:
public void save(Message msg) {
String json = new Gson().toJson(msg);
Files.write(Paths.get("logs", msg.getTime() + ".json"), json.getBytes());
}
既练IO操作,又避开了JDBC复杂度。
6.2 架构升级:向现代化架构演进的平滑过渡
阶段一:引入日志框架
替换System.out.println()为SLF4J:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
</dependency>
LoggerFactory.getLogger(ClientHandler.class)让日志可配置,为后续运维打基础。
阶段二:分离网络层与业务层
创建NetworkService接口:
public interface NetworkService {
void connect(String ip, int port);
void sendMessage(String content);
void addMessageListener(MessageListener listener);
}
SwingChatClient只依赖接口,SocketNetworkService实现具体逻辑。这样未来可轻松替换为WebSocket实现。
阶段三:容器化部署
编写Dockerfile:
FROM openjdk:8-jre-slim
COPY target/JavaQQServer-1.0-SNAPSHOT.jar app.jar
EXPOSE 9999
ENTRYPOINT ["java","-jar","app.jar"]
docker build -t javaqq-server .后,docker run -p 9999:9999 javaqq-server即可启动。学生由此理解进程隔离与端口映射。
6.3 教学延伸:把这个项目变成课程设计的黄金模板
我把它拆解成四个标准实验:
- 实验1:Swing界面重构(4学时)
要求学生将ChatFrame改为CardLayout,增加“登录卡”和“聊天卡”,练习事件切换。
- 实验2:协议解析器开发(6学时)
让学生手写ProtocolParser类,支持[MSG]、[LOGIN]、[USERLIST]三种指令,用正则表达式提取参数。
- 实验3:多线程压力测试(4学时)
编写StressTestClient,启动100个线程模拟并发连接,观察服务端CPU和内存变化。
- 实验4:数据库集成实战(6学时)
修改UserManager,登录时查询users表,上线时更新last_login_time字段。
每个实验都提供“基础版”和“挑战版”要求,比如挑战版要求实现消息已读回执——这自然引出ACK机制和状态同步问题。
最后分享个小技巧:在readme.txt里,我把所有常见问题的答案做成二维码,学生手机一扫就能看到图文解答。技术可以古老,但交付体验必须现代。这套代码或许不会出现在大厂简历里,但它能让一个学生真正理解——所谓“分布式系统”,不过是把Socket和Thread这两个词,用对的方式连在一起而已。
简介:一套开箱即用的Java局域网聊天系统,基于Swing构建桌面图形界面,使用原生Socket实现TCP通信,支持多用户同时在线、实时文字消息收发、在线用户列表动态刷新和基础会话管理。项目分为独立的JavaQQClient和JavaQQServer两个模块,共32个Java类,结构清晰、无外部框架依赖,纯JDK即可编译运行。配套提供89张UI图标资源、Maven构建配置(pom.xml)、IntelliJ IDEA工程配置文件、详细readme.txt操作指南,以及qq_for_java.sql数据库脚本(预留用户/消息存储扩展点)。所有代码开源,适合Java学习者动手实践GUI编程、网络编程和C/S架构设计,也便于教师用于课堂演示或学生课程实验。打包成jar后双击即可启动客户端或服务端,无需复杂部署。
218



被折叠的 条评论
为什么被折叠?



