简介基于Java的局域网聊天室系统包含完整源代码与配套毕业论文专为计算机相关专业毕业设计、课程设计场景准备。系统覆盖客户端/服务端通信、用户登录、消息群发、用户列表维护等典型功能模块可直接运行调试也便于二次扩展。压缩包共239个文件主要由C/C头文件h、源程序cpp、编译生成文件obj、工程配置文件dsp/dsw以及文档doc/txt组成另含可执行程序与少量音频资源整体约14.13MB目录结构清晰模块划分明确便于按模块查阅与修改。已有106人学习下载适合需要快速搭建课题原型并完成论文写作的学生参考。通过阅读源码与论文可掌握Socket网络编程、多线程处理、界面交互等关键知识点还能借鉴其需求分析、总体设计与测试部分的写作思路省去从零摸索的精力。1. 为什么选局域网聊天室当毕业设计一个能落地也能写论文的经典选题“JAVA基于局域网的聊天室系统”这题看起来像初级入门作业但它把 Java 开发里最经典的三个基础点全串了起来网络通信、多线程、Swing 界面编程。我在收课程设计时见过太多“能跑通”的版本——连上就能聊一旦断线重连、快速连发消息、多人同时登录就出现转发遗漏、界面卡死、服务端连接数只增不减。这些问题的源头都集中在没处理 TCP 的流式边界和线程安全。这篇笔记从消息协议设计讲到避坑排查把论文里需要的测试思路也整理出来让你拿到源码能看懂、能改、能应付答辩追问。适合正在选 Java 课题的学生也想把局域网聊天室系统做成有含金量的项目、而不是只会演示的同学。2. 从需求到架构模块划分、消息协议与线程模型聊天室系统的核心问题是“多个人在同一网络里互发消息”。最直接的方案是两台机器点对点直连但这只支持一对一聊天一旦三个人以上在线就必须引入服务器作为消息中转站。服务器负责三件事接受新连接、维护在线用户列表、把消息分发给对应的人。客户端则只做两件事把用户输入发给服务器把服务器转来的消息显示到界面上。这个不对称结构是绝大多数局域网聊天室系统的标准做法。原因有两个一是代码量可控服务端逻辑集中在连接管理与转发客户端逻辑集中在 Swing 界面与消息收发二是论文好写天然的 C/S 架构图画出来清晰评委提问时你可以直接指着图讲职责划分。下面拆成三个包来落地。2.1 模块划分把系统拆成 server、client、common 三个包我一般会把项目拆成三个顶层包server存放服务端入口和客户端连接处理器client存放 Swing 界面与收发线程common放消息对象、消息类型枚举和常量。这个拆法不是随便分的它对应着 Java 面向对象编程里的“接口隔离”思想——两个端都依赖 common但不互相依赖以后想扩展文件传输、群组管理只需要在 common 里加消息类型在 server 和 client 里各自加逻辑。src/ ├── common/ │ ├── Message.java │ └── MessageType.java ├── server/ │ ├── ServerMain.java │ └── ClientHandler.java └── client/ ├── ChatClient.java ├── ChatFrame.java └── ReceiverThread.javacommon包是整棵依赖树的叶子节点任何一端不能反向依赖另一端。这样划分还有个实际好处打包和部署时服务器只需要 server common客户端只需要 client common界面和逻辑可以分开调试。论文里的“系统总体设计”章节也能直接复用这张结构图不用重画。2.2 消息协议为什么用对象流而不是裸字符串聊天室传输的数据无非是“谁发的、发给谁、什么内容”。很多源码喜欢用BufferedWriter写字符串接收端用readLine()一行行读。这个方案在本地演示没问题但只要消息内容里带换行或者两条消息连续发送接收端读到的内容就会错位。原因在于 TCP 是流式协议本身没有消息边界readLine()是靠换行符切分消息的换行符一旦被内容污染整条解析就断了。更稳的做法是自定义一个可序列化消息对象用ObjectOutputStream/ObjectInputStream传输。一条消息通过writeObject()写出时框架会自动带上对象边界接收端每次readObject()读到的就是完整一条粘包和半包问题从根上消失。// common/Message.java —— 所有消息的统一载体 public class Message implements Serializable { private MessageType type; // 消息类型登录、群聊、私聊、心跳、系统通知 private String from; // 发送者用户名 private String to; // 接收者用户名群聊时为空 private String content; // 文本内容 private long timestamp; // 发送时间用于论文里的时延统计 public Message(MessageType type, String from, String content) { this.type type; this.from from; this.content content; this.timestamp System.currentTimeMillis(); } // 私聊专用构造函数 public Message(MessageType type, String from, String content, String to) { this(type, from, content); this.to to; } // getter / setter 省略实际代码里需要补齐 }这里的MessageType枚举决定了整个系统能扩展出多少功能别只定义一两个。我在做这个项目时至少会定义LOGIN、LOGOUT、GROUP_CHAT、PRIVATE_CHAT、HEARTBEAT、SYSTEM六种类型后面私聊、心跳检测、上下线通知都靠它区分。参数上需要注意两点第一Message必须实现Serializable接口否则对象流直接抛NotSerializableException第二Message类中的字段尽量都用字符串和枚举这类简单类型避免嵌套复杂对象导致序列化体积膨胀局域网内虽然带宽不是瓶颈但序列化越简单出问题的概率越低。2.3 线程模型每连接一线程在课程设计里够用ServerSocket.accept()是阻塞方法一次只返回一个连接。如果服务器在主线程里做完“接受连接”再去“处理消息”第二个客户端就永远连不上。所以服务器必须采用“一个客户端对应一个线程”的模型——主线程只负责接受连接每拿到一个Socket就新建一个处理线程各线程互不阻塞。有人会建议上 NIO 或 Netty 来提升并发性能。我的观点是课程设计和毕业论文阶段几十个客户端以内的规模“每连接一线程”是性价比最高的方案。它逻辑简单、答辩容易解释而且 Java 线程在普通笔记本上撑起一两百个连接没有压力。NIO 的Selector模型适合几千连接的长连接场景放在聊天室里属于杀鸡用牛刀还会让论文答辩时被追问“为什么不用更简单方案”的风险变大。3. 把聊天室跑起来服务端与客户端的最小可运行代码这一章的目标是让你照着写完就能在局域网内把消息发起来。我按“服务器先启动 → 客户端后连接”的顺序来写。建议把 common 包先写好再写服务端最后写客户端这样调试时每写一层都能编译验证。3.1 服务端ServerSocket 监听与连接管理的最小代码服务端主类只做一件事创建ServerSocket循环接受连接每个连接交给一个ClientHandler线程。设计要点是把客户端列表用CopyOnWriteArrayList存起来因为广播消息时要遍历列表而客户端随时可能加进来或掉线这个集合能避免遍历时修改导致的ConcurrentModificationException。// server/ServerMain.java —— 聊天室服务端入口 public class ServerMain { private static final int PORT 8899; // 用线程安全的集合保存所有在线客户端处理器 private static final ListClientHandler clients new CopyOnWriteArrayList(); public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(PORT); System.out.println(聊天室服务已启动监听端口 PORT); while (true) { Socket socket serverSocket.accept(); // 阻塞等待新客户端 ClientHandler handler new ClientHandler(socket); clients.add(handler); // 先登记再启动线程 handler.start(); } } // 向所有在线客户端广播消息 public static void broadcast(Message msg) { for (ClientHandler handler : clients) { try { handler.send(msg); } catch (IOException e) { // 单个客户端异常不应该影响其他人正常收消息 System.out.println(广播失败 handler.getUsername()); } } } }PORT选择上有一个隐藏注意事项端口号不要小于 1024那些端口在 Unix/Linux 系统上需要 root 权限才能绑定Windows 上也可能被系统服务占用。我一般用 8899、9999 这类没被常用软件占用的端口。accept()的阻塞性质决定了主线程不能做任何耗时操作把连接处理丢给子线程正是为了释放主线程。ClientHandler是服务端的核心类它持有客户端的输入流和输出流在run()里循环读取消息再按消息类型决定处理逻辑。// server/ClientHandler.java —— 每个在线客户端对应一个处理线程 public class ClientHandler extends Thread { private final Socket socket; private final ObjectInputStream in; private final ObjectOutputStream out; private String username; public ClientHandler(Socket socket) throws IOException { this.socket socket; // 注意必须先创建输出流再创建输入流避免双方互相等待 this.out new ObjectOutputStream(socket.getOutputStream()); this.in new ObjectInputStream(socket.getInputStream()); } Override public void run() { try { while (true) { Message msg (Message) in.readObject(); if (msg.getType() MessageType.LOGIN) { this.username msg.getFrom(); ServerMain.broadcast(new Message( MessageType.SYSTEM, SERVER, username 加入了聊天室)); } else if (msg.getType() MessageType.GROUP_CHAT) { ServerMain.broadcast(msg); } else if (msg.getType() MessageType.PRIVATE_CHAT) { // 私聊逻辑在第4章展开 } } } catch (Exception e) { System.out.println(客户端连接异常 username); } finally { ServerMain.clients.remove(this); ServerMain.broadcast(new Message( MessageType.SYSTEM, SERVER, username 已离开聊天室)); closeQuietly(); } } public void send(Message msg) throws IOException { out.writeObject(msg); out.flush(); // 不 flush消息可能留在缓冲区 } }注意两个细节。第一构造方法里ObjectOutputStream必须先于ObjectInputStream创建这是 TCP 对象流通信里一个常见的死锁点双方都等对方先发流头信息结果互相卡住。第二send()里必须调flush()writeObject只是把对象写进内存缓冲区不flush就可能在网卡里滞留表现为“对方收不到消息但没报错”。3.2 客户端Swing 界面与 Socket 初始化客户端的第一步是建立与服务器的连接把 IP、端口、用户名准备好。这里有一个容易忽略的点很多人直接用new Socket(ip, port)这个方法虽然有超时机制但默认超时时间可能长达几十秒如果服务器没启动界面会卡在那里一动不动。我一般让构造函数只创建未连接的Socket再用connect()显式指定超时时间。// client/ChatClient.java —— 客户端连接与消息发送 public class ChatClient { private Socket socket; private ObjectOutputStream out; private String username; public void connect(String serverIp, int port, String name) throws IOException { this.username name; socket new Socket(); // 3秒连不上就直接报错不拖住界面 socket.connect(new InetSocketAddress(serverIp, port), 3000); out new ObjectOutputStream(socket.getOutputStream()); // 登录消息必须先发服务端靠它记录用户名 out.writeObject(new Message(MessageType.LOGIN, username, )); out.flush(); // 启动独立线程接收消息不能让网络读取阻塞UI线程 new ReceiverThread(socket, this).start(); } public void sendGroupChat(String content) { try { out.writeObject(new Message(MessageType.GROUP_CHAT, username, content)); out.flush(); } catch (IOException e) { System.out.println(消息发送失败连接可能已断开); } } }参数说明connect的第三个参数 3000 是连接超时毫秒数在局域网内通常 100ms 内就能建立连接设 3000 是为了容忍部分设备响应慢。socket.connect()成功后才创建ObjectOutputStream避免在未完成连接的 Socket 上做 I/O。登录消息必须走flush()因为服务端readObject()会阻塞等待第一条消息如果你不 flush双方会一直卡在登录握手阶段。Swing 界面部分的代码量不大核心是一个JTextArea显示聊天记录、一个JTextField输入消息、一个“发送”按钮触发发送动作。这里要注意JTextArea默认不支持自动换行要设置setLineWrap(true)否则长消息显示会超出界面范围。3.3 收发线程与 Swing 刷新避免界面假死的关键写法客户端的消息接收必须放在独立线程里因为readObject()是阻塞的放在界面线程上会导致窗口无法拖动、按钮点不动。但独立线程里也不能直接操作 Swing 组件Swing 不是线程安全的所有组件更新必须在事件调度线程EDT上执行。// client/ReceiverThread.java —— 独立接收线程 public class ReceiverThread extends Thread { private final Socket socket; private final ChatFrame frame; // 主窗口引用 public ReceiverThread(Socket socket, ChatFrame frame) { this.socket socket; this.frame frame; } Override public void run() { try (ObjectInputStream in new ObjectInputStream(socket.getInputStream())) { while (true) { Message msg (Message) in.readObject(); // 把UI更新操作丢给事件调度线程去执行 SwingUtilities.invokeLater(() - frame.appendMessage(msg)); } } catch (IOException | ClassNotFoundException e) { SwingUtilities.invokeLater(() - frame.showDisconnected()); } } }这里最关键的一行是SwingUtilities.invokeLater(...)。它把一个更新界面的任务提交到事件队列中由 EDT 按顺序执行保证不会与按钮点击、窗口绘制产生竞争。如果图省事直接调用frame.appendMessage(msg)短期看没问题一旦消息量大或界面操作频繁就会出现界面闪烁、消息顺序错乱甚至直接死锁。补充一点接收线程里不要手动持有一个ObjectInputStream的“引用变量”然后到处传直接在接收线程内部创建即可因为消息读取只在接收线程里发生。try-with-resources写法保证连接断开时输入流被自动关闭不用再单独处理资源释放。4. 加分项与参数调优在线列表、私聊与心跳参数怎么定到这里一个能群聊的聊天室已经能跑起来了。但课程设计和毕业设计的评分通常看重功能完整度光能发消息属于及格水平。在线用户列表、私聊、掉线检测、历史消息这四项是拉开差距的关键。这一章讲前三个以及它们背后的参数选择。4.1 在线用户列表什么时候用全量推送什么时候用增量推送在线列表的实现有两种方式一种是任何用户上线、下线服务器都把完整列表广播给所有人另一种是只把“谁上线了”“谁下线了”这种变更事件广播出去由客户端自行增删列表项。全量推送的逻辑简单客户端收到列表消息后直接整体刷新JList不存在状态不一致。但问题是聊天室 20 个人时每次更新要发 20 条完整列表体感不明显如果作为扩展方向做群组分组全量推送的流量会指数增长。增量推送只推变化省流量但客户端必须保证自己维护的列表和服务端一致一旦漏处理一条消息列表就永远多一个幽灵用户。我的建议是课程设计阶段用全量推送但把列表封装成独立的消息类型而不是混在普通聊天消息里。// server 广播在线列表 —— 全量推送 public static void broadcastUserList() { ListString names new ArrayList(); for (ClientHandler handler : clients) { names.add(handler.getUsername()); } Message listMsg new Message(MessageType.USER_LIST, SERVER, ); listMsg.setUserList(names); // 复用 Message 的扩展字段 broadcast(listMsg); }在客户端收到USER_LIST消息时用DefaultListModel整体重构列表即可。这样做的好处是论文里能清楚写出“客户端状态由服务端全量同步保证一致”避免在增量更新的一致性问题上给自己挖坑。4.2 私聊与状态消息消息类型的扩展点私聊不需要动服务端架构只需要把消息的to字段利用起来。服务端收到PRIVATE_CHAT消息后根据to字段找到目标用户的ClientHandler只转发给那一个连接而不是广播给所有人。实现上要给ClientHandler加一个getUsername()方法再在ServerMain里加一个按用户名查找处理器的函数。查找时注意用户名相同的情况——最简单的方式是在LOGIN时检查用户名是否已存在存在就拒绝新连接或者强制旧连接下线保证用户名唯一。// 服务端处理私聊消息 Message msg (Message) in.readObject(); if (msg.getType() MessageType.PRIVATE_CHAT) { ClientHandler target ServerMain.getHandlerByUsername(msg.getTo()); if (target ! null) { target.send(msg); // 发给目标用户 this.send(msg); // 同时回发给发送者界面显示“我” } else { this.send(new Message(MessageType.SYSTEM, SERVER, 用户 msg.getTo() 不在线)); } }这里的消息对象不需要新类复用Message的to字段就够了。状态消息上线、下线则利用SYSTEM类型加SERVER为发送者客户端收到SYSTEM类型时用不同颜色显示。这些细节在论文的功能模块设计里都是加分项因为体现了“用协议区分消息语义”的设计思想。4.3 心跳与断线重连间隔、次数与退避参数TCP 连接断开时服务器不会立刻感知尤其是客户端断电、拔网线这种非正常断开场景。如果不加心跳机制服务器会一直把消息转发给一条已经不通的连接广播方法虽然不会报错但用户看到的表象是“某某明明下线了还在列表里”。解决办法是双方约定一个心跳协议客户端每隔固定时间发一个HEARTBEAT包服务器连续 N 次没收到就判定离线。心跳参数的选择直接决定体验参数推荐值说明心跳间隔15 秒局域网内延迟极低15 秒足够短也不会产生明显流量连续丢失次数3 次超过 45 秒无心跳则认为掉线容忍单次网络抖动超时时间5 秒客户端发送时设置SO_TIMEOUT避免读阻塞卡死重连间隔1.5 秒客户端掉线后按固定间隔尝试重连不搞复杂退避客户端的心跳发送独立成一个线程逻辑简单得不能再简单循环里发一个心跳包sleep一个间隔失败就退出循环。// 心跳线程每15秒发一次ping class HeartbeatThread extends Thread { private final ObjectOutputStream out; public void run() { while (!isInterrupted()) { try { out.writeObject(new Message(MessageType.HEARTBEAT, username, ping)); out.flush(); Thread.sleep(15000); } catch (Exception e) { break; // 连接断开结束心跳线程 } } } }服务端记录每个客户端的“最近心跳时间”由一个定时任务每 10 秒扫一次超过 45 秒没有更新的客户端被清理掉。这个机制就是心跳包重传的简化版原理发送方定期重传状态接收方以“连续性”判断存活理解透了面试和论文答辩时都能把这一套讲清楚。重连参数不建议做指数退避聊天室的用户操作频率不高固定 1.5 秒间隔既不会造成服务器压力又能让用户感觉到“它自己恢复了”。指数退避适合消息推送类应用放在课程设计里反而让代码复杂、演示麻烦。5. 从运行到答辩五个让聊天室系统翻车的常见问题与排查思路代码能跑通和系统真正可靠之间隔着一条巨大的沟。我带过不少课程设计小组统计下来最容易翻车的问题集中在下面五个。每一条都按“现象 → 原因 → 解决”的顺序写方便你带着症状对号入座。5.1 服务器能启动但客户端连不上不只是防火墙一个原因现象服务器端控制台打印了“服务已启动”但客户端连接时报ConnectException: Connection refused或直接超时。原因分三种第一服务器绑定了127.0.0.1而不是0.0.0.0。很多教程代码写的是new ServerSocket(8899)这在某些系统上默认只监听本机回环地址局域网其他机器无法访问第二Windows 防火墙拦截了入站连接第三客户端填写的服务器 IP 是192.168.x.x但两台机器实际上不在同一个子网。解决服务端显式绑定所有网卡接口用new ServerSocket(8899, 50, InetAddress.getByName(0.0.0.0))。防火墙问题在 Windows 上是“允许应用通过防火墙”或临时关闭防火墙验证Linux 上则要查iptables。排查顺序建议是先在本机用telnet 127.0.0.1 8899验证服务端正常再用另一台机器ping服务器 IP最后在另一台机器上telnet 服务器IP 8899能通再排查客户端代码。5.2 界面假死在 Socket 线程里直接操作了 Swing 组件现象窗口能打开但一旦收到消息整个窗口就转圈按钮点不动关不掉。原因在ReceiverThread.run()里直接调用了frame.appendMessage()。Swing 组件不是线程安全的网络线程频繁更新界面时与 EDT 发生竞争轻则界面卡顿重则死锁。解决把界面更新包一层SwingUtilities.invokeLater()让更新动作排队交给 EDT 执行。如果消息量非常大还可以用javax.swing.Timer做批量刷新比如 200ms 内累积的消息合并成一次追加减少频繁重绘。课程设计阶段用invokeLater就够了批量刷新属于锦上添花。5.3 消息连在一起或尾部丢失读写的流类型不一样现象客户端连续发两条消息服务器收到第一条的末尾拼着第二条的开头或者发送后能收到部分内容但结尾缺失。原因这是读写两端流类型不匹配导致的。服务器用BufferedReader.readLine()读文本客户端却用ObjectOutputStream.writeObject()写对象或者发送端每发一条消息就new一个ObjectOutputStream导致流头信息重复写入接收端解析错乱。解决统一读写模型。要么全部用ObjectOutputStream/ObjectInputStream走“一条消息一个对象”的路子要么全部用文本流 换行符约定并小心处理消息内容里的换行。混用两种流模型是最容易出 bug 的操作没有之一。代码里检查一下两端的in和out类型是否对应通常问题立刻就暴露了。5.4 客户端退出后服务器连接数只增不减缺了清理机制现象客户端点了“退出”但服务器端列表里的用户名还在其他用户仍然能看到它时间久了连接数越来越多。原因客户端直接System.exit(0)没发LOGOUT消息或者run()方法里没有finally清理逻辑。TCP 连接虽然会因 socket 关闭而关闭但服务器端的ClientHandler对象还留在列表里。解决两处必须同时做。客户端退出时先发LOGOUT消息再关闭 socket服务端在run()的finally块里必须执行ServerMain.clients.remove(this)同时广播下线消息。如果前面说得简单点finally就是你的后悔药断线、异常、主动退出都要走这个出口保证列表没有幽灵用户。5.5 中文乱码与问号字符集和字体在两端的隐性差异现象服务器是 Linux客户端是 Windows收到的中文变成一串问号或者乱码。原因对象流传输字符串默认会用 UTF-8 编码如果某个环节手动用DataOutputStream.writeUTF()或混杂了String.getBytes()的默认平台字符集两端解析就不一致。另外 Swing 界面字体不支持中文时显示成方块或问号其实是界面字体问题不是网络问题。解决网络传输层面坚持用ObjectOutputStream传Message对象字符串编码统一交给 JDK 处理不要手工做getBytes()。界面层面给JTextArea设置一个明确支持中文的字体比如new Font(Dialog, Font.PLAIN, 14)或直接指定“微软雅黑”。排查时先看服务器控制台打印是否正常服务端正常就查客户端字体服务端乱码就查是不是有手工编码逻辑。6. 答辩前的验证三板斧并发脚本、异常场景与测试表格最后这一步是把系统从“能用”变成“可证明能用”的关键。答辩时没有机会让你“演示一遍正常流程就完事”评委一定会问系统的并发能力、异常处理能力和测试依据。所以这一章是给论文和答辩准备素材的。6.1 用并发脚本验证转发能力写一个模拟客户端程序不启动 Swing 界面直接用 Socket 连服务器开 30 个线程模拟 30 个用户同时上线每个用户隔 2 秒发一条消息持续 60 秒统计每个客户端收到多少条。// 并发压测30个模拟客户端同时在线 public class PressTest { public static void main(String[] args) throws Exception { for (int i 0; i 30; i) { final String name user i; new Thread(() - runClient(name)).start(); } } private static void runClient(String name) { try { Socket socket new Socket(); socket.connect(new InetSocketAddress(192.168.1.100, 8899), 3000); ObjectOutputStream out new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream in new ObjectInputStream(socket.getInputStream()); out.writeObject(new Message(MessageType.LOGIN, name, )); out.flush(); for (int i 0; i 30; i) { out.writeObject(new Message(MessageType.GROUP_CHAT, name, msg i)); out.flush(); Message resp (Message) in.readObject(); // 统计逻辑记录收到的消息数量 Thread.sleep(2000); } } catch (Exception e) { System.out.println(name 异常 e.getMessage()); } } }我的习惯是跑两轮第一轮 30 个客户端看有没有异常崩溃第二轮 50 个客户端看消息到达率。如果 50 个客户端、每个发 30 条、总共 1500 条消息到达率 100%就可以在论文测试表里写“系统在 50 并发下无消息丢失”。6.2 异常场景测试断网、强退、服务器重启并发测试证明的是“正常情况下的能力”异常场景证明“边界情况不崩溃”。一张表格能覆盖大部分测试要求场景操作预期表现客户端断电直接关闭客户端机器电源服务器 45 秒内检测掉线列表移除该用户服务器重启服务端进程 kill 后重新启动客户端收到连接断开提示自动重连成功快速登录退出同一账号反复登录退出服务器不报错旧连接被正确清理弱网丢包拔掉网线 5 秒后插回心跳超时触发恢复后自动重连重名登录第二个同名用户尝试登录服务器拒绝或踢出旧连接逻辑明确这五项在论文“系统测试”章节里各写一段每段包括“测试步骤、测试结果、是否符合预期”。我在这一章的测试环节吃过一次亏只做了正常流程测试没测掉线重连答辩时老师现场拔网线系统直接崩了。后来每次提交前这五项是必跑清单。6.3 把“心跳 重连”作为论文的创新点从技术含量来看心跳机制和断线重连是最能在论文里写出深度的模块。把参数、异常处理、状态转换图放进去再配合测试表里的异常场景结果结论就是“系统具备基础容错能力”。这部分不要写成“系统实现了聊天功能”这种流水账要写成“系统通过心跳检测和自动重连机制解决了局域网环境下的断线识别问题”角度完全不一样。最后说一点我的习惯每次改完代码先开 3 个客户端窗口互相发消息验证基本功能再做一遍异常场景清单最后跑一次并发脚本。这套流程固定下来之后系统基本没有在答辩现场翻过车。希望这篇笔记能帮到你少走一点我当年走过的弯路。本文还有配套的精品资源点击获取