简介一份基于Java与DES加密算法的即时通信聊天系统毕业设计源码包面向计算机相关专业学生、指导教师及Java进阶学习者适用于毕业设计、课程设计、项目初期演示等场景也是初学者上手Java网络编程的实用样例。资源内含完整项目工程、部署文档及全部数据资料其中部署文档包含环境准备、项目导入与运行说明代码经过测试运行成功已获导师认可答辩评审分达95分。压缩包共69个文件大小约1020KB文件类型涵盖Java源码、编译后的class文件、Eclipse工程配置、说明文档、截图及配置文件等能够覆盖从源码阅读、环境配置到运行验证的完整闭环。项目分为服务端与客户端两个工程模块采用C/S结构便于理解Socket通信、登录校验、消息转发以及DES加解密链路代码结构清晰可在此基础上做二次功能扩展。已有115人学习下载适合需要完整高分参考案例的毕业生和开发者使用。1. 基于JavaDES加密的即时通信聊天系统毕设题目里的技术点到底在解决什么问题如果你正在为毕业设计选题发愁又不想做那种“增删改查管理系统”凑数那“基于JavaDES加密的即时通信聊天系统”这个题属于性价比很高的那一类。它表面是个局域网聊天软件实际把Java网络编程Socket、多线程并发、对称加密DES、数据库持久化这四门核心课一次性串了起来放到简历上或答辩PPT里每一个模块都能讲出深度。这套系统解决的核心问题是两个客户端之间如何安全地互发消息。所谓“安全”就是消息在网络上传输时不能是明文——万一有人在局域网里抓包聊天内容直接裸奔。而DES加密负责把明文变成密文保证即使被截获对方看到的也是一堆乱码。整个题目的价值正在于“业务虽然简单但链路是完整的”用户要登录认证、消息要转发路由、离线消息要落库、传输过程要加密。这篇内容我们直接从架构拆到代码再拆到部署把整个源码包里的东西讲明白。2. 架构选型与数据库设计Socket 长连接、私有消息协议和两张核心表拿到这个源码包第一件事不是急着双击运行而是先弄清楚它的整体架构。聊天系统最核心的矛盾是服务端怎么知道谁在线客户端发来的消息该转给哪个连接数据又该存到哪里这一章就把这三件事的地基打牢。2.1 为什么即时通信要选 C/S 架构而不是 Web 聊天室常见的做法里很多同学第一反应是做Web版聊天室用浏览器当客户端。但Web方案要么用HTTP短轮询模拟实时服务端压力大而且消息延迟高要么上WebSocket前后端代码量翻一倍对于毕设来说进度完全不可控。这套系统采用经典的C/S客户端/服务器架构用JDK自带的ServerSocket和Socket就能实现不需要引入任何第三方框架。服务端监听一个固定端口每个客户端通过TCP长连接连上来连接一旦建立就一直保持消息随时可以双向推送。客户端下线时连接断开服务端自然能感知到——这种“长连接天然维持在线状态”的特性正是即时通信选C/S最直接的理由。理论学习时最容易忽略的一点是一个服务端要同时服务多个客户端所以每个连接必须交给独立的线程去处理。这就自然引出了多线程编程也是这个题目的核心考点之一。我一般建议用线程池ExecutorService管理这些连接线程既避免无限制创建线程把资源耗尽也方便控制最大连接数。2.2 私有消息协议给每类消息一个身份标识客户端与服务端之间传输的是加过密的密文但密文解密之后服务端总得知道这条消息是“登录请求”“聊天消息”还是“退出通知”所以必须定义一套消息协议。这套系统用的是最直接的口令格式用#分隔各个字段第一个字段表示消息类型。消息类型格式说明LOGINLOGIN#用户名#密码客户端登录认证SENDSEND#发送者#接收者#内容单聊消息BROADCASTBROADCAST#发送者#内容群发消息LOGOUTLOGOUT#用户名主动退出实际处理时有个细节必须注意解析消息要用split(#, 4)而不是split(#)。因为聊天内容本身很可能包含#字符如果直接按所有#切割SEND#zhangsan#lisi#晚上吃#啥这条消息就会被切成5段内容字段被截断。限定切割次数后第4段往后全被当成消息内容不会丢数据。这套系统之所以不用JSON格式是刻意为之。引入JSON库意味着要多打一个第三方jar包对毕设来说增加了部署成本。用#分隔符配合split是原生JDK能力逻辑简单、便于答辩时现场讲解。等到后面系统规模变大、字段变多时再引入JSON或Protobuf也不迟那是演进方向不是毕设阶段必须做的事。2.3 数据库设计用户表、聊天记录表与离线消息数据资料里的SQL脚本通常包含用户表和聊天记录表。用户表存账号密码聊天记录表既存实时消息也承担离线消息的暂存功能。CREATE DATABASE chatdb DEFAULT CHARACTER SET utf8mb4; USE chatdb; -- 用户表 CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 聊天记录表 CREATE TABLE t_message ( id INT AUTO_INCREMENT PRIMARY KEY, sender VARCHAR(32) NOT NULL, receiver VARCHAR(32) NOT NULL, content TEXT NOT NULL, send_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 );这里的status字段是关键设计。当发送者发消息给一个不在线的用户时服务端把消息插入t_message表status保持默认值0等接收者登录上线后服务端查出所有receiver 用户名 AND status 0的记录逐条转发给客户端转发成功后将status更新为1。这样“离线消息”功能就完成了不需要额外建一张表。数据库连接层面源码包里一般会有一个DbUtil工具类统一管理JDBC的连接参数和获取方式。使用MySQL时URL里要显式声明characterEncodingutf8和serverTimezoneAsia/Shanghai前者解决中文乱码后者解决MySQL 8.0以上的时区报错。2.4 源码包里的目录结构长什么样这类项目的源码包解压后结构通常是这个样子拿到手先对照着找入口chat-system/ ├── src/ │ ├── com/chat/common/ # 公共类DesUtil加解密、Constants常量 │ ├── com/chat/server/ # 服务端ChatServer、ClientHandler、DbUtil │ └── com/chat/client/ # 客户端LoginFrame、ChatFrame、ClientCore ├── sql/ │ └── chatdb.sql # 数据库初始化脚本 ├── docs/ │ └── 部署文档.md # 环境要求、启动步骤、常见问题 └── README.md # 项目说明common包放客户端和服务端都要用的类比如加解密工具类server包放服务端程序职责是监听端口、处理连接、转发消息client包放客户端界面和网络逻辑。分层清楚之后排查问题时思路就清晰界面出问题找client转发出问题找server加解密出问题找common不要一上来就乱翻代码。3. 把 DES 加密接进聊天消息密钥、工具类、Base64 与三个关键细节DES加密是这个题目最大的亮点也是答辩时最容易拿分的地方。它核心就一句话发送端把明文变成密文接收端把密文还原成明文中间传输的一律是别人看不懂的乱码。但真正写代码的时候密钥长度、填充模式、编码方式、加密层级每个细节都有坑。3.1 DES 算法在聊天系统里怎么定位DES是经典的对称加密算法所谓“对称”是指加密和解密用同一把密钥。它的特点很明确密钥长度8个字节实际有效位是56位加密时将明文按8字节一组分组处理支持ECB、CBC等多种工作模式默认填充方式是PKCS5Padding。选择DES而不是AES并不是因为DES更安全恰恰相反DES在今天的安全性已经不够看了暴力破解在硬件加速下并非难事。但毕业设计选它有三个实际理由第一JDK原生支持不需要引任何第三方加密库第二代码量少十几行就能封装完方便现场讲解第三答辩时你可以顺着“DES不安全→为什么不用AES→对称加密与非对称加密的区别”这条线展开展示自己的知识面。课程设计真正考察的是你懂不懂这套机制而不是你用了多新的算法。有一点必须提醒真正部署到生产环境时DES已经不适合作为主要加密手段至少要用AES-128以上。这一点在毕业设计文档里最好主动写一笔显得你清楚技术的边界。3.2 DesUtil一行行封装加解密工具类加密解密的封装代码不长但每一行都有讲究。以下这个工具类就是这套系统里common包的核心import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class DesUtil { // 密钥必须是8字节这里用16字符字符串取前8个字节 private static final String KEY chat2024; /** * 加密明文 - 密文(Base64编码) */ public static String encrypt(String plainText) throws Exception { // 1. 创建Cipher对象指定算法DES Cipher cipher Cipher.getInstance(DES); // 2. 构造密钥规范密钥字节数组 算法名 SecretKeySpec keySpec new SecretKeySpec(KEY.getBytes(UTF-8), DES); // 3. 初始化加密模式 cipher.init(Cipher.ENCRYPT_MODE, keySpec); // 4. 执行加密得到密文字节数组 byte[] encrypted cipher.doFinal(plainText.getBytes(UTF-8)); // 5. Base64编码方便作为字符串在Socket中传输 return Base64.getEncoder().encodeToString(encrypted); } /** * 解密密文(Base64编码) - 明文 */ public static String decrypt(String cipherText) throws Exception { Cipher cipher Cipher.getInstance(DES); SecretKeySpec keySpec new SecretKeySpec(KEY.getBytes(UTF-8), DES); cipher.init(Cipher.DECRYPT_MODE, keySpec); // Base64解码还原成密文字节数组再执行解密 byte[] decrypted cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(decrypted, UTF-8); } }这段代码里有三个参数必须理解透。第一KEY的长度必须是8个字节“chat2024”刚好8个字符、UTF-8编码下是8个字节多一个少一个都会在cipher.init时直接抛InvalidKeyException。第二Cipher.getInstance(DES)使用的是默认的ECB模式和PKCS5Padding填充如果要把模式写显式一点可以写成Cipher.getInstance(DES/ECB/PKCS5Padding)效果一样但语义更清楚。第三加密后得到的是二进制字节数组里面可能包含不可见字符甚至0x00不能直接当字符串放上Socket所以必须用Base64编码转成可见的ASCII字符解密时再Base64解码还原。3.3 密钥怎么管常量密钥还是动态协商这个项目最简方案是把密钥写死在DesUtil里像上面的代码那样。好处显而易见实现简单、客户端服务端天然一致、部署省事。坏处也很明显密钥写死在代码里反编译就能看到加密形同虚设。但对于教学项目这是在可接受范围内的因为考察重点在于“你会不会用加密机制”而不在于“你的密钥保管得多严密”。如果想让项目多一个加分项我建议把密钥从代码里抽出来放到config.properties配置文件里代码通过ResourceBundle或Properties加载Properties props new Properties(); props.load(new FileInputStream(config.properties)); String key props.getProperty(des.key);这样做的好处是部署时不改代码只改配置客户端和服务端各自维护一份相同的配置。这也是实际工程里常见的做法在答辩时能讲出“配置与代码分离”的理念。再进一步还有动态协商方案服务端启动时用KeyGenerator随机生成一个DES密钥客户端登录成功后向服务端请求密钥。但这个方案有一个绕不开的问题——密钥在网络上传输时本身是明文谁抓包谁就拿到了密钥等于白加密。要解决就得引入RSA非对称加密来保护密钥传输复杂度会直线上升。这条路线对毕设来说有些过重我建议作为答辩时的“未来展望”提一句即可不用真做。3.4 加解密放在哪一层数据流的四个节点加密不是只加在“发送”这一步就完了整个链路上要自洽。这套系统的数据流是这样走的客户端A输入明文 →DesUtil.encrypt()得到Base64密文 → 通过DataOutputStream.writeUTF()发送 → 服务端readUTF()收到密文 →DesUtil.decrypt()还原明文 → 解析消息类型做业务处理 → 转发给客户端B前再次encrypt()→ 客户端BreadUTF()收密文 →decrypt()还原明文 → 显示到聊天界面。这里最容易被忽视的是服务端也要参与加解密。有些同学图省事让服务端只做“透明转发”客户端A加密后直接原样发给客户端B服务端根本不解密。这样确实少写几行代码但服务端完全不知道消息内容无法做敏感词过滤、无法离线落库、也无法进行消息审计。更严重的是如果客户端A和客户端B使用的密钥不一致B会直接解密失败。还有几个容易翻车的地方。第一不要在日志或控制台打印解密前的密文更不要打印解密后的明文至少在演示时要克制。第二Base64编码后的密文里可能出现、/、这些特殊字符如果中间环节用URL传参要再做一层URL编码否则会被解析成空格导致解密失败。这个题目里消息走Socket传输不存在URL编码问题但如果你后续做成Web版这就是个必踩的坑。4. 服务端与客户端核心实现登录、消息路由、离线缓存与双线程收发前面把协议、数据库、加密都准备齐了这一章把它们组装成一个能跑通的系统。服务的启动入口、在线用户的维护、消息转发逻辑、客户端的收发线程每一块都是必考必问的内容。4.1 服务端启动与线程模型一个 accept 循环加一个线程池服务端启动入口一般长这样public class ChatServer { private ServerSocket server; // 线程池避免无限创建线程 private ExecutorService pool Executors.newFixedThreadPool(16); // 在线用户表用户名 - 客户端连接处理器 private ConcurrentHashMapString, ClientHandler onlineUsers new ConcurrentHashMap(); private static final int PORT 8888; public void start() throws IOException { server new ServerSocket(PORT); System.out.println(聊天服务已启动端口: PORT); while (true) { Socket socket server.accept(); System.out.println(新客户端接入: socket.getInetAddress()); // 每个连接交给线程池中的线程处理 ClientHandler handler new ClientHandler(socket, this); pool.execute(handler); } } public static void main(String[] args) throws IOException { new ChatServer().start(); } }这里有两个关键点。第一个是ServerSocket(PORT)默认监听所有网卡地址也就是说局域网内其他机器都能通过服务端的IP连接不需要额外配置“监听0.0.0.0”。第二个是accept()是阻塞方法每来一个连接就返回一个Socket这个Socket后续的读写操作如果放在主线程里后面的客户端就永远连不进来所以必须交给独立线程。用Executors.newFixedThreadPool(16)限制最大同时处理16个客户端超过的请求会在队列里等待。4.2 登录与在线用户表用一张 Map 维持会话状态每个连接进来后ClientHandler就是客户端与服务端的桥梁。它的第一个任务就是处理登录登录成功后把自己注册到在线用户表里。public class ClientHandler implements Runnable { private Socket socket; private ChatServer server; private String username; private boolean running true; public ClientHandler(Socket socket, ChatServer server) { this.socket socket; this.server server; } Override public void run() { try (DataInputStream in new DataInputStream(socket.getInputStream()); DataOutputStream out new DataOutputStream(socket.getOutputStream())) { // 第一条消息必须是登录请求 String loginCipher in.readUTF(); String loginPlain DesUtil.decrypt(loginCipher); String[] loginParts loginPlain.split(#, 3); username loginParts[1]; // 简化处理真实项目中密码需要校验 server.getOnlineUsers().put(username, this); System.out.println(用户 username 登录成功当前在线: server.getOnlineUsers().size()); // 登录成功后拉取离线消息 pullOfflineMessages(); // 循环读取后续所有消息 while (running) { String cipherText in.readUTF(); String plainText DesUtil.decrypt(cipherText); System.out.println(收到 username 的消息: plainText); routeMessage(plainText); } } catch (EOFException e) { System.out.println(用户 username 连接断开); } catch (Exception e) { e.printStackTrace(); } finally { server.getOnlineUsers().remove(username); System.out.println(用户 username 已下线剩余在线: server.getOnlineUsers().size()); } } }ConcurrentHashMap在这里是必选项而不是可选项。聊天场景下多个客户端线程可能同时往这个Map里放数据如果使用普通的HashMap多线程写入时可能造成死循环甚至CPU飙满。ConcurrentHashMap的并发安全机制保证了对它的读写操作不会破坏内部数据结构。4.3 消息路由与离线落库在线转发离线存库routeMessage是服务端的大脑根据消息类型决定下一步动作private void routeMessage(String plainText) throws Exception { String[] parts plainText.split(#, 4); String type parts[0]; if (SEND.equals(type)) { String from parts[1]; String to parts[2]; String content parts[3]; // 1. 先落库无论对方在不在线都要保存聊天记录 saveMessage(from, to, content); // 2. 对方在线则直接转发密文 ClientHandler target server.getOnlineUsers().get(to); if (target ! null) { String cipher DesUtil.encrypt(plainText); target.send(cipher); } else { // 对方不在线消息已经落库status0上线后自动拉取 System.out.println(用户 to 不在线消息已保存为离线消息); } } else if (LOGOUT.equals(type)) { running false; } } private void saveMessage(String from, String to, String content) { String sql INSERT INTO t_message(sender, receiver, content) VALUES(?, ?, ?); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, from); ps.setString(2, to); ps.setString(3, content); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }这个消息路由逻辑里藏着一个容易搞错的设计决策先落库再判断对方是否在线。有些同学会把“在线判断”放在前面在线就直接转发、不管存储结果聊天记录缺失答辩时被老师一问就露馅。正确的业务语义是“所有消息都应该被持久化离线消息只是持久化之后多一步延迟转发”。拉取离线消息的代码对应前面提到的status字段登录成功后执行查询private void pullOfflineMessages() throws Exception { String sql SELECT id, sender, content FROM t_message WHERE receiver ? AND status 0; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { long id rs.getLong(id); String sender rs.getString(sender); String content rs.getString(content); String msg SEND# sender # username # content; send(DesUtil.encrypt(msg)); // 逐条补发 markRead(id); // 标记已读 } } } }4.4 客户端接收线程和发送线程必须分开客户端最容易写错的地方是收发不分家。很多新手客户端代码只有一条执行线程先send再readUTF结果消息发完就卡死在等待服务器回包上。正确做法是发送走主线程界面线程接收在单独线程里持续监听。public class ClientCore { private Socket socket; private DataOutputStream out; private DataInputStream in; private ChatFrame frame; // 聊天界面 public void connect(String host, int port) throws IOException { socket new Socket(host, port); in new DataInputStream(socket.getInputStream()); out new DataOutputStream(socket.getOutputStream()); // 启动接收线程持续读取服务端转发来的消息 new Thread(this::receiveLoop).start(); } public void sendMessage(String plainText) throws Exception { out.writeUTF(DesUtil.encrypt(plainText)); out.flush(); } private void receiveLoop() { try { while (true) { String cipherText in.readUTF(); String plainText DesUtil.decrypt(cipherText); // 更新界面必须回到Swing事件线程 SwingUtilities.invokeLater(() - frame.appendMessage(plainText)); } } catch (EOFException e) { System.out.println(与服务器断开连接); } catch (Exception e) { e.printStackTrace(); } } }writeUTF和readUTF是这里的一个隐含红利它们自带2字节的长度前缀在Socket传输时能够自动区分消息边界彻底避免TCP粘包导致的多条消息混在一起。这一点在下一章还会细讲。另外接收线程里更新Swing界面时用了SwingUtilities.invokeLater这是Swing单线程模型的硬性要求非UI线程不允许直接操作界面组件否则轻则界面闪烁重则直接抛异常。5. 部署文档之外的避坑清单局域网联调、粘包、乱码与连接泄漏部署文档通常只写“环境要求JDK 1.8、MySQL 5.7步骤先建库再启动服务端最后启动客户端”但真实运行的时候十个有八个会在这几类问题上翻车。这一章列的都是我见过的高频故障每一条都按现象、原因、解决三段来说。5.1 服务端能启动、本机连不上先查这三处现象服务端控制台打出了“聊天服务已启动”但客户端连接时报ConnectException: Connection refused。原因一并列出来第一服务端启动时实际上根本没监听成功比如8888端口已被系统其他进程占用此时ServerSocket构造方法直接抛BindException但有些工程把异常吞掉了表面看像启动了第二Windows防火墙拦住了Java进程的入站连接本机测试时往往能通换成局域网另一台机器就连不上第三客户端连接时误把 host 填成localhost而服务端跑在另一台IP为192.168.x.x的机器上两端不在同一个网段。解决先在本机执行netstat -ano | findstr 8888确认端口没被占再在服务端执行ping 客户端IP确认网络通最后在Windows防火墙的“允许应用通过防火墙”里把java.exe勾上或者临时关掉防火墙测试一下一般问题就定位了。5.2 解密报 BadPaddingException密钥不一致的典型现场现象某条消息发过去后接收端控制台抛出javax.crypto.BadPaddingException: Given final block not properly padded或者更直接一点解密后是一串乱码。原因绝大多数情况下是通信双方的密钥不一致。比如服务端DesUtil里的KEY写的是chat2024客户端写的是chat2023双方用不同的密钥对同一段密文解密结果必然是填充校验失败。还有一种可能是密钥长度根本不是8字节比如用了chat5个字节SecretKeySpec虽然不会在构造时报错但底层处理时会因为密钥位长不足而行为异常。解决把DesUtil里的密钥常量加上注释标明“客户端和服务端必须完全一致”同时在整个src目录下全局搜索KEY 确认所有副本的值是同一个。给密钥统一加上前后空格也容易出问题比如 chat2024 和chat2024肉眼几乎看不出区别却会让解密彻底失败。5.3 消息一多就“串台”或解密失败字节流长度边界问题现象连续快速发送多条消息接收端偶尔丢消息或者解密后内容混在一起比如收到两条不同聊天对象的半截话拼成了一条。原因TCP是流式协议消息在传输层没有天然的“一条消息”边界。如果用getInputStream().read(byte[])手工读字节数组服务端并不知道这次读到的数据是正好一条消息、半条消息还是两条消息粘在一起。这种“粘包/拆包”问题是网络编程的经典大坑。解决这套系统里最省事的方案就是用DataOutputStream.writeUTF()/DataInputStream.readUTF()这对组合。writeUTF会先写入2字节的长度前缀readUTF读出2字节长度后再读对应数量的字节天然解决了粘包问题。不要在writeUTF之外再手工叠加一层长度头那样反而画蛇添足。需要注意的一点是writeUTF写入的字符串不能超过65535字节对聊天消息来说完全不构成瓶颈。5.4 客户端直接点 X 关掉服务端不会立刻知道现象客户端直接关闭窗口服务端在线用户表里这个用户还留着列表显示在线但消息发过去石沉大海。过一会儿服务端控制台才出现SocketException: Connection reset的异常堆栈。原因TCP协议规定了一方关闭连接时应该发送FIN包走完四次挥手流程但用户直接杀进程或断网时FIN包发不出来服务端这边的读操作就一直阻塞在readUTF()上感知不到对端已经没了。只有等到下一次尝试发送数据时内核才会通过 RST 包通知服务端“这条连接已经断了”。解决在ClientHandler的run()方法里同时捕获EOFException和SocketException前者对应正常关闭后者对应异常断开两种情况都执行onlineUsers.remove(username)清理在线状态。更主动的解法是心跳机制客户端每隔30秒发一个HEARTBEAT消息服务端超过90秒没收到某个客户端的任何数据就主动判定对方掉线。这个机制我在第6章展开。5.5 数据库连接泄漏聊天聊得正欢服务器突然拒绝连接现象系统跑了几小时后服务端控制台开始刷SQLException: Too many connections。原因DbUtil.getConnection()每次调用都通过DriverManager新建一个物理数据库连接如果调用方没有在finally块里关闭连接就永远泄漏在MySQL那边。聊天场景下每发一条消息就插入一条记录连接泄漏速度极快。还有一种隐藏问题直接用字符串拼接SQL把消息内容塞进去一旦内容里带单引号就会拼出语法错误。解决所有数据库操作统一用try-with-resources语法Connection、PreparedStatement、ResultSet都会在代码块结束时自动关闭包括异常路径不用再写繁琐的finally块。同时SQL中的条件值和插入值一律用PreparedStatement的setString绑定既能防注入又能省去手动拼接的转义麻烦。6. 把优秀项目再做高一档抓包验证 DES 加密效果与三个演进方向项目跑通了加密也加上了但“加密真的生效了吗”这个问题很多人答不上来。文字层面看不出加密效果得用抓包工具看实际传输的内容来验证。这一章给出一套验证方法再补几个不破坏现有架构的升级方向。6.1 Wireshark 抓包验证密文真的上了“线”步骤很简单。先在运行客户端的机器上打开Wireshark选择对应的网卡本机测试选Loopback: lo局域网测试选以太网或WLAN在过滤栏输入tcp.port 8888然后回到聊天窗口发一条消息比如“你好这是测试”。回到Wireshark找到对应报文右键选择“追踪流 → TCP流”面板里显示的应该是类似SBBP1PmXNkLHXbJHS29OjMi7OhY4k/这样的Base64字符而不是“你好这是测试”的明文。看到这个结果说明加密链路确实生效了。如果TCP流里直接能看到中文或英文明文回去检查ClientCore.sendMessage()是不是漏掉了DesUtil.encrypt()这一步再检查发送端代码里加密后是不是又拼了解密后的明文一起发送。6.2 按性价比排序的三个演进方向升级方向工作内容答辩价值配置化改造端口、密钥、数据库连接信息抽到 config.properties低代码量体现工程意识心跳机制客户端定时发HEARTBEAT服务端超时清理解决真实掉线问题讲起来有说服力消息确认ACK接收端收到消息后回执ACK发送端超时重发完整闭环复杂度可控我一般建议至少做配置化改造和心跳机制这两个。配置化改造半小时就能完成但部署时不用再打开源码改常量这份“可维护性”在答辩时值不少分。6.3 我的经验习惯接手任何一份源码包我习惯先把跑通放在第一位确认功能完整后再做修改而不是上来就重构。每改完一个模块至少做一遍“异常路径冒烟测试”关掉服务端再发消息、连续快速发长文本、用两个不同密钥的客户端互连这些场景能暴露出比功能本身更多的问题。密钥和端口这类配置我现在一律放到配置文件里这个习惯就是从这类练手项目里养成的。希望帮到你。本文还有配套的精品资源点击获取