资讯动态

Java聊天室实战:Socket多线程+Swing+MySQL完整源码解析

发布时间:2026/10/15 8:48:37 来源:尧图企业网站定制
1. 项目概述与核心价值做 Java 开发的几乎没有人能绕开 Socket。课堂上讲网络编程老师会拿聊天室当例子面试聊并发也会问多线程通信想搞懂 GUI 程序怎么跟后端交互Swing 又是最常见的练习题。但你真去搜“聊天室源码”找到的要么只有单机伪聊天、要么界面粗糙到没法看、要么没有数据持久化重启数据就全没了。这篇博文我想拆解的是一个用了 Java Socket 多线程 Swing MySQL 数据库存储的聊天室项目带完整源码可直接复现。它不是一个玩具 Demo而是一套结构完整的教学级项目支持多客户端同时在线、服务端多线程并发处理、公屏消息广播、上下线通知、聊天记录入库。先说它能干什么。启动服务端后N 个客户端可以同时连接任意客户端发的消息会实时广播到所有人屏幕上。新用户上线会有系统提示用户关闭窗口离开也会被感知并通知其余人。同时每一条聊天消息和上下线事件都会写进 MySQL哪怕服务端重启、客户端退出后重进也能把历史记录捞出来看。这种“带数据库存储”的设计是把一个单纯的网络 demo 升级成接近真实项目的关键一步。再说适合谁来读。如果你正在学 Java 网络编程想把 TCP 的 accept、read、write 从“能跑通”变成“懂设计”这篇适合你如果你要做课设或者毕设需要一套功能完整、可演示、能交差的聊天室系统这篇更适合你如果你工作中被问到“多线程怎么处理并发连接”“JDBC 怎么存消息”看完这篇文章你能拿出一个真正跑得通的答案。我会把项目从整体设计、关键技术点、核心代码逻辑、数据库表结构、运行流程到问题排查全部摊开来讲力求你看完就能照着写出来。所有源码我都用可运行的完整 Java 类展示不用伪代码糊弄。2. 技术选型与架构设计思路2.1 为什么是 Socket 而不是 Netty/RMI现在的 Java 网络编程技术栈早就分层了Netty 是高性能异步框架、RMI 是远程方法调用、WebSocket 是 HTTP 握手后的长连接方案。那这个项目为什么选最底层的 Socket核心原因是教学和认知价值。Socket 是 Java 网络编程的地基理解它之后Netty 你才能看懂它的 Channel 是在封装什么、EventLoop 是在优化什么。聊天室本质是 TCP 长连接 消息广播用原生 ServerSocket 和 Socket 写你能看到每一次连接、每一条流的读写、每一个线程的生命周期这些底层细节在框架里都是被隐藏的。而且从方案适配来看聊天室这种小规模并发场景比如几十个客户端原生 Socket 多线程完全够用。每连接一线程的模式代码直观、好理解、好调试性能也能满足教学演示和中小型内部工具的诉求。用 Netty 反而把异步、管道、编解码这些复杂性提前压到你脸上不利于学习 TCP 编程的核心模型。2.2 为什么服务端要配多线程如果不做多线程服务端代码长这样ServerSocket serverSocket new ServerSocket(8888); while (true) { Socket socket serverSocket.accept(); // 处理这个 socket 的读写… }问题出在 accept 之后的处理上。如果你在循环里单独处理一个客户端的读写那第一个客户端不发送消息服务端就会一直阻塞在 read 操作上第二个客户端永远等不到 accept 的机会。这在专业上有个叫法单线程阻塞式 I/O 的饥饿问题。解决思路其实很直觉既然一个人忙不过来就雇多个人。服务端 accept 到新连接后立刻把这个 Socket 交给一个专门的新线程去“伺候”主线程马上回到 accept 状态继续等待下一个连接。每个客户端都对应一条独立的线程它们之间互不阻塞。这里有个很多人忽略的设计点消息广播时的线程同步。每个客户端线程在 read 到一条消息后不能只把自己那条连接写完就完事还要往所有其他客户端的输出流里写一份。多个线程同时修改“在线客户端列表”这个共享集合必须做同步控制否则就会出现 ConcurrentModificationException 或者消息漏发、错发。我后面会在代码解析里具体展示怎么锁它。2.3 为什么用 Swing 做界面Swing 被吐槽丑、老、慢但它确实是 Java 原生 GUI 里最稳的选择。JavaFX 虽然新但很多学校的教材和课程设计要求里已经默认你用 Swing 实现了更关键的是Swing 的事件分发线程模型和 Socket 读线程的消息回调放在一起能讲透跨线程更新 UI 的问题这是 Swing 教学的核心考点之一。在这个项目里客户端界面承担两个职责用户输入消息、点击发送按钮把消息写进 Socket 输出流监听 Socket 输入流收到服务端广播来的消息后展示在聊天区域第二个职责天然是阻塞式 read必须放在独立线程里跑。那就产生了经典问题子线程能不能直接操作 JTextArea直接操作会触发 Swing 线程安全检查轻则刷新异常重则界面卡死。正确姿势是使用 SwingUtilities.invokeLater 把 UI 更新丢回事件分发线程执行。这个坑几乎所有写 Swing 的人都踩过我会在实操部分细讲。2.4 为什么接 MySQL 数据库不接数据库的聊天室只需要一个 ConcurrentHashMap 存在线列表消息哪怕是广播完就丢弃也可以——毕竟屏幕上有记录。但加一层数据库存储后项目直接多出了几大块价值消息持久化重启不丢消息能随时查历史用户管理的基础可以扩展登录、用户信息表数据层和网络层的解耦演示网络线程负责收发消息数据层负责落库职责分离本项目采用的存储策略是把所有聊天记录包括上下线通知都写进一张表每条消息带发送人、接收范围公屏/私聊、内容、时间戳。这样客户端关闭重进服务端可以从库里查最近 N 条记录回放给你。这个“历史消息回放”功能是普通聊天室 Demo 里比较少见的完整设计。3. 核心代码架构与关键细节拆解3.1 整体模块划分这个项目分成四个核心 Java 类文件职责边界非常清晰ServerMain服务端入口负责启动 ServerSocket、循环 acceptServerThread处理单个客户端连接的线程负责读消息、广播、落库ClientMain客户端入口负责初始化 Swing 界面、连接服务端ClientThread客户端侧的消息接收线程负责从 Socket 读服务端广播内容并刷新 UI另外还要有一个数据库工具类DBUtil封装 JDBC 连接和消息插入、查询方法。数据访问逻辑集中在 DBUtil 里网络层只负责调用不直接写 JDBC 代码这样后期换数据库或者改表结构只需要动一个类。3.2 源码解析服务端启动与连接监听服务端的主流程看这段代码public class ServerMain { private static ListSocket onlineClients new CopyOnWriteArrayList(); public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(8888); System.out.println(聊天室服务端启动监听端口 8888); while (true) { Socket socket serverSocket.accept(); System.out.println(新客户端接入 socket.getInetAddress().getHostAddress()); onlineClients.add(socket); // 每个连接交给一个独立线程处理 new ServerThread(socket, onlineClients).start(); // 广播上线提示 String tip 系统消息新用户加入聊天室当前在线人数 onlineClients.size(); broadcast(tip, onlineClients); } } }两个细节值得注意第一onlineClients 用了 CopyOnWriteArrayList。这个线程安全的 List 变体在写操作增删元素时会复制底层数组读操作不加锁。这是刻意为之的取舍在线列表的读频率远高于写频率广播每条消息都要遍历它CopyOnWriteArrayList 可以保证遍历时不抛 ConcurrentModificationException同时读性能很高。实际项目里并发量上来了还可以换 ConcurrentHashMap 以昵称为 key但教学场景用 CopyOnWriteArrayList 更容易讲透。第二accept 之后立刻把 socket 加入在线列表再交给子线程。顺序不能反。如果先 new ServerThread 再 add极小的概率下子线程已经开始读消息并广播列表里却还没有这个新 socket就会导致这个客户端看不到自己发送的第一条消息。3.3 源码解析客户端消息接收线程与 Swing 线程安全客户端有两个线程在跑一个是 Swing 事件分发线程用户点发送按钮、敲回车时触发事件另一个是 ClientThread阻塞在 socket.getInputStream() 的 read 操作上等待服务端广播数据。ClientThread 的核心逻辑如下public class ClientThread extends Thread { private Socket socket; private JTextArea messageArea; public ClientThread(Socket socket, JTextArea messageArea) { this.socket socket; this.messageArea messageArea; } Override public void run() { try (BufferedReader reader new BufferedReader( new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line reader.readLine()) ! null) { final String msg line; SwingUtilities.invokeLater(() - { messageArea.append(msg \n); messageArea.setCaretPosition(messageArea.getDocument().getLength()); }); } } catch (IOException e) { SwingUtilities.invokeLater(() - messageArea.append(系统提示连接已断开\n)); } } }这个类就是跨线程更新 UI 的标准示范。Socket 读线程是一个普通的用户线程当它收到消息后不能直接去 messageArea.append原因是 Swing 的组件模型不是线程安全的多个线程同时操作 UI 组件会导致不可预期的刷新异常。SwingUtilities.invokeLater 会把 UI 更新的任务排队到事件分发线程上由事件分发线程统一执行。这是 Swing 单线程模型的铁律。还有一个细节容易被忽略messageArea.setCaretPosition 要放在 append 之后。如果不把光标移到末尾当聊天记录超出显示区域后新消息会不断把视口往下推但滚动条却不自动跟随用户每次都得手动拉到底部才能看到最新消息。加上这一行保证新消息永远可见。3.4 源码解析服务端客户端消息处理与广播服务端线程处理一条消息的完整流程是从当前 socket 的输入流里 read 到一行文本把消息连同发送时间、发送人插入数据库遍历在线列表向其他所有客户端输出流写这条消息如果客户端关闭连接read 返回 null从在线列表移除并广播下线通知广播的代码长这样private void broadcast(String msg, ListSocket clients) { for (Socket client : clients) { try { PrintWriter writer new PrintWriter( new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8), true); writer.println(msg); } catch (IOException e) { // 说明这个客户端连接异常断开 clients.remove(client); } } }PrintWriter 构造函数的第二个参数 autoFlush 必须传 true否则 println 只写入缓冲区不主动 flush消息就会积压在缓冲区里发不出去客户端那边看起来就是“服务端没反应”实际上数据全堆在内存里没走网络。这里广播消息时有个性能上的小问题每次广播都 new 一个 PrintWriter 包裹输出流。正常项目里这个 writer 应该在线程启动时就创建好并复用而不是每次广播前创建。这个项目作为教学演示我保留了这种写法以便让代码更直观但是你要拿到实际场景里去写记住要复用一个 writer 实例。判断一个客户端是否掉线的标准是 readLine() 返回 null。TCP 是字节流没有消息边界的概念客户端正常关闭时服务端的 read 操作会返回 -1BufferedReader 包装后表现为返回 null如果是异常掉线read 会抛出 SocketException。两种情形都要从在线列表里移除这个 client 并广播下线信息否则列表会残留死连接后续广播会反复向已关闭的 socket 写数据触发 IOException。4. 数据库设计、历史记录查询与消息回放逻辑4.1 表结构设计数据库用 MySQL库名 chat_db核心就一张表CREATE DATABASE IF NOT EXISTS chat_db DEFAULT CHARSET utf8mb4; USE chat_db; CREATE TABLE chat_message ( id INT PRIMARY KEY AUTO_INCREMENT, sender VARCHAR(50) NOT NULL COMMENT 发送人昵称, target VARCHAR(20) DEFAULT all COMMENT 接收对象all 表示公屏, content TEXT NOT NULL COMMENT 消息内容, msg_type TINYINT DEFAULT 0 COMMENT 0文字 1系统通知, create_time DATETIME NOT NULL COMMENT 消息时间 ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;设计上有几点考量用 utf8mb4 而不是 utf8。MySQL 的 utf8 最多只支持 3 字节编码存储不了 emoji 表情和少数生僻汉字一旦聊天内容里带 emoji插入就会报 Incorrect string value 错误。utf8mb4 兼容 4 字节字符所有 emoji 都能入库。这个坑非常隐蔽很多聊天室项目在演示时打字正常一开 emoji 就崩问题就出在字符集上。sender 字段不要用外键关联用户表。当前版本没有做登录注册一行用户名就够。以后要扩展登录体系再单独加 user 表做关联不必现在过度设计。msg_type 区分消息和系统通知。“用户上线/下线”这类事件也入库但类型标记为 1前端展示时可以给系统通知加不同的前缀比如“【系统】”。4.2 JDBC 操作与参数细节数据库工具类 DBUtil 中插入消息和查询历史记录分别对应两个方法public static void insertMessage(String sender, String target, String content, int msgType) { String sql INSERT INTO chat_message(sender, target, content, msg_type, create_time) VALUES(?,?,?,?,NOW()); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, sender); ps.setString(2, target); ps.setString(3, content); ps.setInt(4, msgType); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } public static ListMessageVO queryRecentMessages(int limit) { String sql SELECT id, sender, target, content, msg_type, create_time FROM chat_message ORDER BY id DESC LIMIT ?; // 注意查询要用倒序取最新 N 条但在内存里要反转成正序展示 ListMessageVO list new ArrayList(); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, limit); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { MessageVO msg new MessageVO(); msg.setSender(rs.getString(sender)); msg.setContent(rs.getString(content)); msg.setCreateTime(rs.getTimestamp(create_time)); msg.setMsgType(rs.getInt(msg_type)); list.add(msg); } } } catch (SQLException e) { e.printStackTrace(); } Collections.reverse(list); return list; }两个代码层面的硬性规范必须用 PreparedStatement不能拼接 SQL。聊天内容属于典型的用户输入不可信场景String 拼接会引入 SQL 注入风险。PreparedStatement 是参数化查询用户输入只作为参数传入永远不可能被当成可执行 SQL 语句。try-with-resources 释放资源。Connection、PreparedStatement、ResultSet 全部要关。以前写 JDBC 老是要在 finally 里判断非 null 再 close代码又臭又长还容易漏。Java 7 之后用 try-with-resources 结构资源自动关闭代码瘦了一大圈且更安全。4.3 历史消息回放的完整链路客户端在连接到服务端后界面上立刻显示最近的历史记录这个效果是怎么做到的链路如下客户端启动后先向服务端发送一条特殊指令字符串比如[HISTORY]服务端线程 readLine 读到[HISTORY]不当作普通聊天消息而是调用 DBUtil.queryRecentMessages(20)取出最近 20 条记录服务端把这 20 条记录拼成带固定格式的多行文本每行以[MSG]前缀区分循环写回该客户端客户端 ClientThread 收到带[MSG]前缀的行时不重复显示在聊天区域这个设计看起来简单但语义上是在应用层自定义了简单的协议格式。不只是把心跳、系统控制指令、历史回放指令全混在字符串流里用约定前缀区分这其实就是最原始的应用层协议设计。等以后做更复杂的聊天室无非就是把前缀协议扩展成 JSON 消息体套路是一样的。我给个小建议为了减少这种输入输出流两端的格式约定错误可以把消息封装类独立出来比如定义一个 Message 类带 sender、type、content、timestamp 字段再写一个简单的序列化方法把 Message 对象转成字符串、反序列化方法把字符串转回 Message。这样就不用在多个文件里硬编码[MSG]这种魔法字符串。5. 完整运行流程与界面操作实录5.1 环境准备与项目启动实操环境下建议用 JDK 8 或 JDK 11。JDK 17 也能跑但部分老教程依赖的内部 API 在 17 里会受限建议统一用 JDK 11 及以上版本。数据库方面本地装 MySQL 5.7 或 8.0Windows 下推荐直接用 Navicat 图形化工具执行建表 SQL没有图形化工具就在 mysql 命令行里 source 执行。主要数据库驱动用 mysql-connector-java 8.0.x把 jar 包放到项目 lib 目录并在 IDE 里加入 ClassPath或者用 Maven 引入dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency然后修改 DBUtil 里的连接参数确认能连上你自己的数据库private static final String URL jdbc:mysql://localhost:3306/chat_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4; private static final String USER root; private static final String PASSWORD 你的密码;JDBC URL 里有两个参数一定要加serverTimezoneAsia/Shanghai是解决 MySQL 8.0 驱动的时区报错characterEncodingutf8mb4是确保写入的字符串以 UTF-8 传输防止中文乱码进库。5.2 启动服务端和多个客户端在 IDE 里先运行 ServerMain控制台输出“聊天室服务端启动监听端口 8888”后再运行第一份 ClientMain。客户端界面会弹出一个 JFrame 窗口第一步需要填写昵称我这里做了个简单的 JOptionPane 弹窗输入昵称点“连接”按钮后与服务端建立 TCP 连接。依次再启动第二个、第三个 ClientMain此时第一个客户端窗口的聊天区域应该立刻收到系统提示“系统消息新用户加入聊天室当前在线人数 N”。在任意客户端输入框打一行字点发送其他所有客户端窗口同一时间能看到这条消息。这里有个小细节多开客户端时你没法在一个进程里跑多个 main 方法。正确做法是把不同的客户端实例分不同的运行配置或者干脆把项目打包成 jar用命令行java -jar chat-client.jar开多个终端窗口来跑。IDEA 里可以配置允许运行多个实例也能解决。5.3 验证数据库存储与历史回放发几条消息后用数据库客户端查看聊天表SELECT * FROM chat_message ORDER BY id;数据已经老老实实落库每条消息带着昵称、时间、内容。接下来测试历史回放关闭所有客户端窗口把服务端也停掉然后重新启动服务端和其中一个客户端。连接成功后的瞬间客户端聊天区域自动出现之前发过的最后 20 条记录每条带【历史】前缀和发送时间。到这里这个聊天室项目的完整功能就全部跑通了。实际使用过你会发现历史回放用的“最后 20 条”在 MySQL 里取数是倒序查 limit 20内存里 reverse 成正序这个反转是不可省略的如果不反转客户端展示出来的会是倒叙聊天记录看起来就像“时间倒流”很别扭。6. 常见问题排查与避坑技巧6.1 端口被占用BindException最容易撞上的问题。上次启动的服务端没有正常关闭端口还在 TIME_WAIT 状态再次运行 ServerMain 就报 Address already in use。排查命令直接看端口占用Windowsnetstat -ano | findstr 8888Linuxlsof -i :8888确认是僵死进程占用后kill 掉进程再启动。顺手提一句开发时建议把端口号配置到 properties 文件里而不是硬编码在代码里换端口不用动源码重新编译。6.2 中文乱码字符集不统一是元凶中文乱码问题的根源在于客户端、服务端、数据库三处的字符编码没对齐。这三处必须全部统一成 UTF-8少一个都不行。客户端输入框内容 → Socket 输出流OutputStreamWriter 要指定 UTF-8 而不是用平台默认编码服务端 BufferedReader 读入 → 写数据库PreparedStatement 传入的就是字符串内部编码和 JDBC 驱动字符集相关JDBC URL 带 characterEncodingutf8mb4数据库表 charset 也是 utf8mb4如果发现客户端之间收发中文正常、入库后查出来是乱码那问题几乎可以肯定出在 JDBC URL 或者表字符集上检查方向很明确。6.3 客户端界面卡死Swing 线程违规操作有同学会把 ClientThread 里收到消息后的 UI 更新直接写成 messageArea.append(msg)没有用 SwingUtilities.invokeLater 包一层。表面看有时候也不报错但时机不对就会出现界面卡顿、刷新异常、组件不可见等诡异问题。Swing 的单线程模型要求所有对 UI 组件的访问都必须在事件分发线程里完成。这不是一个可以碰运气的约定而是一条硬性规则。报错信息也不会总是清晰提示你“你在错误线程里操作了 UI”更多时候是莫名卡死、闪烁、界面不刷新。所以代码规范一定要写死从非事件线程更新任何 Swing 组件一律用 invokeLater 包裹。6.4 消息发送但别人收不到autoFlush 设 false 了PrintWriter 构造函数的第二个参数为 false 时println 只是把数据写进包装的缓冲区里缓冲区满或手动 flush 前不会真正发到网络上。症状就是本地控制台看到自己发送的消息已经执行了但其他客户端什么都收不到。把 autoFlush 参数改成 true 是治标治本的方法是理解 flush 的本质缓冲区把多次小写入合并成大块数据一次性输出提高网络吞吐效率。在聊天室这种每条消息都需要立即送达的场景自动 flush 更合适如果项目是批量发送日志或文件传输反而要设 false 手动控制 flush 时机来提升性能。6.5 数据库连接报 Public Key Retrieval 错误MySQL 8.0 默认使用 caching_sha2_password 认证JDBC 驱动首次连接时会要求获取服务端的 RSA 公钥。报错信息大概长这样Public Key Retrieval is not allowed。解决办法是在 JDBC URL 上的加一个参数jdbc:mysql://localhost:3306/chat_db?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4allowPublicKeyRetrievaltrue 允许客户端自动获取服务端公钥。注意这个参数只在 useSSLfalse 时推荐开启生产环境还是要配 SSL 证书否则账号密码在网络上是明文传输的存在安全隐患。6.6 客户端突然掉线后服务端广播崩溃客户端强行关闭窗口不点正常退出按钮socket 关闭不会主动通知服务端。服务端下一次尝试向这个死连接写数据时系统才会扔出来一个 SocketException: Connection reset。我前面展示的 broadcast 方法里已经用 try-catch 把这个异常接住了并在 catch 里把异常 socket 从在线列表移除。但有个更刁钻的情况客户端是在读消息时崩溃服务端的 write 可能不会立刻报错。更好的方案是服务端线程定期做心跳检测例如每 30 秒给客户端发送一次 ping 指令客户端收到后回 pong连续多次无响应就判定掉线并清理。这个机制这个项目里没有做属于进阶扩展项但有时间一定要加上。7. 源码附带的扩展思路与进阶改造方向7.1 从公聊改成私聊当前版本所有消息都广播给全客户端。改成私聊的核心思路是消息里带接收者 ID服务端广播时先判断 target 字段的值target 为 all广播给所有人target 为某个昵称只发给在线列表里昵称匹配的那个客户端实现上你需要维护一个 MapString, Socket 的映射关系客户端连接后先上报昵称服务端注册到 map。以后做登录注册这个 map 也是会话管理的基础。7.2 增加注册登录功能现在客户端直接填昵称就能进真实项目明显不行。扩展点是在 client 启动时先弹登录框用户名密码通过 Socket 传给服务端服务端查 user 表校验。这块的工作量主要集中在数据库增加 user 表以及协议里增加登录指令类型。用户表和消息表怎么关联建议消息表里新增 user_id 字段sender 保留冗余用户名这样展示历史记录时不用 join 用户表而做用户维度的查询时又能用 user_id 高效过滤兼顾查询效率和表关系清晰。7.3 线程池替代每连接一线程每连接一线程的写法直观但连接数大了之后每个连接一个线程线程上下文切换开销和栈内存占用会让你很快遇到瓶颈。实战中这时候用线程池:ExecutorService pool Executors.newCachedThreadPool(); while (true) { Socket socket serverSocket.accept(); pool.submit(new ServerThread(socket, onlineClients)); }CachedThreadPool 按需创建新线程空闲线程超过 60 秒会被回收适合这种短连接多、长连接少点的混合场景。如果全是长连接就要用 FixedThreadPool提前把线程数控制在合理区间。7.4 消息协议升级成 JSON现在的字符串前缀协议等于在裸 TCP 上自己定义了一套私有格式。字段多了之后解析会越来越痛苦。升级方案是用 Jackson 或 Gson 把消息类序列化成 JSON 字符串String json objectMapper.writeValueAsString(messageObj); writer.println(json);收到 JSON 的反序列化回来多字段消息根本不用写一堆 split、substring。而且 JSON 利落了后面接 WebSocket 服务端、写小程序客户端都可以直接复用这套序列化逻辑。8. 我对这个项目的整体评价与手记这个聊天室项目我前后带过不少初学者完整写过从零到跑通基本要一个周末能踩的坑和能学的知识点非常密集。它麻雀虽小但覆盖了 Java 网络编程里最重要的四个必修内容TCP Socket 通信、多线程并发处理、GUI 事件模型、JDBC 数据持久化。我个人在实际操作中的一些体会分享出来给大家参考如果你是自己做练习不要看一眼源码就复制粘贴跑一遍就完事那样收获极低。我建议按这个顺序自己敲先写一个能互相发消息的单线程版本再把服务端改成多线程再加数据库存储再加历史回放。每一步跑通后再叠下一步你会发现每一步的报错和修复都是一次彻底的理解。如果你是做课程设计交作业一定要准备一个演示脚本。把“启动服务端 → 开两个客户端 → 互发消息 → 查看数据库落到 → 重启客户端加载历史记录”这个流程完整走一遍每一部都要知道下一步点击哪个界面。答辩时被问到底层原理能把广播流程和线程模型讲清楚比代码本身更拉分。如果后续想做开源项目可以把它包装成一款带界面的局域网内网聊天工具后面加文件传输、已读回执、消息加密。最后再分享一个小技巧写多线程 Socket 程序时强烈建议所有对外输出都打上时间戳和线程名。比如 System.out.println(Thread.currentThread().getName() | message)排查问题的时候你能直观看到哪条消息是哪个线程发出来的比单纯看消息内容排查效率高得多。这个习惯我从这个项目开始养成的后来调线上 Java 服务排查线程问题时真的救了我好几次。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑