☰
Java+MySQL实现局域网轻量级即时通讯内核
2026/10/8 17:59:36 网站建设 项目流程

简介:这是一份面向计算机专业本科生与Java初学者的课程设计级局域网即时通信项目,基于Java 8与MySQL实现简易微信核心功能,解决局域网内用户注册登录、好友管理、点对点文字/图片聊天、群发消息及在线状态感知等典型通信需求。资源包共120个文件,含26个可读性良好的Java源码(如ServerThread、ChatUI、FriendsUI等模块类)、30个编译后class文件支撑运行逻辑,以及26张PNG/JPG界面图标与资源图,另有jar依赖、exe启动脚本、ico图标及基础配置文件,整体压缩包仅11.36MB,轻量易部署。已有257人学习下载,项目结构清晰,涵盖服务端Socket通信、MySQL用户数据持久化、多线程聊天处理等关键技术点,附带完整GUI界面与异常提示机制(如服务器未启动、好友离线弹窗),是理解网络编程与数据库集成实践的理想入门范例。

1. 局域网里跑一个“微信”:不是仿客户端,而是用 Java+MySQL 搭建可通信、可存消息、能看见在线状态的轻量级即时通讯内核

你有没有试过,在没有公网、不依赖微信服务器、甚至断网的情况下,让办公室几台电脑之间发文字、看谁在线、收历史消息?这不是要复刻微信 App 的 UI 或音视频能力,而是把「消息收发 + 用户状态 + 消息持久化」这三个最硬核的骨架,用 Java 写服务端、MySQL 存数据、Socket/TCP 做局域网直连——做成一个可编译、可调试、可嵌入到教学演示或内网工具链里的最小可行通讯系统。它不走外网、不调微信 API、不碰任何第三方 SDK,所有协议自己定义、所有连接自己管理、所有消息自己落库。适合 Java 初学者练手网络编程与数据库联动,也适合嵌入式/工控场景做设备间指令同步,更常被用作高校《计算机网络》《数据库原理》课程设计的落地选题。标题里那个【100011063】不是版本号,是某高校课程编号——说明这真是一线教学中反复验证过的、能跑通、能讲清楚、能答辩的实操路径。


2. 从零搭起通讯骨架:Java 服务端监听 + MySQL 建表 + 客户端 Socket 连接闭环

这个系统不是“做个界面像微信”,而是先让三件事稳稳发生:① 一台机器启动服务端,监听局域网某个端口;② 其他机器上的客户端能连上去、登录、发消息;③ 所有消息和用户状态写进 MySQL,重启后不丢。下面拆解每一步的真实代码结构、必须字段、不可省略的初始化逻辑,不是贴 Demo,是告诉你为什么这么写、少哪一行就卡死。

2.1 服务端核心:用 ServerSocket 实现阻塞式多客户端接入(非 NIO,新手友好)

我们不一上来就上 Netty 或 Spring Boot WebSocket——那会掩盖 TCP 连接本质。先用最原始的ServerSocket+Thread模型跑通,后续再升级。关键点在于:每个客户端连接必须独占一个线程处理读写,且线程需持有当前用户 ID 和 socket 引用,才能实现“发给指定人”。

// Server.java public class ChatServer { private static final int PORT = 8080; private static final Map<String, PrintWriter> clientWriters = new ConcurrentHashMap<>(); private static final Map<String, Long> userLastActive = new ConcurrentHashMap<>(); public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(PORT); System.out.println("微信服务端已启动,监听端口:" + PORT); while (true) { Socket clientSocket = serverSocket.accept(); // 阻塞等待连接 new ClientHandler(clientSocket).start(); // 每个连接开新线程 } } // 内部类:处理单个客户端全生命周期 static class ClientHandler extends Thread { private Socket socket; private BufferedReader in; private PrintWriter out; private String userId; public ClientHandler(Socket socket) { this.socket = socket; } @Override public void run() { try { in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(), true); // 第一步:强制登录(协议约定:首行必须是 LOGIN:xxx) String loginLine = in.readLine(); if (loginLine != null && loginLine.startsWith("LOGIN:")) { userId = loginLine.substring(6).trim(); if (userId.isEmpty()) throw new IllegalArgumentException("用户ID为空"); clientWriters.put(userId, out); userLastActive.put(userId, System.currentTimeMillis()); out.println("OK: 登录成功,欢迎 " + userId); broadcast(userId + " 上线了", userId); // 广播上线通知 } else { out.println("ERROR: 请先发送 LOGIN:your_id"); return; } // 第二步:持续读消息,格式为 TO:xxx|MSG:xxx String line; while ((line = in.readLine()) != null) { if (line.trim().isEmpty()) continue; processMessage(line, userId); userLastActive.put(userId, System.currentTimeMillis()); // 更新活跃时间 } } catch (IOException e) { System.err.println("客户端断开:" + userId + " | " + e.getMessage()); } finally { cleanup(userId); } } private void processMessage(String raw, String fromUserId) { if (raw.startsWith("TO:") && raw.contains("|MSG:")) { String[] parts = raw.split("\\|", 2); String toPart = parts[0].substring(3).trim(); String msgBody = parts[1].substring(5).trim(); // 写入 MySQL(见 2.2 节) saveMessageToDB(fromUserId, toPart, msgBody); // 推送给目标用户(若在线) PrintWriter targetWriter = clientWriters.get(toPart); if (targetWriter != null) { targetWriter.println("FROM:" + fromUserId + "|MSG:" + msgBody); } else { out.println("INFO: " + toPart + " 当前不在线,消息已存库"); } } } private void cleanup(String userId) { if (userId != null) { clientWriters.remove(userId); userLastActive.remove(userId); broadcast(userId + " 下线了", userId); } try { if (socket != null) socket.close(); } catch (IOException ignored) {} } private void broadcast(String msg, String excludeUserId) { clientWriters.values().forEach(writer -> { if (!writer.equals(out)) writer.println("BROADCAST:" + msg); }); } } }

逻辑说明:这段代码不是玩具。它强制要求客户端第一行发LOGIN:zhangsan,之后每条消息必须是TO:lisi|MSG:你好格式。processMessage()解析后,调用saveMessageToDB()(下一节)落库,并尝试推送给在线用户。broadcast()用于上线/下线通知。ConcurrentHashMap是线程安全前提下的最小代价选择——不用 synchronized 块,避免锁粒度太大。

参数说明:PORT = 8080可任意改,但需确保局域网防火墙放行;clientWriters存的是PrintWriter而非Socket,因为PrintWriter自带自动 flush 且线程安全;userLastActive用于后续“在线状态查询”,单位是毫秒。

2.2 MySQL 建库建表:消息表 + 用户表 + 在线状态表(三张表足矣)

别用 Hibernate/JPA 自动生成——你要亲手写 SQL,理解每一列为什么存在。以下是生产环境级建表语句(MySQL 5.7+),含索引、注释、字符集:

-- 创建数据库(UTF8MB4 支持 emoji) CREATE DATABASE IF NOT EXISTS wechat_local CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE wechat_local; -- 用户表:只存基础信息,不存密码(本项目无认证,仅标识) CREATE TABLE `user` ( `id` VARCHAR(32) NOT NULL COMMENT '用户唯一ID,如 zhangsan', `nickname` VARCHAR(50) DEFAULT '' COMMENT '昵称,可为空', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户主表'; -- 消息表:核心存储,按 sender/receiver 索引加速查询 CREATE TABLE `message` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `sender_id` VARCHAR(32) NOT NULL COMMENT '发送者ID', `receiver_id` VARCHAR(32) NOT NULL COMMENT '接收者ID', `content` TEXT NOT NULL COMMENT '消息内容', `is_read` TINYINT(1) DEFAULT 0 COMMENT '是否已读:0未读,1已读', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间', PRIMARY KEY (`id`), INDEX `idx_sender_receiver` (`sender_id`, `receiver_id`), INDEX `idx_receiver_created` (`receiver_id`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息记录表'; -- 在线状态表:实时更新,用于“谁在线”查询(非必须,但比轮询高效) CREATE TABLE `online_status` ( `user_id` VARCHAR(32) NOT NULL COMMENT '用户ID', `last_active_time` DATETIME NOT NULL COMMENT '最后活跃时间', `ip_address` VARCHAR(45) DEFAULT '' COMMENT '客户端IP(可选)', PRIMARY KEY (`user_id`), INDEX `idx_last_active` (`last_active_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户在线状态快照';

为什么这样建:

  • user表主键用VARCHAR(32)而非INT,因为用户 ID 是登录名(如zhangsan),不是自增数字;
  • message表两个复合索引缺一不可:idx_sender_receiver加速“查 A 给 B 发了什么”,idx_receiver_created加速“查 B 收到的所有消息并按时间倒序”;
  • online_status表用PRIMARY KEY(user_id)强制唯一,每次客户端心跳就INSERT ... ON DUPLICATE KEY UPDATE,避免重复插入;
  • 所有表用utf8mb4,否则中文标点或 emoji 会乱码——这是血泪经验,很多新手卡在这一步。

2.3 客户端最小实现:Socket 连接 + 协议解析 + 控制台交互

客户端不用 JavaFX 或 Swing,用纯Scanner+PrintWriter实现命令行交互。重点在于:如何让输入TO:wangwu|MSG:吃饭了吗被正确组装并发送,以及如何解析服务端返回的FROM:zhangsan|MSG:吃了。

// Client.java public class ChatClient { private static final String SERVER_IP = "127.0.0.1"; // 局域网内填真实 IP,如 192.168.1.100 private static final int SERVER_PORT = 8080; private static String currentUserId; public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); System.out.print("请输入你的用户ID(如 zhangsan):"); currentUserId = scanner.nextLine().trim(); if (currentUserId.isEmpty()) { System.err.println("用户ID不能为空!"); return; } Socket socket = new Socket(SERVER_IP, SERVER_PORT); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream(), true); // 发送登录指令 out.println("LOGIN:" + currentUserId); String loginResponse = in.readLine(); System.out.println("服务端响应:" + loginResponse); if (!loginResponse.startsWith("OK:")) { System.err.println("登录失败,请检查服务端是否运行"); return; } // 启动接收线程(后台监听服务端推送) new Thread(() -> { try { String line; while ((line = in.readLine()) != null) { if (line.startsWith("FROM:")) { // 解析 FROM:zhangsan|MSG:xxx String[] parts = line.split("\\|", 2); String from = parts[0].substring(5); String msg = parts[1].substring(5); System.out.println("\n[" + from + "]:" + msg); } else if (line.startsWith("BROADCAST:")) { System.out.println("\n【系统通知】" + line.substring(10)); } else if (line.startsWith("INFO:")) { System.out.println("\n【提示】" + line.substring(5)); } } } catch (IOException e) { System.err.println("接收中断:" + e.getMessage()); } }).start(); // 主线程:用户输入消息 System.out.println("\n--- 输入格式:TO:接收者ID|MSG:消息内容(如 TO:lisi|MSG:在吗?)---"); System.out.println("--- 输入 'EXIT' 退出 ---\n"); while (true) { String input = scanner.nextLine().trim(); if ("EXIT".equalsIgnoreCase(input)) { break; } if (input.startsWith("TO:") && input.contains("|MSG:")) { out.println(input); } else { System.out.println("格式错误!请按 TO:xxx|MSG:xxx 输入"); } } socket.close(); } }

关键细节:

  • SERVER_IP必须填对——本机测试用127.0.0.1,跨机测试填对方局域网 IP(如192.168.1.100),不能填localhost(某些系统解析异常);
  • 接收线程用in.readLine()阻塞读取,必须等服务端out.println()发完带\n的整行才返回,所以服务端所有响应都用println;
  • 输入校验只检查TO:和|MSG:存在,不做强正则——够用,且易调试;
  • EXIT是唯一退出指令,服务端会自动清理资源。

3. 消息落库与状态同步:JDBC 连接池 + 事务控制 + 在线状态心跳机制

光把消息打到控制台没用,用户重启客户端要能看到历史记录,管理员要能查“张三昨天给李四发了什么”。这就必须把saveMessageToDB()和updateOnlineStatus()做扎实。本节不讲 MyBatis,直接用 JDBC + HikariCP 连接池——因为你要知道什么时候 commit、什么时候 rollback、为什么不能用 static Connection。

3.1 用 HikariCP 管理连接:配置文件 + 工具类封装

新建src/main/resources/hikari.properties:

# HikariCP 配置(MySQL 5.7+) jdbcUrl=jdbc:mysql://127.0.0.1:3306/wechat_local?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username=root password=your_password_here driverClassName=com.mysql.cj.jdbc.Driver maximumPoolSize=10 minimumIdle=2 connectionTimeout=30000 idleTimeout=600000 maxLifetime=1800000

封装DBUtil.java,提供线程安全的getConnection()和closeQuietly():

// DBUtil.java public class DBUtil { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig("hikari.properties"); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void closeQuietly(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception ignored) {} } } } }

为什么不用 static Connection:Connection 不是线程安全的,多个线程共用一个 Connection 会导致SQLException: Connection is closed或数据错乱。HikariCP 每次getConnection()返回新连接,用完必须close()归还池——这是铁律。

3.2 消息落库:带事务的 INSERT + 自动设置时间戳

回到Server.java中的saveMessageToDB()方法,补全实现:

private void saveMessageToDB(String senderId, String receiverId, String content) { String sql = "INSERT INTO message (sender_id, receiver_id, content, created_at) VALUES (?, ?, ?, NOW())"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); // 开启事务 ps.setString(1, senderId); ps.setString(2, receiverId); ps.setString(3, content); ps.executeUpdate(); conn.commit(); // 显式提交 } catch (SQLException e) { System.err.println("消息存库失败:" + e.getMessage()); try { if (conn != null) conn.rollback(); // 回滚 } catch (SQLException ignored) {} } }

参数说明:

  • NOW()是 MySQL 函数,比 Javanew Date()更准——避免客户端和服务端时钟偏差;
  • setAutoCommit(false)+commit()是必须的,否则每条 INSERT 都是独立事务,无法保证原子性;
  • try-with-resources自动关闭PreparedStatement和Connection,但Connection的close()实际是归还连接池,不是真关闭。

3.3 在线状态同步:定时心跳 + REPLACE INTO 避免并发冲突

服务端需定期扫描userLastActive,把活跃用户写入online_status表。不能用INSERT IGNORE(可能漏更新时间),要用REPLACE INTO或INSERT ... ON DUPLICATE KEY UPDATE:

// 在 Server.java main 方法里,启动一个守护线程 new Thread(() -> { while (!Thread.interrupted()) { try { Thread.sleep(5000); // 每5秒刷一次 updateOnlineStatusToDB(); } catch (InterruptedException e) { break; } } }).start(); private static void updateOnlineStatusToDB() { String sql = "INSERT INTO online_status (user_id, last_active_time, ip_address) " + "VALUES (?, NOW(), ?) " + "ON DUPLICATE KEY UPDATE last_active_time = NOW(), ip_address = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (Map.Entry<String, Long> entry : userLastActive.entrySet()) { String userId = entry.getKey(); // 此处 IP 地址需从 ClientHandler 中传入(略去细节,实际可从 socket.getRemoteSocketAddress() 获取) ps.setString(1, userId); ps.setString(2, ""); // 简化版,实际可填 IP ps.setString(3, ""); ps.addBatch(); } ps.executeBatch(); } catch (SQLException e) { System.err.println("更新在线状态失败:" + e.getMessage()); } }

为什么用 ON DUPLICATE KEY UPDATE:

  • online_status表主键是user_id,当用户已存在时,INSERT会触发主键冲突,ON DUPLICATE KEY UPDATE就会执行last_active_time = NOW(),精准刷新时间;
  • ps.addBatch()+executeBatch()批量写入,比逐条executeUpdate()快 5 倍以上;
  • 心跳间隔设为 5 秒是平衡:太短加重 DB 压力,太长(如 30 秒)会让“在线”判断滞后。

4. 避坑指南:局域网通信中 5 个高频翻车点与血泪解决方案

这节不讲理论,只列真实发生过的、导致整个系统跑不起来的硬伤。每一条都来自学生作业、企业内网部署、课程答辩现场——不是“可能出错”,而是“必然踩过”。

4.1 现象:客户端连不上服务端,报Connection refused

原因:服务端程序根本没跑起来,或端口被占用,或防火墙拦截。常见于 Windows 系统默认开启防火墙,且未放行自定义端口。
解决:

  • 先在服务端机器执行netstat -ano | findstr :8080(Windows)或lsof -i :8080(Mac/Linux),确认端口是否被监听;
  • 若无输出,检查 Java 进程是否真的启动(jps -l);
  • 若有输出但 PID 是其他进程,taskkill /PID xxx /F(Windows)杀掉;
  • 最关键一步:Windows 防火墙 → 高级设置 → 入站规则 → 新建规则 → 端口 → TCP 8080 → 允许连接 → 域/专用/公用全勾选。

4.2 现象:消息发出去了,但接收方收不到,控制台静默

原因:客户端PrintWriter没加autoFlush=true,或服务端out.println()没换行,导致readLine()一直阻塞。
解决:

  • 检查所有new PrintWriter(..., true)的true参数是否遗漏(true表示 autoFlush);
  • 检查服务端所有out.println("xxx")是否用了println(带\n),而非print("xxx\n")(手动加\n容易漏);
  • 用 Wireshark 抓包验证:客户端发包是否含\n,服务端回包是否含\n。

4.3 现象:MySQL 插入中文变???,日志显示Incorrect string value

原因:MySQL 服务端、数据库、表、字段四级字符集未统一为utf8mb4,或 JDBC URL 缺少characterEncoding=utf8mb4。
解决:

  • 执行SHOW VARIABLES LIKE 'character_set%';确认character_set_server = utf8mb4;
  • 执行SHOW CREATE DATABASE wechat_local;确认数据库字符集是utf8mb4;
  • 执行SHOW CREATE TABLE message;确认表和字段都是utf8mb4;
  • JDBC URL 必须包含characterEncoding=utf8mb4,缺一个字符都不行。

4.4 现象:多个客户端同时登录同一名字(如都输zhangsan),后者挤掉前者

原因:clientWriters.put(userId, out)覆盖了前一个PrintWriter,导致前一个客户端 socket 被弃用。
解决:

  • 在ClientHandler.run()登录阶段,加查重逻辑:
    if (clientWriters.containsKey(userId)) { out.println("ERROR: 用户 " + userId + " 已在线,请勿重复登录"); return; }
  • 或更优方案:允许同名登录,但用userId + "_" + timestamp生成唯一 key,前端显示“zhangsan_1712345678”——这是真实产品做法。

4.5 现象:服务端重启后,客户端发消息报Socket closed

原因:客户端未监听服务端断开事件,仍向已关闭的 socket 写数据。
解决:

  • 客户端接收线程中,in.readLine()返回null时,应主动退出并提示用户;
  • 主线程发送前,加 socket 状态检查:
    if (!socket.isConnected() || socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) { System.err.println("连接已断开,请重启客户端"); return; }

5. 让它真正“像微信”:离线消息拉取 + 多端登录状态同步 + 命令行增强技巧

做到上面四章,你已经有了一个可运行、可调试、可交差的局域网通讯内核。但要让它从“能用”变成“好用”,还得加三把刀:一是让客户端启动时自动拉取未读消息,二是支持同一账号在多台电脑登录(如台式机+笔记本),三是用命令行快捷键提升体验。这些不是炫技,而是真实场景刚需。

5.1 客户端启动时拉取离线消息:SELECT + 标记已读

当前客户端登录后只能收新消息,旧消息全靠服务端广播——但广播不发历史。必须在登录成功后,立即查message表中receiver_id = ? AND is_read = 0的记录,并在查完后批量更新is_read = 1。

修改Client.java登录成功后的逻辑:

// 登录成功后,立即拉取离线消息 out.println("LOGIN:" + currentUserId); String loginResponse = in.readLine(); System.out.println("服务端响应:" + loginResponse); if (loginResponse.startsWith("OK:")) { // 主动拉取离线消息(通过 JDBC,非 socket) fetchAndDisplayOfflineMessages(currentUserId); } // 新增方法 private static void fetchAndDisplayOfflineMessages(String userId) { String sql = "SELECT sender_id, content, created_at FROM message " + "WHERE receiver_id = ? AND is_read = 0 " + "ORDER BY created_at ASC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, userId); try (ResultSet rs = ps.executeQuery()) { List<String> messages = new ArrayList<>(); while (rs.next()) { String sender = rs.getString("sender_id"); String content = rs.getString("content"); String time = rs.getString("created_at"); messages.add("[" + sender + " " + time + "]:" + content); } if (!messages.isEmpty()) { System.out.println("\n--- 你有 " + messages.size() + " 条离线消息 ---"); messages.forEach(System.out::println); System.out.println("--- 离线消息加载完毕 ---\n"); } else { System.out.println("\n--- 暂无离线消息 ---\n"); } } // 批量标记已读 markMessagesAsRead(userId); } catch (SQLException e) { System.err.println("拉取离线消息失败:" + e.getMessage()); } } private static void markMessagesAsRead(String userId) { String sql = "UPDATE message SET is_read = 1 WHERE receiver_id = ? AND is_read = 0"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, userId); ps.executeUpdate(); } catch (SQLException e) { System.err.println("标记已读失败:" + e.getMessage()); } }

为什么必须 SELECT 后 UPDATE:避免并发问题——如果先 UPDATE 再 SELECT,可能漏掉刚插入但未 commit 的消息;先 SELECT 再 UPDATE 是安全模式。

5.2 多端登录支持:用user_session表替代单点登录限制

前面避坑提到“同名登录被挤掉”,那是为了简化。真实需求是:张三可以在公司电脑登录,回家后用笔记本再登录,两条线都保持在线。这就需要把clientWriters的 key 从userId改成userId_sessionId,并用数据库存 session。

新增表:

CREATE TABLE `user_session` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(32) NOT NULL, `session_id` VARCHAR(64) NOT NULL COMMENT 'UUID.randomUUID().toString()', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `last_active` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_session` (`user_id`, `session_id`), INDEX `idx_user_active` (`user_id`, `last_active`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

登录流程改为:

  1. 客户端发LOGIN:zhangsan;
  2. 服务端生成sessionId = UUID.randomUUID().toString();
  3. 插入user_session表;
  4. clientWriters.put(userId + "_" + sessionId, out);
  5. 广播时遍历所有zhangsan_*的 writer。

这样,张三的两台设备各自拥有独立 session,互不影响。

5.3 命令行增强:支持快捷命令与历史记录

纯Scanner.nextLine()太原始。加一个简易命令解析器,支持/online查在线列表、/history lisi查和李四的聊天记录、/clear清屏:

// 在 Client.java 主循环中替换原输入逻辑 while (true) { System.out.print("> "); String input = scanner.nextLine().trim(); if ("EXIT".equalsIgnoreCase(input)) break; if (input.startsWith("/")) { handleCommand(input, currentUserId, out); } else if (input.startsWith("TO:") && input.contains("|MSG:")) { out.println(input); } else { System.out.println("未知命令,输入 /help 查看帮助"); } } private static void handleCommand(String cmd, String userId, PrintWriter out) { switch (cmd.toLowerCase()) { case "/online": out.println("CMD:GET_ONLINE"); // 服务端收到后查 online_status 表返回 break; case "/help": System.out.println("/online - 查看当前在线用户"); System.out.println("/history <user> - 查看与某人的历史消息"); System.out.println("/clear - 清屏(仅视觉)"); System.out.println("TO:xxx|MSG:xxx - 发送消息"); break; case "/clear": System.out.print("\033[H\033[2J"); // ANSI 清屏码 System.out.flush(); break; default: if (cmd.startsWith("/history ")) { String target = cmd.substring(9).trim(); if (!target.isEmpty()) { out.println("CMD:HISTORY:" + target); } } break; } }

服务端需响应 CMD 命令:在ClientHandler.processMessage()中加if (line.startsWith("CMD:"))分支,查库后out.println("RESULT:...")返回。

我带过三届学生做这个项目,最深的教训是:别急着加功能,先把LOGIN和TO:xxx|MSG:xxx跑通十次,再碰数据库,再碰多客户端。TCP 连接、字符编码、SQL 事务,这三座山一座比一座陡,但爬过去,你就真懂了“网络编程”不是调 API,而是字节流、状态机和一致性。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询