简介:这是一份面向计算机相关专业在校学生与开发者的Java即时通信聊天系统毕业设计资源,核心亮点在于引入DES加密机制保障消息传输安全,适合作为毕设、课程设计或项目立项演示的参考方案。压缩包共69个文件,约1020KB,以class编译文件与java源码为主体,另含classpath、project工程配置、jar依赖、keyfile密钥文件及png界面截图、md说明文档等,目录中可见客户端与服务端分离的工程结构,便于理解C/S通信架构与加密流程。目前已有117人学习关注。项目已通过导师评审,答辩评分达95分,代码经测试可正常运行,读者可据此掌握Socket通信、DES加解密、多端消息收发等关键实现,并在此基础上修改扩展功能,快速完成自己的毕设或课设任务。
1. 从一份毕设源码说起:Java+DES 的即时通信聊天系统到底能跑出什么
很多同学拿到「基于 Java+DES 加密的即时通信聊天系统」这类毕设题目时,第一反应是去搜现成源码,下载下来发现能跑,但一问加密怎么做的、消息怎么保证不被篡改、Socket 长连接怎么维持,就答不上来了。这篇笔记就围绕这个标题,把一套可复现的即时通信聊天系统从架构、加密、通信到部署完整拆一遍,重点讲清楚 DES 在这个系统里到底扮演什么角色、参数怎么设、哪些地方最容易翻车。适合正在做 Java 课程设计、毕设,或者想用 Java 从零搭一个带加密通信的聊天系统的同学。整套方案的核心链路是:客户端 Socket 连接服务端,消息体先做 DES 对称加密再走网络传输,服务端解密后路由转发给目标客户端,同时用多线程处理并发连接。下面按「先立住原理、再动手复现」的顺序展开,每一步都给出可抄的代码和参数说明。
2. 系统架构与 DES 加密选型:为什么毕设场景下它够用
2.1 即时通信系统的三层结构怎么拆
一套能跑通的 Java 聊天系统,最小可用架构分三层:客户端 UI 层、通信层、服务端转发层。客户端负责采集用户输入、加密消息、发送和接收;通信层基于 TCP Socket 维持长连接;服务端负责维护在线用户列表、解密消息、按目标用户 ID 转发。
常见做法是用ServerSocket监听固定端口,每个客户端接入后服务端开一个独立线程处理该连接的读写。客户端用Socket连接服务端,输入流和输出流分别对应接收和发送。用户上线时先发一条注册消息(携带用户名),服务端把它存进一个ConcurrentHashMap<String, ClientHandler>,key 是用户名,value 是该连接的处理器。后续发消息时,服务端根据消息里的目标用户名找到对应 handler,把加密后的消息体转发过去。
这里有个关键设计点:加密和解密放在哪一层。我一般把 DES 加解密放在通信层,也就是消息序列化之后、写入输出流之前做加密,读取输入流之后、反序列化之前做解密。这样业务层拿到的永远是明文对象,加密对业务透明,后期换算法也不用动业务代码。
为什么不用 HTTP 短连接?即时通信的核心诉求是「实时推送」,短连接需要客户端不断轮询,延迟高、服务端压力大。TCP 长连接一次握手后持续复用,服务端可以主动推消息,这才是聊天系统该有的样子。毕设场景下并发量不大,一台机器跑一个服务端足够撑几十上百个连接。
2.2 DES 对称加密在聊天系统里的定位
DES(Data Encryption Standard)是一种对称分组加密算法,分组长度 64 位(8 字节),密钥长度 56 位(实际传入 8 字节,其中每字节第 8 位是奇偶校验位)。加密和解密用同一把密钥,所以叫对称加密。
在聊天系统里,DES 解决的是「消息在网络上明文传输被截获」的问题。如果不加密,任何人用抓包工具都能看到聊天内容;加了 DES 之后,抓到的是一串密文,没有密钥解不开。对于毕设来说,DES 的价值在于:算法经典、Java 标准库javax.crypto直接支持、代码量少、容易讲清楚原理,答辩时能说清加密模式和填充方式就够用了。
但必须说清楚 DES 的边界:56 位密钥在现代算力下已经不安全,暴力破解成本很低。所以真实生产环境不会用 DES,而是用 AES。毕设用 DES 没问题,但你要知道它的局限,答辩时如果能主动说出「DES 密钥太短,生产环境应换 AES-128 或 AES-256」,反而是加分项。
选型上还有一个决策:用 ECB 还是 CBC 模式。ECB(电子密码本)模式对每个 8 字节分组独立加密,相同的明文块产生相同的密文块,会泄露数据模式,安全性差。CBC(密码分组链接)模式引入一个初始化向量 IV,每个明文块先和前一个密文块异或再加密,相同明文产生不同密文,安全性更好。毕设里我建议用 CBC,代码只多几行,但答辩时能体现你懂加密模式的区别。
2.3 用 Java 标准库实现 DES 加解密的完整代码
下面这段代码是整套系统的加密核心,直接放在客户端的DESUtil类里,服务端复用同一个类。密钥和 IV 必须客户端服务端一致,否则解出来是乱码。
import javax.crypto.Cipher; import javax.crypto.spec.DESKeySpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.SecretKeyFactory; import javax.crypto.SecretKey; import java.util.Base64; public class DESUtil { // 密钥必须8字节,DES要求56位有效密钥 private static final String KEY = "12345678"; // CBC模式需要8字节初始化向量 private static final String IV = "abcdefgh"; // 加密:明文 -> Base64密文 public static String encrypt(String plainText) throws Exception { DESKeySpec keySpec = new DESKeySpec(KEY.getBytes("UTF-8")); SecretKeyFactory factory = SecretKeyFactory.getInstance("DES"); SecretKey secretKey = factory.generateSecret(keySpec); // 指定 DES/CBC/PKCS5Padding:算法/模式/填充 Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes("UTF-8")); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8")); // Base64编码,避免二进制数据在网络传输中出问题 return Base64.getEncoder().encodeToString(encrypted); } // 解密:Base64密文 -> 明文 public static String decrypt(String cipherText) throws Exception { DESKeySpec keySpec = new DESKeySpec(KEY.getBytes("UTF-8")); SecretKeyFactory factory = SecretKeyFactory.getInstance("DES"); SecretKey secretKey = factory.generateSecret(keySpec); Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes("UTF-8")); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); byte[] decrypted = Base64.getDecoder().decode(cipherText); return new String(cipher.doFinal(decrypted), "UTF-8"); } }逻辑说明:DESKeySpec把 8 字节字符串转成 DES 密钥规格,SecretKeyFactory生成实际密钥对象。Cipher.getInstance("DES/CBC/PKCS5Padding")这一行最关键,三个部分分别是算法、工作模式、填充方式,三者必须加解密两端完全一致,否则报BadPaddingException。
参数说明:KEY必须是 8 字节,少于 8 字节会抛InvalidKeyException,多于 8 字节只有前 8 字节生效。IV在 CBC 模式下必须是 8 字节,ECB 模式不需要 IV。PKCS5Padding负责把不足 8 字节的明文补齐到分组长度,解密时自动去掉填充。Base64 编码不是加密的一部分,是为了让二进制密文能安全地放进字符串协议里传输。
提示:密钥和 IV 硬编码在代码里只适合毕设演示。真实项目应该从配置文件或环境变量读取,且每个会话用不同 IV。
3. Socket 通信与多线程并发:把加密消息真正发出去
3.1 服务端监听与客户端接入的最小实现
服务端启动流程:创建ServerSocket绑定端口,死循环accept()等待客户端连接,每接入一个连接就交给线程池处理。这里用ExecutorService而不是每个连接 new 一个 Thread,避免连接数暴涨时线程耗尽。
import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.*; public class ChatServer { private static final int PORT = 8888; // 固定大小线程池,毕设场景50个并发连接够用 private static final ExecutorService POOL = Executors.newFixedThreadPool(50); // 在线用户表,key用户名 value该连接的处理器 public static final ConcurrentHashMap<String, ClientHandler> ONLINE = new ConcurrentHashMap<>(); public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(PORT); System.out.println("聊天服务端启动,监听端口:" + PORT); while (true) { // accept阻塞,直到有客户端接入 Socket socket = serverSocket.accept(); System.out.println("新连接接入:" + socket.getRemoteSocketAddress()); POOL.execute(new ClientHandler(socket)); } } }逻辑说明:ServerSocket只负责监听和接受连接,真正的读写交给ClientHandler。ONLINE用ConcurrentHashMap是因为多个线程会同时读写这张表,普通HashMap在并发下会出问题。
参数说明:PORT选 1024 以上避免权限问题,8888 是常见测试端口。线程池大小按预期并发数设,每个连接占一个线程,50 个线程对应 50 个同时在线用户。如果连接数可能超过几百,应该换成 NIO 或 Netty,但毕设用阻塞 IO 加线程池足够。
3.2 ClientHandler 如何解密消息并转发
ClientHandler是服务端的核心,它实现Runnable,在run()里循环读取客户端发来的消息,解密后解析协议,再转发给目标用户。
import java.io.*; import java.net.Socket; public class ClientHandler implements Runnable { private final Socket socket; private BufferedReader in; private PrintWriter out; private String username; public ClientHandler(Socket socket) { this.socket = socket; } @Override public void run() { try { in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true); // 第一条消息约定为注册消息,格式:LOGIN|用户名 String first = in.readLine(); if (first != null && first.startsWith("LOGIN|")) { this.username = first.substring(6); ChatServer.ONLINE.put(username, this); System.out.println(username + " 上线"); } String line; while ((line = in.readLine()) != null) { // 消息格式:MSG|目标用户|DES密文 String[] parts = line.split("\\|", 3); if (parts.length == 3 && "MSG".equals(parts[0])) { String target = parts[1]; String cipherText = parts[2]; // 服务端解密验证(可选),再转发密文给目标 String plain = DESUtil.decrypt(cipherText); System.out.println(username + " -> " + target + " : " + plain); ClientHandler targetHandler = ChatServer.ONLINE.get(target); if (targetHandler != null) { // 转发原始密文,目标客户端自己解密 targetHandler.send("MSG|" + username + "|" + cipherText); } } } } catch (Exception e) { System.out.println(username + " 连接异常:" + e.getMessage()); } finally { if (username != null) { ChatServer.ONLINE.remove(username); } try { socket.close(); } catch (IOException ignored) {} } } // 向本连接对应的客户端发送一行数据 public void send(String msg) { out.println(msg); } }逻辑说明:注册消息用LOGIN|用户名格式,服务端解析后把当前 handler 存进在线表。聊天消息用MSG|目标|密文格式,服务端解密后打印日志(方便调试),然后把原始密文转发给目标客户端。目标客户端收到后用同样的密钥解密。
参数说明:split("\\|", 3)限制分割成 3 段,因为密文里可能包含|字符(Base64 编码后一般不含,但保险起见限制段数)。PrintWriter的第二个参数true表示自动 flush,否则消息会卡在缓冲区发不出去,这是新手最常踩的坑之一。
3.3 客户端发送与接收的双线程模型
客户端需要同时做两件事:读用户键盘输入并发送,以及接收服务端转发来的消息。单线程做不到,必须开两个线程。主线程负责读键盘,另开一个线程专门接收。
import java.io.*; import java.net.Socket; import java.util.Scanner; public class ChatClient { public static void main(String[] args) throws Exception { Socket socket = new Socket("127.0.0.1", 8888); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true); Scanner scanner = new Scanner(System.in); System.out.print("输入用户名:"); String username = scanner.nextLine(); // 发送注册消息 out.println("LOGIN|" + username); // 接收线程:持续读取服务端转发来的消息 new Thread(() -> { try { String line; while ((line = in.readLine()) != null) { String[] parts = line.split("\\|", 3); if (parts.length == 3 && "MSG".equals(parts[0])) { String from = parts[1]; String plain = DESUtil.decrypt(parts[2]); System.out.println("[" + from + "] " + plain); } } } catch (Exception e) { System.out.println("接收线程结束:" + e.getMessage()); } }).start(); // 主线程:读键盘输入,加密后发送 while (true) { System.out.print("发给谁:"); String target = scanner.nextLine(); System.out.print("内容:"); String content = scanner.nextLine(); String cipher = DESUtil.encrypt(content); out.println("MSG|" + target + "|" + cipher); } } }逻辑说明:接收线程在后台持续阻塞读,一旦有消息就解密打印。主线程负责交互,把用户输入的内容加密后按协议格式发出。两个线程共享同一个Socket的输入输出流,一个只读一个只写,互不干扰。
参数说明:new Socket("127.0.0.1", 8888)里的 IP 和端口必须和服务端一致,跨机测试时改成服务端实际 IP。out.println自动加换行符,服务端readLine按行读取,两端协议必须对齐,否则会一直阻塞。
4. 避坑与排查:DES 聊天系统最容易翻车的 5 个地方
4.1 现象:解密报 BadPaddingException,原因:密钥或 IV 不一致
这是最高频的翻车点。客户端加密用的密钥和服务端解密用的密钥只要有一个字节不同,解密就会抛javax.crypto.BadPaddingException: Given final block not properly padded。IV 同理,CBC 模式下 IV 不一致也会报这个错。
解决:把密钥和 IV 抽到一个公共常量类里,客户端服务端引用同一个类,杜绝手抄不一致。如果还是报错,先单独写一个 main 方法测试encrypt再decrypt能否还原,排除加解密本身的问题,再排查网络传输是否改动了密文。
4.2 现象:消息发出去对方收不到,原因:PrintWriter 没开自动 flush
new PrintWriter(outputStream)默认不自动刷新,消息写进缓冲区后不会立即发出,对方readLine一直阻塞。这个坑很隐蔽,因为本地测试时缓冲区可能因为写满而被动刷新,看起来时好时坏。
解决:构造PrintWriter时第二个参数传true,或者每次println后手动调flush()。我一般直接用new PrintWriter(new OutputStreamWriter(os, "UTF-8"), true),一劳永逸。
4.3 现象:中文消息解密后变乱码,原因:字符集不统一
加密前getBytes()不指定字符集,默认用平台编码,Windows 下是 GBK,Linux 下是 UTF-8,跨平台就乱码。解密后new String()不指定字符集同理。
解决:所有getBytes和new String都显式指定"UTF-8",包括InputStreamReader和OutputStreamWriter的构造。上面代码里每一处都带了"UTF-8",照抄就不会出问题。
4.4 现象:多个客户端同时发消息,服务端在线表数据错乱,原因:用了非并发容器
如果在线用户表用普通HashMap,多个ClientHandler线程同时put和get会触发并发修改,轻则数据丢失,重则死循环(JDK7 的 HashMap 扩容死链问题)。
解决:用ConcurrentHashMap,它的put和get是线程安全的,且读操作不加锁,性能足够。如果还需要遍历在线用户,用ConcurrentHashMap的forEach而不是先转成List再遍历。
4.5 现象:客户端关闭后服务端线程不释放,原因:没在 finally 里清理资源
客户端异常断开时,如果服务端run()方法没有finally块清理ONLINE表和关闭 socket,这个用户名会一直占着,别人无法用同名登录,线程也不会回收,连接数多了服务端就卡死。
解决:把清理逻辑放finally,确保无论正常退出还是异常退出都执行。同时给 socket 设置setSoTimeout,避免读操作永久阻塞导致线程无法退出。
5. 进阶技巧:把 DES 换成 AES 并加上消息完整性校验
毕设答辩时如果只讲 DES,老师大概率会问「DES 密钥这么短,实际项目怎么办」。这时候你能现场把 DES 换成 AES 并说清区别,就是实打实的加分。AES 密钥支持 128/192/256 位,分组长度 128 位,Java 标准库同样直接支持,改动量很小。
import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class AESUtil { // AES-128密钥16字节 private static final String KEY = "1234567890abcdef"; // CBC模式IV16字节 private static final String IV = "abcdef1234567890"; public static String encrypt(String plainText) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(IV.getBytes("UTF-8"))); return Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes("UTF-8"))); } public static String decrypt(String cipherText) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(IV.getBytes("UTF-8"))); return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)), "UTF-8"); } }对比一下 DES 和 AES 的关键参数差异:
| 维度 | DES | AES-128 |
|---|---|---|
| 密钥长度 | 56 位(8 字节) | 128 位(16 字节) |
| 分组长度 | 64 位(8 字节) | 128 位(16 字节) |
| IV 长度 | 8 字节 | 16 字节 |
| Cipher 字符串 | DES/CBC/PKCS5Padding | AES/CBC/PKCS5Padding |
| 密钥规格类 | DESKeySpec | SecretKeySpec |
| 安全性 | 已可暴力破解 | 目前安全 |
换的时候注意三点:密钥从 8 字节改成 16 字节,IV 从 8 字节改成 16 字节,DESKeySpec换成SecretKeySpec且算法名传"AES"。其余代码结构完全一样,这就是对称加密接口统一的好处。
再进一步,对称加密只保证机密性,不保证完整性。攻击者虽然解不开密文,但可以篡改密文导致解密出错。要防篡改,可以加一个 HMAC:发送前对密文算一个 HMAC-SHA256 摘要,接收方先验摘要再解密。摘要不匹配直接丢弃,避免把篡改数据送进解密流程。
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class HmacUtil { private static final String HMAC_KEY = "hmac-secret-key-2024"; public static String sign(String data) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(HMAC_KEY.getBytes("UTF-8"), "HmacSHA256")); return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes("UTF-8"))); } public static boolean verify(String data, String sign) throws Exception { return sign(data).equals(sign); } }发送时把密文和签名一起发:MSG|目标|密文|签名。接收方先verify再decrypt,签名不对直接拒绝。这样机密性和完整性都有了,答辩时能讲出「加密 + 签名」的双重保障,比只讲 DES 高一个层次。
最后说个我自己的习惯:每次改完加密相关代码,先写一个main方法跑一遍「明文 → 加密 → 解密 → 比对」,确认能还原再接入网络模块。加密问题一旦混进网络调试,排查成本翻倍,先隔离验证能省下大量时间。希望帮到你。
本文还有配套的精品资源,点击获取