ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java即时通信聊天系统实战:DES加密与C/S架构从零跑通

Java即时通信聊天系统实战:DES加密与C/S架构从零跑通 简介这是一份面向计算机相关专业学生与开发者的Java即时通信聊天系统毕业设计项目源码采用DES加密算法保障通信安全适合作为毕业设计、课程设计或项目立项演示的参考方案。资源包共69个文件约1020KB包含11个java源文件、40个class编译文件、2个png界面截图、1个jar依赖包及keyfile密钥文件等另附部署文档与全部数据资料目录结构清晰便于快速理解客户端与服务端的整体架构。目前已有116人学习关注。项目已通过导师指导与答辩评审获得95分评价代码经过测试可正常运行。读者可从中获取完整的即时通信实现思路、DES加解密模块设计、客户端与服务端交互逻辑以及部署配置方法既能直接用于毕设答辩也可在此基础上修改扩展功能适合不同基础的学习者参考进阶。1. 从一份 JavaDES 聊天系统源码说起它到底能跑出什么很多同学拿到「基于 JavaDES 加密的即时通信聊天系统」这类毕业设计压缩包时第一反应是解压、找 main 方法、点运行然后被一堆配置文件、数据库脚本和端口冲突按在地上摩擦。这个标题背后其实是一套非常典型的 C/S 架构即时通信系统客户端负责界面与消息收发服务端负责连接管理、消息路由和用户状态维护而 DES 加密则被放在消息传输这一层用来保证聊天内容在网络中不是明文裸奔。它解决的核心问题是「多用户之间如何实时、可靠、带一定安全性地互发消息」适合正在做课程设计、毕业设计或者想用一个完整小项目把 Java 网络编程、多线程、Swing/JavaFX 界面和对称加密串起来的人。你不需要是 Java 老手但至少要能看懂类、接口、Socket 和线程的基本写法否则后面调不通的时候会非常痛苦。2. 先把架构和 DES 加密的位置定清楚为什么不是上来就写代码2.1 即时通信系统的三层拆分与线程模型这套系统最常见的落地结构是「客户端界面层 通信协议层 服务端转发层」。客户端界面层用 Swing 或 JavaFX 做登录窗、好友列表和聊天窗通信协议层定义消息格式比如用|或 JSON 把「类型发送者接收者内容」拼成一个包服务端转发层则用一个ServerSocket监听端口每个客户端连上来就分配一个独立线程线程里循环读取该客户端的输入流解析后转发给目标用户。这里的关键不是代码多高级而是线程模型要清晰一个客户端一个线程线程之间通过一个全局的MapString, PrintWriter或ConcurrentHashMap来共享在线用户和输出流。很多同学写到最后消息发不出去就是因为把输出流写在了局部变量里线程一结束流就关了。DES 加密在这套结构里的位置通常是在「消息内容」进入网络之前加密在对方收到之后解密。也就是说协议层拼包时内容字段是密文而不是明文。这样做的好处是即使有人抓包看到的也是一串 Base64 或十六进制字符。但要注意DES 是对称加密密钥必须双方都知道所以系统里一般会有一个固定的密钥字符串或者通过登录阶段协商一个密钥。毕业设计层面固定密钥加 DES 已经足够体现「加密通信」这个点但你要清楚它的边界DES 密钥只有 56 位有效长度现代标准下并不安全这里更多是教学演示用途。2.2 DES 加密在聊天消息里的具体用法与参数在 Java 里用 DES标准做法是Cipher.getInstance(DES)然后配合SecretKeyFactory和DESKeySpec生成密钥。密钥字符串一般要求 8 字节不足补零或截断。加密模式常用DES/ECB/PKCS5PaddingECB 模式简单不需要 IV适合毕业设计快速跑通但 ECB 会把相同明文块加密成相同密文块安全性弱。如果你想让项目看起来更完整可以用DES/CBC/PKCS5Padding但需要额外传一个 8 字节的 IV客户端和服务端必须一致否则解密出来就是乱码。下面这段代码是服务端和客户端都可以复用的 DES 工具类核心逻辑import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import java.util.Base64; public class DesUtil { // 密钥必须是 8 字节这里用固定字符串实际项目应改为配置或协商 private static final String KEY 12345678; public static String encrypt(String plainText) throws Exception { DESKeySpec keySpec new DESKeySpec(KEY.getBytes()); SecretKeyFactory factory SecretKeyFactory.getInstance(DES); SecretKey secretKey factory.generateSecret(keySpec); // ECB 模式不需要 IV适合快速验证 Cipher cipher Cipher.getInstance(DES/ECB/PKCS5Padding); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encrypted cipher.doFinal(plainText.getBytes(UTF-8)); // 用 Base64 编码避免网络传输时出现不可见字符 return Base64.getEncoder().encodeToString(encrypted); } public static String decrypt(String cipherText) throws Exception { DESKeySpec keySpec new DESKeySpec(KEY.getBytes()); SecretKeyFactory factory SecretKeyFactory.getInstance(DES); SecretKey secretKey factory.generateSecret(keySpec); Cipher cipher Cipher.getInstance(DES/ECB/PKCS5Padding); cipher.init(Cipher.DECRYPT_MODE, secretKey); byte[] decrypted cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(decrypted, UTF-8); } }这段代码里KEY是 8 字节字符串DESKeySpec负责把它转成 DES 密钥材料Cipher负责实际加解密。Base64编码是为了让密文能安全地放进文本协议里传输。参数上DES/ECB/PKCS5Padding是最省事的组合如果你改成DES/CBC/PKCS5Padding就要在cipher.init时多传一个IvParameterSpec并且保证客户端和服务端 IV 完全一致。很多同学解密失败第一反应是密钥错了其实往往是模式或填充写得不一致比如一边 ECB 一边 CBC或者一边 PKCS5 一边 NoPadding。2.3 消息协议怎么拼一个可扩展的包结构协议层不要用纯字符串拼接然后split到底因为消息内容里一旦出现分隔符就会解析错位。常见做法是用一个固定分隔符但内容先做 Base64 或 URL 编码或者直接用 JSON用 Gson 或 Jackson 序列化。毕业设计里为了减少依赖可以用类型#发送者#接收者#Base64(密文)这种格式解析时按#切分内容字段单独 Base64 解码再 DES 解密。下面是一个简单的协议拼包与解包示例public class MessageProtocol { // 类型LOGIN、CHAT、LOGOUT、USER_LIST public static String pack(String type, String from, String to, String content) throws Exception { String encrypted DesUtil.encrypt(content); // 用 # 分隔内容已经是 Base64不会包含 # return type # from # to # encrypted; } public static String[] unpack(String raw) { // 限制切分数量防止内容里出现 # 导致数组越界 return raw.split(#, 4); } }pack方法先把聊天内容 DES 加密再拼成协议字符串unpack用split(#, 4)限制最多切四段这样即使密文里出现#也不会影响前三个字段。参数上type决定服务端怎么处理这条消息from和to用来路由content是密文。如果你用 JSON就把这些字段放进一个对象里序列化后发送解析时反序列化扩展性更好但需要引入额外 jar 包。3. 从零跑通服务端与客户端最小可运行步骤3.1 服务端启动ServerSocket 与在线用户表服务端要做的第一件事是监听端口然后为每个连接开线程。下面是一个最小服务端骨架包含在线用户表和消息转发逻辑import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ConcurrentHashMap; public class ChatServer { // 在线用户用户名 - 输出流 private static final ConcurrentHashMapString, PrintWriter ONLINE_USERS new ConcurrentHashMap(); public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(8888); System.out.println(服务端已启动监听 8888 端口); while (true) { Socket socket serverSocket.accept(); // 每个客户端一个线程 new Thread(new ClientHandler(socket)).start(); } } static class ClientHandler implements Runnable { private Socket socket; private String username; private BufferedReader in; private PrintWriter out; 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(); String[] parts MessageProtocol.unpack(first); if (!LOGIN.equals(parts[0])) { socket.close(); return; } username parts[1]; ONLINE_USERS.put(username, out); System.out.println(username 已上线); String line; while ((line in.readLine()) ! null) { String[] msg MessageProtocol.unpack(line); String type msg[0]; String from msg[1]; String to msg[2]; String content msg[3]; if (CHAT.equals(type)) { // 解密后重新加密转发或者直接转发密文 String plain DesUtil.decrypt(content); String forward MessageProtocol.pack(CHAT, from, to, plain); PrintWriter target ONLINE_USERS.get(to); if (target ! null) { target.println(forward); } } else if (LOGOUT.equals(type)) { break; } } } catch (Exception e) { e.printStackTrace(); } finally { if (username ! null) { ONLINE_USERS.remove(username); System.out.println(username 已下线); } try { socket.close(); } catch (IOException ignored) {} } } } }这段代码里ONLINE_USERS用ConcurrentHashMap保证多线程下安全存取。每个客户端连接后第一个包必须是LOGIN服务端把用户名和输出流存起来。收到CHAT消息时先解密拿到明文再重新加密转发给目标用户。这里有一个细节转发时重新加密意味着每条消息的密文可能不同但明文一致。如果你直接转发原密文目标用户也能解密但服务端就无法做内容过滤或日志。参数上端口8888可以改但客户端必须一致UTF-8编码要显式指定否则中文会乱码。3.2 客户端登录与消息收发Swing 界面最小闭环客户端要做三件事连服务端、发登录包、收发消息。下面是一个控制台版客户端核心逻辑先跑通再套界面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(MessageProtocol.pack(LOGIN, username, , )); // 开一个线程专门接收消息 new Thread(() - { try { String line; while ((line in.readLine()) ! null) { String[] msg MessageProtocol.unpack(line); if (CHAT.equals(msg[0])) { String plain DesUtil.decrypt(msg[3]); System.out.println([ msg[1] ] 对你说 plain); } } } catch (Exception e) { e.printStackTrace(); } }).start(); // 主线程负责发送 while (true) { System.out.print(发送给); String to scanner.nextLine(); System.out.print(内容); String content scanner.nextLine(); out.println(MessageProtocol.pack(CHAT, username, to, content)); } } }客户端启动后先连127.0.0.1:8888输入用户名后发送LOGIN包。接收线程不断读取服务端转发来的消息解密后打印。主线程读取控制台输入拼成CHAT包发送。参数上to必须是服务端在线用户表里存在的用户名否则消息会被丢弃。如果你要套 Swing就把System.out.println换成往JTextArea追加文本把scanner.nextLine()换成按钮事件里的输入框取值。注意 Swing 的 UI 更新必须在 EDT 线程里做否则会出现界面卡死或异常。3.3 数据库与用户资料可选但加分毕业设计如果只做内存版重启后用户和聊天记录全丢答辩时容易被问。常见做法是加一个 MySQL 库建user表和message表。user表存用户名、密码哈希、昵称message表存发送者、接收者、密文、时间。登录时查库校验聊天时异步插入记录。下面是一个建表 SQLCREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, nickname VARCHAR(50) ); CREATE TABLE message ( id INT PRIMARY KEY AUTO_INCREMENT, from_user VARCHAR(50), to_user VARCHAR(50), content TEXT, send_time DATETIME DEFAULT CURRENT_TIMESTAMP );password不要存明文至少用 MD5 或 SHA-256 加盐。message.content存密文还是明文取决于你的设计存密文更安全但服务端要解密才能存明文存明文方便查询但失去加密意义。毕业设计层面存密文并在客户端解密展示能体现「加密通信」的完整性。JDBC 连接用PreparedStatement防注入连接池可以用 Druid 或 HikariCP但简单项目直接DriverManager也能跑。4. 部署文档里最容易翻车的几个点避坑与排查4.1 端口被占用导致服务端启动失败现象运行服务端时抛出java.net.BindException: Address already in use。原因8888 端口已经被其他程序占用或者上一次服务端没关干净。解决换端口比如改成 9999同时改客户端连接端口或者在命令行用netstat -ano | findstr 8888找到占用进程并结束。注意改端口后客户端和服务端必须同步否则连不上。4.2 DES 解密报错Given final block not properly padded现象客户端收到消息后解密抛出javax.crypto.BadPaddingException: Given final block not properly padded。原因密钥不一致、加密模式不一致、填充方式不一致或者密文在传输中被截断。解决先确认两端KEY字符串完全相同再确认Cipher.getInstance参数完全一致最后检查协议解析时有没有把密文切错。常见错误是split(#)没有限制数量内容里带#导致密文被截断。4.3 中文乱码InputStreamReader 没指定编码现象聊天内容里的中文变成???或乱码。原因new InputStreamReader(socket.getInputStream())默认用系统编码Windows 中文环境是 GBK而发送端用 UTF-8。解决所有InputStreamReader和OutputStreamWriter都显式传UTF-8包括服务端和客户端。另外PrintWriter构造时第二个参数true表示自动刷新别忘了。4.4 消息发出去对方收不到在线用户表 key 不匹配现象A 给 B 发消息服务端日志显示已转发但 B 没收到。原因ONLINE_USERS里的 key 是登录时输入的用户名而发送时to字段可能多了空格或大小写不一致。解决登录和发送时都对用户名做trim()并统一大小写规则。另外如果 B 已经下线但表里没移除也会导致消息丢失所以finally块里一定要remove。4.5 客户端界面卡死Swing 线程与网络线程混用现象点击发送按钮后界面无响应或者接收消息时界面不刷新。原因在 EDT 线程里做了阻塞的网络读写或者在非 EDT 线程里直接更新 UI。解决网络收发放在独立线程UI 更新用SwingUtilities.invokeLater包起来。比如接收线程里拿到消息后不要直接textArea.append而是SwingUtilities.invokeLater(() - textArea.append(...))。5. 进阶技巧把 DES 换成 AES 并保留原有协议DES 在毕业设计里够用但如果你想让项目在答辩时更有说服力可以把加密层换成 AES协议和线程模型完全不用动。AES 支持 128 位密钥安全性比 DES 高得多Java 原生支持AES/ECB/PKCS5Padding或AES/CBC/PKCS5Padding。下面是一个 AES 工具类接口和DesUtil保持一致这样你只需要替换工具类其他代码不用改import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class AesUtil { // AES 密钥 16 字节 private static final String KEY 1234567890abcdef; public static String encrypt(String plainText) throws Exception { SecretKey secretKey new SecretKeySpec(KEY.getBytes(UTF-8), AES); Cipher cipher Cipher.getInstance(AES/ECB/PKCS5Padding); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encrypted cipher.doFinal(plainText.getBytes(UTF-8)); return Base64.getEncoder().encodeToString(encrypted); } public static String decrypt(String cipherText) throws Exception { SecretKey secretKey new SecretKeySpec(KEY.getBytes(UTF-8), AES); Cipher cipher Cipher.getInstance(AES/ECB/PKCS5Padding); cipher.init(Cipher.DECRYPT_MODE, secretKey); byte[] decrypted cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(decrypted, UTF-8); } }替换时把MessageProtocol里的DesUtil.encrypt和DesUtil.decrypt改成AesUtil对应方法即可。参数上AES 密钥必须是 16 字节128 位或 32 字节256 位这里用 16 字节。ECB 模式同样不需要 IVCBC 模式需要 16 字节 IV。如果你用 CBC记得两端 IV 一致。验证方法很简单启动服务端和两个客户端互相发中文、英文、带特殊符号的消息看是否都能正确加解密和显示。再抓包看一眼确认网络上传输的是 Base64 密文而不是明文。我自己的习惯是每改一次加密工具类先写一个main方法单独测加解密确认encrypt后再decrypt能还原再接入聊天流程。这样出问题时能快速定位是加密层还是网络层。另外密钥不要硬编码在代码里提交到公开仓库毕业设计至少放到配置文件里读配置加载。希望帮到你。本文还有配套的精品资源点击获取
返回列表