Java局域网简易微信:Socket通信+MySQL持久化实战

发布时间:2026/9/28 2:40:30
Java局域网简易微信:Socket通信+MySQL持久化实战 简介这是一份面向计算机专业本科生与Java初学者的课程设计级局域网即时通信项目基于Java 8与MySQL实现简易微信核心功能适用于网络编程、数据库应用及Socket通信等课程实践。资源包共120个文件含26个Java源码涵盖ServerThread、ChatUI、FriendsUI等关键模块、30个编译后class文件、44张图片资源png/jpg用于界面图标与截图、以及jar、exe、ico等可执行与配置文件整体11.36MB结构完整便于调试与二次开发。已有256人学习下载体现了其在教学场景中的实用价值。读者可直接运行服务端与客户端深入理解TCP Socket通信机制、MySQL用户/好友/消息表设计、多线程聊天处理逻辑以及异常状态提示如服务器未启动、好友离线等的工程化实现是掌握Java桌面应用与局域网通信原理的典型参考案例。1. 局域网内跑通一个“能发消息、存记录、不连外网”的微信雏形它不是替代品而是帮你把 Java 网络编程 MySQL 数据持久化 Socket 通信三块硬骨头在真实交互场景里一次性嚼碎的练兵场你不需要部署服务器、不用申请域名、不碰 HTTPS 证书甚至手机都不用掏——只要两台电脑在同一 WiFi 下用 IntelliJ 写几十行 Java建三张 MySQL 表就能让 A 发的“你好”实时出现在 B 的窗口里且每条消息自动落库、带时间戳、支持历史回溯。这不是玩具 Demo而是我带新人做「分布式协同系统入门」时必过的第一关它强制你直面 TCP 连接管理、线程安全写库、消息序列化边界、MySQL 事务隔离级别在并发插入时的真实表现。很多 Java 初学者卡在“学了 JDBC 却不会写多线程聊天”或“懂 Socket 却存不了数据”——这个项目就是专治这种割裂感。它不追求 UI 美观Swing 足够不模拟微信全部功能无朋友圈、无语音但每行代码都对应一个可调试、可打断点、可查日志的真实问题。如果你正被“Java 网络编程怎么和数据库联动”困扰或者面试被问到“如何设计一个轻量级 IM 后端”这个局域网微信就是最扎实的起点。2. 用 Java Socket 实现点对点消息收发从 raw TCP 连接建立到消息帧协议设计2.1 为什么不用 WebSocket 或 Netty先用原生 Socket 把通信黑匣子打开新手常误以为“IM 就该上 Netty”但 Netty 是封装层它掩盖了连接生命周期、粘包/半包、心跳保活这些底层细节。本项目坚持用java.net.Socket和ServerSocket目的只有一个让你亲手处理每一个字节流。比如当客户端发送hello服务端InputStream.read()可能一次只读到hel下一次才读到lo——这就是典型的 TCP 粘包。若直接new String(buffer)就会得到乱码或截断。我们不回避它而是用明确的消息帧协议来解决。提示本方案不适用广域网或高并发场景单服务端最多支撑 50 个客户端但对局域网内 2–10 台设备通信完全足够且所有逻辑可控、可 debug。2.2 定义最小可行消息帧4 字节长度头 UTF-8 内容体我们采用定长头部 变长内容的二进制协议这是 IM 领域最经典、最易调试的设计字段长度说明length4 字节int消息体 UTF-8 编码后的字节长度网络字节序Big Endiancontentlength字节真实消息文本UTF-8 编码这样设计的好处是服务端每次先读 4 字节解析出len再循环读取len字节即可完整拿到一条消息彻底规避粘包。Java 中用DataInputStream/DataOutputStream直接支持readInt()/writeInt()无需手动处理字节序。// 客户端发送消息Client.java public void sendMessage(String msg) throws IOException { byte[] content msg.getBytes(StandardCharsets.UTF_8); dataOutput.writeInt(content.length); // 写入长度头 dataOutput.write(content); // 写入内容体 dataOutput.flush(); // 强制刷出缓冲区 }// 服务端接收消息ServerHandler.java public String receiveMessage() throws IOException { int len dataInput.readInt(); // 先读4字节长度 byte[] buffer new byte[len]; int totalRead 0; while (totalRead len) { int read dataInput.read(buffer, totalRead, len - totalRead); if (read -1) throw new EOFException(Connection closed unexpectedly); totalRead read; } return new String(buffer, StandardCharsets.UTF_8); // 解码为字符串 }参数说明StandardCharsets.UTF_8是硬性要求避免 Windows 默认 GBK 与 Linux UTF-8 不一致导致解码失败dataOutput.flush()不可省略否则消息可能滞留在缓冲区对方收不到read()返回值必须校验TCP 连接异常中断时read可能返回-1需主动抛异常终止线程。2.3 多客户端连接管理用 ConcurrentHashMap 存活连接拒绝全局静态变量服务端需同时处理多个客户端连接每个连接对应一个独立线程ServerHandler。为实现“群发”或“私聊”必须维护在线用户列表。我们用ConcurrentHashMapString, Socket存储key 为用户名登录时由客户端提供value 为对应 Socket。注意绝不使用static ListSocket或static Map——这会导致多线程并发修改时ConcurrentModificationException且无法保证 key 唯一性。// Server.java 中的连接注册逻辑 private static final ConcurrentHashMapString, Socket ONLINE_USERS new ConcurrentHashMap(); public static void registerUser(String username, Socket socket) { if (ONLINE_USERS.putIfAbsent(username, socket) ! null) { throw new IllegalArgumentException(Username username already exists); } } public static Socket getUserSocket(String username) { return ONLINE_USERS.get(username); } public static void removeUser(String username) { ONLINE_USERS.remove(username); }关键点putIfAbsent()是原子操作天然线程安全比synchronized更轻量用户名作为 key意味着同一用户名不能重复登录符合微信“账号唯一”逻辑removeUser()在Socket关闭时调用防止内存泄漏——这是后续排查“连接数暴涨”的核心依据。3. 用 MySQL 存储消息与用户建表规范、JDBC 连接池配置与事务控制3.1 三张表搞定基础数据模型user、message、chat_log本项目不引入 ORM如 MyBatis/Hibernate全程手写 SQL JDBC目的是看清每一笔 INSERT 的执行路径和事务边界。表结构设计遵循最小够用原则表名字段类型说明useridBIGINT PK AI主键usernameVARCHAR(50) UNIQUE NOT NULL登录名唯一索引password_hashCHAR(64) NOT NULLSHA-256 加密密码明文密码仅用于本地测试created_atDATETIME DEFAULT CURRENT_TIMESTAMP注册时间messageidBIGINT PK AI主键senderVARCHAR(50) NOT NULL发送者 usernamereceiverVARCHAR(50) NOT NULL接收者 username私聊或ALL广播contentTEXT NOT NULL消息正文sent_atDATETIME DEFAULT CURRENT_TIMESTAMP发送时间is_readTINYINT(1) DEFAULT 0是否已读0未读1已读chat_logidBIGINT PK AI主键session_idVARCHAR(100) NOT NULL会话标识格式A_B或B_A确保双向唯一message_idBIGINT NOT NULL关联 message.idcreated_atDATETIME DEFAULT CURRENT_TIMESTAMP日志创建时间注意chat_log表是冗余设计目的是快速查询某两人之间的全部对话WHERE session_id IN (A_B, B_A)避免每次 JOINmessage表扫描全表。3.2 HikariCP 连接池配置5 个参数决定性能与稳定性直接new Connection()会严重拖慢响应每次新建 TCP 连接耗时 10–50ms必须用连接池。HikariCP 是当前 Java 生态事实标准配置项极少但每个都关键// DatabaseConfig.java public static HikariDataSource getDataSource() { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/wechat_local?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue); config.setUsername(root); config.setPassword(your_password); // 生产环境务必从配置文件读取 config.setMaximumPoolSize(10); // 最大连接数局域网场景 5–10 足够 config.setMinimumIdle(2); // 最小空闲连接防冷启动延迟 config.setConnectionTimeout(3000); // 获取连接超时3秒避免线程卡死 config.setIdleTimeout(600000); // 空闲连接存活时间10分钟 config.setMaxLifetime(1800000); // 连接最大寿命30分钟防 MySQL wait_timeout 断连 return new HikariDataSource(config); }参数详解maximumPoolSize10局域网内并发连接数极低设太高反而增加 MySQL 线程开销connectionTimeout3000必须设否则getConnection()可能无限阻塞如 MySQL 服务宕机maxLifetime1800000MySQL 默认wait_timeout288008小时但连接池需主动回收设为 30 分钟更稳妥useSSLfalse局域网通信无需加密开启 SSL 会显著降低吞吐量且需额外证书配置。3.3 消息落库的事务边界INSERT message INSERT chat_log 必须原子用户点击“发送”后需同时向message表插入消息并向chat_log表插入会话日志。这两步必须在一个事务内完成否则会出现“消息显示成功但历史记录丢失”的数据不一致。// MessageService.java public void saveMessage(String sender, String receiver, String content) throws SQLException { String sql INSERT INTO message (sender, receiver, content) VALUES (?, ?, ?); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { conn.setAutoCommit(false); // 关闭自动提交 ps.setString(1, sender); ps.setString(2, receiver); ps.setString(3, content); ps.executeUpdate(); // 获取刚插入的 message.id ResultSet rs ps.getGeneratedKeys(); long messageId 0; if (rs.next()) messageId rs.getLong(1); // 插入 chat_log String logSql INSERT INTO chat_log (session_id, message_id) VALUES (?, ?); try (PreparedStatement logPs conn.prepareStatement(logSql)) { String sessionId generateSessionId(sender, receiver); logPs.setString(1, sessionId); logPs.setLong(2, messageId); logPs.executeUpdate(); } conn.commit(); // 两步都成功才提交 } catch (SQLException e) { // 任一步失败整个事务回滚 if (conn ! null) conn.rollback(); throw e; } }关键逻辑conn.setAutoCommit(false)是事务起点ps.getGeneratedKeys()获取自增主键避免二次查询generateSessionId()保证A→B和B→A归入同一会话Math.min(A,B)_Math.max(A,B)rollback()在 catch 块中显式调用确保异常时数据零残留。4. Swing GUI 与业务逻辑解耦事件驱动架构让界面不卡顿、消息不丢4.1 为什么不用 JavaFXSwing 的 EDT 机制更利于教学穿透JavaFX 对新手隐藏了事件分发线程EDT概念而 Swing 的SwingUtilities.invokeLater()强制你直面“UI 更新必须在 EDT 执行”这一铁律。本项目所有耗时操作Socket 连接、数据库写入、消息接收循环均在后台线程运行UI 线程只负责渲染这是避免界面假死的核心。// LoginFrame.java 登录按钮监听器 loginButton.addActionListener(e - { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); // 启动后台登录线程 new Thread(() - { try { boolean success loginService.login(username, password); // 登录结果必须切回 EDT 更新 UI SwingUtilities.invokeLater(() - { if (success) { openChatWindow(username); dispose(); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); } }); } catch (Exception ex) { SwingUtilities.invokeLater(() - JOptionPane.showMessageDialog(this, 连接失败 ex.getMessage()) ); } }).start(); });血泪经验若在后台线程直接调用JOptionPane.showMessageDialog()会抛IllegalStateException非 EDT 线程操作 UIdispose()必须在SwingUtilities.invokeLater()内执行否则窗体关闭逻辑可能失效new String(passwordField.getPassword())是 Swing 密码框的标准取值方式getPassword()返回char[]为安全考虑。4.2 消息接收的双缓冲队列防止 Swing EDT 被长消息阻塞服务端推送消息过来时若直接在 Socket 读取线程中SwingUtilities.invokeLater(() - textArea.append(msg))当消息量大如连续 100 条时EDT 会被大量append()请求塞满导致 UI 卡顿。解决方案用BlockingQueue做消息中转。// ChatWindow.java private final BlockingQueueString messageQueue new LinkedBlockingQueue(); private final ScheduledExecutorService uiUpdater Executors.newScheduledThreadPool(1); // 启动 UI 更新调度器每 50ms 拉取一次新消息 uiUpdater.scheduleAtFixedRate(() - { try { String msg; while ((msg messageQueue.poll()) ! null) { textArea.append(msg \n); textArea.setCaretPosition(textArea.getDocument().getLength()); // 自动滚动到底部 } } catch (Exception e) { e.printStackTrace(); } }, 0, 50, TimeUnit.MILLISECONDS); // Socket 接收线程中只做入队操作 new Thread(() - { try { while (!Thread.currentThread().isInterrupted()) { String msg serverHandler.receiveMessage(); messageQueue.offer([收到] msg); // 非阻塞入队绝不卡住接收线程 } } catch (IOException e) { // 连接断开清理资源 SwingUtilities.invokeLater(() - JOptionPane.showMessageDialog(this, 与服务器断开连接)); } }).start();参数说明LinkedBlockingQueue是线程安全队列offer()不会阻塞scheduleAtFixedRate每 50ms 检查一次队列频率足够平滑人眼感知不到延迟又不至于高频轮询浪费 CPUsetCaretPosition()确保新消息始终显示在底部避免用户手动滚动丢失最新内容。5. 避坑指南局域网微信开发中最常翻车的 4 个硬核问题5.1 现象客户端能连上服务端但发消息后服务端readInt()报EOFException原因客户端 Socket 关闭时未发送 FIN 包或服务端未正确处理read()返回-1。常见于异常退出CtrlC、未调用socket.close()、或网络设备如路由器主动切断空闲连接。解决客户端退出前必须dataOutput.close()→socket.close()服务端receiveMessage()中read()返回-1时立即break并清理ONLINE_USERS增加心跳机制客户端每 30 秒发PING消息服务端超时 60 秒未收则踢出连接。5.2 现象MySQL 插入中文乱码数据库里显示??原因MySQL 服务端、数据库、表、连接 URL 四层字符集不统一。即使建表时指定CHARSETutf8mb4若连接 URL 未加characterEncodingutf8mb4JDBC 仍按默认 ISO-8859-1 解析。解决MySQL 服务端配置my.cnf[mysqld] default-character-set utf8mb4创建数据库时CREATE DATABASE wechat_local CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;JDBC URL 强制指定jdbc:mysql://...?characterEncodingutf8mb4useUnicodetrue验证SHOW VARIABLES LIKE character_set%;确保character_set_client/connection/results全为utf8mb4。5.3 现象两个客户端 A、B 登录后A 发消息给 BB 收不到但服务端日志显示“已转发”原因ONLINE_USERS.get(B)返回null但代码未判空直接getOutputStream()导致NullPointerException被吞掉尤其在try-catch中空 catch。解决所有get()操作后必须if (socket null) { log.warn(User {} offline, receiver); return; }在ServerHandler构造函数中注册socket时同步设置SO_TIMEOUT30000避免read()长期阻塞使用jstack查看线程状态确认ServerHandler线程是否因 NPE 挂起。5.4 现象Swing 界面输入框无法输入中文或输入法候选框位置错乱原因Swing 组件未启用输入法支持或 JVM 启动参数缺失。Windows 系统尤其明显。解决启动程序时添加 JVM 参数-Dfile.encodingUTF-8 -Dsun.jnu.encodingUTF-8在main()方法开头强制设置System.setProperty(sun.awt.im.disabled, false);JTextField/JTextArea创建后调用component.enableInputMethods(true);虽文档说默认 true但某些 JDK 版本需显式启用。6. 进阶技巧用 MySQL Binlog 实现实时消息同步绕过 Socket 连接瓶颈当你发现局域网内 5 台以上设备同时在线时ServerSocket的单线程 accept 多线程 handler 模式开始出现延迟平均响应 200ms此时不必升级 Netty而是换一条路让 MySQL 成为消息总线。原理很简单——利用 MySQL 的binlog日志将message表的每一次 INSERT 解析为消息事件由各客户端监听 binlog 变化直接拉取新消息。这本质是“数据库即消息队列”规避了 Socket 连接管理的复杂度。6.1 开启 MySQL binlog 并配置为 ROW 格式Binlog 默认关闭且必须设为ROW格式才能捕获具体字段变更STATEMENT格式只记录 SQL无法解析content值-- 登录 MySQL root 用户执行 SET GLOBAL log_bin ON; SET GLOBAL binlog_format ROW; SET GLOBAL binlog_row_image FULL; -- 确保 INSERT 记录所有字段 -- 永久生效需修改 my.cnf -- [mysqld] -- log-binmysql-bin -- binlog-formatROW -- binlog-row-imageFULL6.2 客户端用 Maxwell 或 Debezium 监听 binlog轻量级选 MaxwellMaxwell 是纯 Java 编写的 binlog 解析器输出 JSON 到 Kafka 或 stdout。本项目用其 stdout 模式配合 Java ProcessBuilder 实时消费// BinlogListener.java public class BinlogListener { private final Process process; public BinlogListener() throws IOException { // 启动 maxwell 命令需提前下载 maxwell-1.40.0.jar ProcessBuilder pb new ProcessBuilder( java, -cp, /path/to/maxwell-1.40.0.jar, com.zendesk.maxwell.Maxwell, --userroot, --passwordyour_password, --hostlocalhost, --port3306, --producerstdout, --filterinclude:wechat_local.message ); this.process pb.start(); } public void startListening(ConsumerJSONObject onMessage) { new Thread(() - { try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream()))) { String line; while ((line reader.readLine()) ! null) { try { JSONObject json new JSONObject(line); if (insert.equals(json.getString(type))) { JSONObject data json.getJSONObject(data); String sender data.getString(sender); String receiver data.getString(receiver); String content data.getString(content); String sentAt data.getString(sent_at); // 构造消息对象并通知 UI onMessage.accept(new JSONObject() .put(from, sender) .put(to, receiver) .put(content, content) .put(time, sentAt) ); } } catch (JSONException e) { // 忽略解析失败的非 message 事件如 DDL } } } catch (IOException e) { e.printStackTrace(); } }).start(); } }落地要点Maxwell 需要replication client和replication slave权限GRANT REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO maxwell%;--filter参数精确限定只监听message表避免日志爆炸onMessage回调中SwingUtilities.invokeLater()更新 UI保持线程安全Maxwell 进程崩溃时process.waitFor()可触发重启逻辑保障高可用。6.3 对比 Socket 方案的收益与代价维度Socket 方案Binlog 方案连接管理需维护 N 个长连接内存占用随客户端数线性增长客户端仅需短连接查询 binlog无连接状态消息实时性毫秒级TCP 直传秒级binlog 写入 → Maxwell 解析 → 客户端消费通常 1s扩展性单服务端瓶颈明显50 连接性能陡降依赖 MySQL 性能500 客户端无压力开发成本需处理粘包、心跳、重连、线程安全仅需集成 Maxwell逻辑更简单适用场景教学、小团队内部工具、低延迟强交互生产环境、设备数 20、对连接稳定性要求高我现在的习惯是新人项目一律用 Socket逼他们啃透网络底层上线项目第一版就上 Binlog因为 MySQL 已是基础设施再搭一套消息中间件纯属重复造轮子。这条路径不是银弹但它让我在三年内交付了 7 个局域网协同系统零次因通信模块故障回滚。希望帮到你。本文还有配套的精品资源点击获取