简介:这是一份面向毕业设计与课程设计的Java局域网聊天室系统完整资料,适合计算机相关专业学生和初学Java网络编程的开发者使用。内容包含ChatClient与ChatServer两部分的源代码、配套论文以及工程配置文件,覆盖Socket通信、多线程、界面交互等核心模块,便于读者理解局域网即时通讯的完整实现流程,也可直接用于答辩或课设验收。压缩包共239个文件,主要有.h/.cpp源码文件、Visual C++工程文件(.dsw/.dsp/.clw等)、exe演示程序、wav音频、doc论文及lib库文件,整体大小为14.13MB。目前已有107人学习下载。通过学习可以获得完整可运行的聊天室项目源码、毕业论文参考文档、工程配置与演示程序,既能对照源码分析架构,也能在此基础上二次扩展,是毕业设计和课程设计的高性价比参考资料。能够有效支撑项目展示与功能讲解。
1. 为什么课程设计答辩时,一个局域网聊天室能把你问穿
“JAVA基于局域网的聊天室系统”这十个字,出现在无数份毕业设计和课程设计的选题清单里。先说一个反直觉的结论:它看起来是入门练手项目,实际上最容易在答辩现场翻车。因为功能越简单,导师越有精力把 Socket 生命周期、线程安全、协议设计、界面卡顿这些底层细节逐个问穿。这个项目解决的是“从 Java 语法走向真实网络编程”的最后一公里,适合想要源代码+论文一次交付、又不想引入 Spring 全家桶和复杂中间件的人。但如果你以为聊天室只是几行 Socket 拼起来,后面几个章节的坑你会挨个踩一遍。
2. 需求和技术选型:先把功能边界锁死,再谈代码
2.1 课程设计版和毕业设计版差在哪
很多同学拿到题目第一反应是“聊天室嘛,一个服务器挂一堆客户端,广播消息就行”。真做起来才发现,课程设计和毕业设计对规模的期望完全不同。
课程设计版的功能边界一般很收敛:一个服务器进程、多个客户端连接、群聊消息广播、在线用户列表刷新、客户端能正常退出。少数学校会要求加分项,比如私聊、表情、字体颜色。毕业设计版就不一样了,它需要“系统感”和“论文支撑”,常见做法是在基础聊天之外再加三样东西:私聊定向转发、消息历史持久化、客户端的异常断线清理。有些做得好的还会加文件传输、离线消息缓存,甚至把界面从 Swing 换成 JavaFX。
我的建议是:动手写代码之前,先拿一张纸和导师确认功能清单,明确哪些必须做、哪些加分做。最忌讳的是自己去“升级”,比如引入 Spring Boot + WebSocket + MongoDB 做所谓的微服务聊天室。功能一多,论文工作量看起来很大,但答辩时导师问“局域网场景为什么不用原生 Socket 而用 WebSocket”“握手开销值不值”,你答不上来,反而把自己逼进死角。课程设计/毕设评分的重点不是功能多,而是每个功能你都能说出“为什么这样实现”。
2.2 通信层选型:Socket 裸写还是 Netty
通信层是聊天室的心脏,选型直接决定你一周能写完还是一个月还在调 BUG。这里有两套主流方案:原生 Socket 和 Netty。
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 原生 Socket + 多线程 | 代码量小、概念直白、JDK 自带零依赖 | 高并发下线程开销大、需要自己处理半包 | 课程设计、本科毕设、快速交付 |
| Netty | 高性能、NIO 模型、自带编解码器 | 引入 NIO 概念、学习曲线陡、答辩容易深挖 | 研究生课题、对并发有明确要求的系统 |
课设和本科毕设我一般直接用原生 Socket。理由不是 Netty 不好,而是“可解释性”。答辩评委问“你这个消息是怎么从 A 发到 B 的”,用原生 Socket 你可以从头讲到尾:ServerSocket 等待连接、Socket 建立通道、InputStream 读字节、OutputStream 写字节,每一环都能在代码里指出来。换成 Netty,要解释 ByteBuf、EventLoop、ChannelPipeline,任何一个环节没吃透都会被追问,反而拉低印象分。记住:毕设项目的本质是“证明你掌握了它”,不是“证明你用了更厉害的工具”。
2.3 界面层选型:Swing 还是 JavaFX
界面层的选择同样影响开发节奏。Swing 是 JDK 自带的 GUI 工具包,资料多、代码直白、任何能运行 Java 的机器都能跑。JavaFX 界面更现代,支持 CSS 美化,但需要额外引入模块,老版本 JDK 环境部署起来麻烦。对局域网聊天室这种工具型界面,Swing 足够:一个 JFrame 装聊天记录区,一个 JTextField 输消息,一个 JButton 发送,再挂一个在线用户列表 JList。如果你毕设想显得精致,用 JavaFX 也不是不行,但要在论文里多写一笔“JavaFX 的 UI 线程模型”,否则界面再好看也只是加分项。
数据持久化也要提前定。聊天记录如果只存内存,服务器一重启就全没了,论文里“数据存储”章节只能写半页。常见做法是引入 SQLite 这种嵌入式数据库,零安装、单文件、JDBC 直接操作。也可以退一步用文本文件追加记录,但要讲清楚“为什么不用 MySQL”——因为局域网聊天室不需要独立数据库服务器,嵌入式存储部署成本更低。这个理由在论文里是加分项,而不是减分项。技术选型的核心原则是:每一个选择都能说出理由,且理由能扛住追问。
3. 系统架构与消息协议:动手写代码前先定这三件事
3.1 客户端-服务器模型:三个角色、两个消息方向
局域网聊天室的标准拓扑是一个服务器 + N 个客户端。服务器是唯一的消息中转站,客户端之间不直接通信。这样做的好处是:在线状态集中管理、消息路由逻辑单一、权限控制好做。你不需要像 P2P 那样处理 NAT 穿透和节点发现,这也是“局域网”这个限定带来的简化。
系统里其实有三个抽象角色:服务器(Server)、客户端会话(ClientHandler)、在线用户表(ConcurrentHashMap)。服务器负责 accept 连接,每个连接进来就创建一个 ClientHandler 跑在独立线程里,ClientHandler 持有一个 Socket 的输入输出流,在线用户表维护“用户名 -> ClientHandler”的映射。消息有两个方向:上行方向是客户端把登录、发言、私聊、退出事件发给服务器;下行方向是服务器把广播消息、私聊消息、在线列表变更推给客户端。
这里有一个容易被忽略的设计点:服务器向客户端推送消息,是服务器主动调用客户端 Socket 的输出流,而不是“客户端定时来拉取”。这意味着服务端必须保存每个客户端对应的输出流引用,写入时还要考虑并发——两个线程同时往同一个 Socket 写数据会交错。后面代码部分会处理这个问题。
3.2 消息协议设计:用 JSON 统一六种消息类型
聊天室本质上是一个消息转发系统,消息长什么样决定了代码好不好写。常见做法是直接约定 JSON 字符串作为通信格式,原因很简单:调试时可以打印原始报文,出了问题一眼就能看出是哪个字段不对,这比自定义二进制协议和手写解析器省太多事。
我一般把消息定义为六个类型:login、chat、private、userlist、logout、heartbeat。统一格式如下:
{ "type": "login", "from": "alice", "to": "all", "content": "", "timestamp": 1700000000000 }各字段含义:
| 字段 | 含义 | 取值规则 |
|---|---|---|
| type | 消息类型 | login / chat / private / userlist / logout / heartbeat |
| from | 发送方用户名 | 登录时确定,之后不可变 |
| to | 接收方 | 群聊固定为 all,私聊为目标用户名 |
| content | 消息内容 | 文本消息正文;登录时可为空 |
| timestamp | 毫秒时间戳 | 客户端生成,服务器不改写 |
login消息的from字段就是注册的用户名,服务器收到后查重并加入在线表。chat消息的to固定是all,服务器拿到后广播给所有在线客户端。private消息的to是目标用户,服务器从在线表里找到对应 ClientHandler 定向转发。userlist是服务器主动下发的在线用户列表,一般用 JSON 数组放在content字段里。logout由客户端在正常退出时发送,服务器收到后清理映射。heartbeat是可选类型,配合断线检测使用。
用 JSON 协议还有一层好处:论文里可以画一张“消息时序图”,把登录、群聊、退出的报文来回画清楚,评委一看就知道你系统设计是完整的。如果后续想扩展文件传输,只需要在协议里加一个type和content的 base64 字段,老消息格式完全不受影响。
3.3 线程模型:每客户端一线程怎么讲才算“懂原理”
局域网聊天室的并发模型有两条路,一条是“每客户端一线程”,另一条是“线程池 + 任务队列”。课设阶段,每客户端一线程最直观:accept()一个连接就new Thread(...).start(),这个线程专门负责读取该客户端的消息。代码好写、逻辑好讲、调试好定位,几十个客户端测下来没有任何压力。
但这条路要能自圆其说,你需要知道它的代价。一个 Java 线程默认栈大小在 512KB 到 1MB 之间,纯内存开销不大,但线程数量多了以后,上下文切换成本会明显上升。LAN 环境下聊天室撑死几十人在线,完全不是问题;但如果场景换成公网千人聊天室,这个模型就扛不住了。标准说法是:每客户端一线程适合“连接数少、单连接吞吐低”的教学场景,生产环境应该用线程池或 NIO 事件驱动。
如果你想让答辩多一个亮点,可以在代码注释里写“此处用每客户端一线程,连接数达到千级时应替换为线程池”,然后口头说一句“线程池可以复用线程,避免频繁创建销毁的系统调用”,就足够体现你考虑过扩展性。实际做法是把new Thread(new ClientHandler(socket)).start()换成executor.execute(new ClientHandler(socket)),其中executor是Executors.newFixedThreadPool(20)。注意线程池大小不要拍脑袋填,一个可行基准是“音频编解码类任务 2 倍 CPU 核数,纯 IO 转发类任务可以 4 倍 CPU 核数,再高就要考虑队列堆积”。
4. 核心代码实现:把最小聊天室跑起来再谈扩展
4.1 服务器主循环与 ClientHandler
服务器端的入口是ServerSocket监听固定端口,每来一个客户端就交给一个独立线程。下面是主循环的标准写法:
public class ChatServer { private static final int PORT = 8888; private static final ConcurrentHashMap<String, ClientHandler> clients = new ConcurrentHashMap<>(); private static volatile boolean running = true; public static void main(String[] args) throws IOException { try (ServerSocket serverSocket = new ServerSocket(PORT)) { System.out.println("服务器已启动,监听端口 " + PORT); while (running) { Socket socket = serverSocket.accept(); // 每个客户端连接交给独立线程处理 new Thread(new ClientHandler(socket)).start(); } } } }注意两个参数:PORT决定监听端口,选 8888 这类大端口不容易和系统常用端口冲突;running是一个volatile标志,用于优雅停机。accept()是阻塞方法,没有客户端连接时线程会一直挂在这里等待。try-with-resources 保证服务器关闭时ServerSocket一定释放端口,否则反复调试时会遇到“端口被占用”的坑。
4.2 在线用户表:为什么要用 ConcurrentHashMap
clients是整个系统的核心数据结构,键是用户名,值是这个用户对应的ClientHandler对象。它会被多个线程同时访问:用户登录时写入、用户退出时删除、广播时遍历。如果用了普通 HashMap,并发写入轻则丢数据,重则造成死循环,所以这里必须用并发容器。
public class ClientHandler implements Runnable { private final Socket socket; private final DataInputStream in; private final DataOutputStream out; private String username; public ClientHandler(Socket socket) throws IOException { this.socket = socket; // 统一使用 UTF-8,后面会解释为什么 this.in = new DataInputStream(new BufferedInputStream(socket.getInputStream())); this.out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())); this.out.flush(); } @Override public void run() { try { String firstMessage = in.readUTF(); this.username = parseUsername(firstMessage); // putIfAbsent:用户名已存在时返回旧值,避免覆盖 ClientHandler old = clients.putIfAbsent(username, this); if (old != null) { sendMessage("{\"type\":\"error\",\"content\":\"用户名已存在\"}"); return; } System.out.println(username + " 上线了,当前在线 " + clients.size() + " 人"); broadcastUserList(); // 主循环:不断读取该客户端的消息 while (true) { String raw = in.readUTF(); handleMessage(raw); } } catch (IOException e) { System.out.println(username + " 连接异常:" + e.getMessage()); } finally { clients.remove(username); broadcastUserList(); closeQuietly(); } } }putIfAbsent是一个值得在答辩时展开的细节。如果直接写put,两个用户同时用同一个昵称登录,后到的会覆盖先到的,先到的用户就再也收不到自己的消息了。putIfAbsent是原子操作,保证“先检查再写入”整个过程不会被其他线程打断。DataInputStream.readUTF()和DataOutputStream.writeUTF()自带长度前缀和 UTF-8 编码,聊天文本场景足够用,这也是我推荐这个组合的原因。
4.3 广播与私聊:快照遍历和消息路由
handleMessage是服务器端最核心的方法,负责把读到的 JSON 消息分发给目标客户端。广播时要小心一点:如果直接遍历clients.values(),遍历过程中正好有用户掉线,finally块里的remove会修改集合。虽然ConcurrentHashMap的迭代器不会直接抛ConcurrentModificationException,但可能读到刚被移除的旧引用,导致消息发给了已经断开的连接。常见做法是先做一次快照再遍历:
private void handleMessage(String raw) throws IOException { JsonObject obj = JsonParser.parseString(raw).getAsJsonObject(); String type = obj.get("type").getAsString(); String from = obj.get("from").getAsString(); String to = obj.get("to").getAsString(); switch (type) { case "chat": { // 快照遍历:把当前在线用户复制到一个新列表 List<ClientHandler> snapshot = new ArrayList<>(clients.values()); for (ClientHandler handler : snapshot) { try { handler.sendMessage(raw); } catch (IOException e) { handler.shutdown(); // 发送失败说明对方连接已断,触发清理 } } break; } case "private": { ClientHandler target = clients.get(to); if (target != null) { target.sendMessage(raw); } else { sendMessage("{\"type\":\"error\",\"content\":\"对方不在线\"}"); } break; } default: break; } }发送方法必须加synchronized,这是多线程并发写同一 Socket 的关键:
public synchronized void sendMessage(String raw) throws IOException { out.writeUTF(raw); out.flush(); }不加synchronized会发生什么?两个线程同时调用out.writeUTF,底层BufferedOutputStream的字节缓冲会被交错写入,客户端读到的消息就会混成一团。这个细节在答辩时属于“线程安全”的高频考点,主动写出来比被问到再解释强得多。
4.4 客户端:Swing 界面与独立接收线程
客户端用 Swing 搭一个最朴素的三块结构:上方聊天记录区JTextArea,左边在线用户JList,下方输入框JTextField和发送按钮JButton。Swing 的一个铁律是:界面组件只能在事件分发线程(EDT)里操作,所以网络接收线程读到消息后,不能直接调用chatArea.append(...),而要交给SwingUtilities.invokeLater排队。
public class ChatClient { private Socket socket; private DataOutputStream out; private JTextArea chatArea; public void connect(String serverIp, int port) throws IOException { socket = new Socket(serverIp, port); out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())); out.flush(); // 独立线程负责接收服务器消息 new Thread(() -> { try (DataInputStream in = new DataInputStream(new BufferedInputStream(socket.getInputStream()))) { while (true) { String raw = in.readUTF(); // 网络线程不能直接改 UI,必须切回 EDT SwingUtilities.invokeLater(() -> { chatArea.append(raw + System.lineSeparator()); chatArea.setCaretPosition(chatArea.getDocument().getLength()); }); } } catch (IOException e) { SwingUtilities.invokeLater(() -> chatArea.append("连接已断开" + System.lineSeparator())); } }).start(); } public void sendMessage(String text) throws IOException { String json = String.format( "{\"type\":\"chat\",\"from\":\"%s\",\"to\":\"all\",\"content\":\"%s\",\"timestamp\":%d}", username, text, System.currentTimeMillis()); out.writeUTF(json); out.flush(); } }serverIp这个参数要单独说:同一台机器测试填127.0.0.1没问题,两台机器联调必须填服务器的局域网 IP,不能用客户端自己的 IP。setCaretPosition是让聊天区自动滚到底部,不然消息一多就停在最上方,这个交互细节容易被忽略但体验提升明显。
4.5 心跳与断线检测:让服务器感知“悄悄消失”的客户端
聊天室最容易被忽视的是异常断线。用户正常点窗口右上角关闭时,JVM 退出会关闭 Socket,服务器端readUTF()会抛出EOFException,这是可感知的。但如果客户端进程被强制结束、电脑休眠、网线被拔,TCP 连接不会立刻通知服务器,服务器线程会一直阻塞在readUTF()上,在线用户列表里就留下了一个“僵尸用户”。
标准解法是设置读取超时 + 心跳包配合。服务器给 Socket 设置SO_TIMEOUT,客户端定期发送heartbeat消息,服务器把收到任意消息的时间记录下来。一旦超过阈值没收到消息,就判定为失联并清理:
// 服务器端设置读超时 socket.setSoTimeout(30_000); volatile long lastReceiveTime = System.currentTimeMillis(); // 在 run() 的读循环里处理超时 try { String raw = in.readUTF(); lastReceiveTime = System.currentTimeMillis(); handleMessage(raw); } catch (SocketTimeoutException e) { // 30 秒没数据,检查是否超过宽限期限 if (System.currentTimeMillis() - lastReceiveTime > 60_000) { System.out.println(username + " 心跳超时,强制下线"); break; } }两个时间参数分别代表SO_TIMEOUT和“失联阈值”。30 秒是一个折中值:太小会误杀正常不说话的用户,太大则僵尸用户存活太久。客户端侧对应写一个ScheduledExecutorService,每 20 秒发一条heartbeat消息,服务器收到后在循环里更新时间戳即可。心跳机制属于“加分项中的核心项”,论文里写“通过应用层心跳感知半开连接”,这句话能直接提升系统设计的评价档次。
5. 避坑指南:联调时最容易翻车的 5 个场景
5.1 连不上服务器:先 ping、再 telnet、后查防火墙
现象:客户端一启动就抛java.net.ConnectException: Connection refused,或者干脆SocketTimeoutException。新手第一反应是代码有问题,其实十有八九是网络层面没通。
排查顺序应该是:先在服务器上执行ipconfig确认本机 IP,然后在客户端机器上ping 服务器IP确认二层连通。通了之后用telnet 服务器IP 8888测试端口是否开放,如果提示连接失败,多半是服务器进程没起来或者端口被占用。Windows 下查看端口占用用netstat -ano | findstr 8888,Linux 用netstat -tunlp | grep 8888。最后再检查防火墙:Windows 第一次运行 Java 进程时,系统一般会弹出防火墙授权框,如果点了“取消”,就需要去防火墙入站规则里手动放行 8888 端口。还有一种很容易忽略的情况是同一 WiFi 下两台设备 IP 不在同一网段,比如一台 192.168.1.8、一台 192.168.2.5,路由器开启了 AP 隔离,两边虽然连同一个路由器但互相不可见——这时 ping 都不通,问题就不在代码而在网络配置。
5.2 客户端强制断网,服务器还留着“僵尸用户”
现象:服务器在线用户列表一直显示某个昵称,但这个人早就没了。正常的logout消息只覆盖了“优雅退出”路径,无法应对进程被强制结束或网络瞬断。原因就是前面说过的 TCP 半开连接——网线断开瞬间,服务器不会收到任何 FIN 包,readUTF()永远阻塞在等待数据状态。
解决:按 4.5 节设置SO_TIMEOUT并实现心跳。这里有一条经验:不要只发logout后立刻System.exit(0),要给服务器 200 毫秒的收包时间,否则可能在消息到达前连接就被操作系统回收了。Swing 客户端里可以重写windowClosing事件,先发 logout 再关闭:
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { try { out.writeUTF("{\"type\":\"logout\",\"from\":\"" + username + "\"}"); out.flush(); } catch (IOException ex) { // 连接已断,忽略 } finally { System.exit(0); } } });DO_NOTHING_ON_CLOSE是这里的关键参数,默认的EXIT_ON_CLOSE会直接杀掉进程,不给发送 logout 的机会。
5.3 广播时抛 ConcurrentModificationException
现象:服务器运行一会儿后,某个用户下线时控制台突然刷出java.util.ConcurrentModificationException,之后所有客户端都收不到新消息。原因基本是用了ArrayList保存客户端集合,广播线程正在for (ClientHandler h : list)遍历时,另一个线程在remove掉线用户,两个操作发生在同一个 ArrayList 上,直接踩了快速失败迭代器的雷。
解决:换ConcurrentHashMap是第一步,但遍历时最好再做一次快照。把for (ClientHandler h : clients.values())改成for (ClientHandler h : new ArrayList<>(clients.values()))。快照遍历的意义在于:整个广播过程基于一个稳定的集合,不会因为并发修改读到“只删了一半”的中间状态。这属于“一次写对,答辩不慌”的写法。
5.4 中文乱码:GBK 与 UTF-8 的经典拉扯
现象:同一台 Windows 机器上跑服务器和客户端,发中文一切正常;换成 Mac 或 Linux 客户端连 Windows 服务器,消息全部变成乱码。原因:Java 的String在内存里是 Unicode,但通过网络传输时必须编码成字节流。Windows 中文系统默认文件编码是 GBK,如果代码里没有显式指定字符集,new OutputStreamWriter(socket.getOutputStream())就会用系统默认编码,两个平台编码不一致,解码自然出错。
解决:所有涉及字节流转字符流的地方,显式指定StandardCharsets.UTF_8。如果用DataInputStream/DataOutputStream,则readUTF/writeUTF内部实现是修改版 UTF-8,跨平台没问题。另外在启动参数里加一行-Dfile.encoding=UTF-8,可以把 IDE 和命令行环境钉死在 UTF-8 上。判断乱码方向有一个技巧:如果汉字变成一排问号,是写入时编码不对;如果变成“䏿–‡”这种拉丁字符,是读取时解码不对。
5.5 Swing 界面假死:网络线程改了 UI 组件的后果
现象:程序能收发消息,但聊天窗口拖不动、点发送没反应、最小化后无法还原,整个界面像死了一样。原因:网络接收线程直接调用了chatArea.append(...)、userList.setListData(...)这类 UI 更新方法,而 Swing 的几乎所有组件都必须在 EDT 线程里操作。跨线程更新 UI,轻则界面不刷新,重则造成 EDT 死锁。
解决:所有从网络线程往界面推送内容的操作,一律包进SwingUtilities.invokeLater(() -> {...})。注意invokeLater是异步的,调用完立即返回;如果后续逻辑依赖界面更新完成,需要换invokeAndWait,但不要在invokeAndWait里做耗时操作,否则 EDT 和网络线程互相等待,照样卡死。调试这类问题还有一个笨办法:在可疑的 UI 更新方法里打印Thread.currentThread().getName(),Swing 事件线程名字通常包含 “AWT-EventQueue”,如果日志里是 “Thread-N”,说明跑错线程了。
6. 论文写作与答辩演示:把代码变成一套能自圆其说的交付物
6.1 从代码反向梳理论文章节
不少人的写作顺序是“先写代码,最后熬夜凑论文”,结果论文写出来像流水账。反过来做会更顺:写完代码后,按“需求分析 → 系统设计 → 系统实现 → 系统测试”的顺序,把代码里已经存在的决策点翻译成文字。比如ConcurrentHashMap的选择对应“并发用户管理设计”,JSON 六种消息类型对应“通信协议设计”,心跳机制对应“异常恢复设计”。论文里放的关键代码片段不要整段贴,挑核心的 20 行并逐行解释即可,评委想看的是“你理解这段代码”,不是“你会复制代码”。
6.2 测试用例表:用一张表堵住“运行正常”的万能扣分句
系统测试章节最忌讳只写“经测试,系统运行正常”。至少准备 8 条测试用例,每条用例有操作步骤和预期结果,例如:
| 用例编号 | 功能点 | 操作步骤 | 预期结果 | 实际结果 |
|---|---|---|---|---|
| TC-01 | 用户登录 | 启动两个客户端,分别用 alice 和 bob 登录 | 双方在线列表都能看到两人 | 通过 |
| TC-02 | 重名处理 | alice 在线时,第三个客户端用 alice 登录 | 服务器拒绝登录并提示“用户名已存在” | 通过 |
| TC-03 | 中文消息 | alice 发送“你好”,bob 接收 | 内容无乱码 | 通过 |
| TC-04 | 异常断线 | 强制结束 bob 的客户端进程,等待 60 秒 | alice 的在线列表中 bob 消失 | 通过 |
“异常断线”这条测试用例是加分项,因为大多数课设只测了正常退出。写测试记录时顺带把线程日志粘一张截图,这比自言自语“没测出 BUG”有说服力得多。
6.3 答辩演示的三个现场准备
第一,准备两台电脑,或者一台电脑启动两个客户端,演示“多用户并发收发”。第二,提前把服务器和客户端打成可执行 JAR 包,演示时用java -jar启动,不要打开 IDE 现点运行,IDE 编译卡住或弹窗会当场扣分。第三,主动演示一次“强制结束客户端进程,服务器用户列表 60 秒内自动移除”,这个场景能直接呼应论文里的心跳机制,比被动等评委提问强得多。
我的个人习惯是:提交前用两个客户端来回发 300 条消息,观察乱码和卡顿,再强制结束一个客户端进程,确认服务器在 60 秒内把它踢出在线列表,才敢打包提交。这套流程不是什么玄学,就是把你系统里最容易出问题的三条链路,在答辩前亲手压一遍。希望帮到你。
本文还有配套的精品资源,点击获取