拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Java Socket实现局域网聊天工具:C/S架构、多线程与消息协议设计

简介这是一份基于Java的即时通信软件毕业设计文档面向计算机相关专业毕业生、Java初学者及需要完成课程设计的开发者可用于快速梳理即时通信系统的选题思路与开发全流程。文档从课题研究背景与关键技术切入详细介绍了Socket网络编程、多线程并发处理、TCP/IP协议及数据加密等实现要点并给出完整的系统需求分析、C/S架构设计、异步通信方式以及数据库概要设计涵盖用户表、好友表、在线状态表、离线信息表等核心结构。同时文档对系统设计进行了深入拆解包括服务器端与客户端的功能模块划分、注册登录模块、消息协议定义、离线消息存储与转发机制以及线程池优化、敏感信息加密等实际开发中的注意事项全文章节结构完整从绪论、需求分析到详细设计与实现均有涉及。资源包为单个doc文件共1个文档压缩包大小638KB内容结构清晰、目录完整便于直接查阅和二次编辑。目前已有110人学习适合正在准备毕业设计或希望系统掌握即时通信软件设计方法的人参考。1. 从局域网聊天的需求说起为什么选 Java C/S 而不是 B/S即时通信软件听起来门槛不高但真要从零实现一套可用的局域网聊天工具技术点并不少连接管理、并发读写、消息协议、离线存储任何一个环节没想透联调时都会回来找你。这个项目用纯 Java 实现了完整的 C/S 架构即时通信系统服务端负责用户认证、好友关系维护和消息中转客户端负责界面展示和点对点直连。它没有用 WebSocket也没有引入 Netty所有通信都建立在 Java Socket 之上对理解 TCP 编程和线程模型非常有帮助。如果你正在做 Java 网络编程相关的课程设计或者想把多线程、数据库、GUI 开发串成一个大作业这个项目拆解下来的方案可以反复套用。2. 通信骨架Socket、线程模型与消息协议设计2.1 登录信令与好友列表的拉取流程系统采用经典的 C/S 双端结构服务器启动后监听固定端口客户端通过 Socket 发起 TCP 连接。登录流程不是一次性请求/响应而是拆成两段客户端先发送封装好的账号密码服务器验证成功后再把该用户的好友列表、在线好友 IP 和端口号一次性回传。这里的「好友列表」并不直接从数据库灌给客户端而是通过ArrayListFriendModel组装成模型对象再经SendModel.sendFriList()序列化发送。之所以这样设计是为了让客户端不需要直接访问数据库所有数据访问都收敛在服务器端。常见做法是让LoginListener线程专门接收登录包验证通过后同时执行三件事写入登录表、刷新服务器端表格、推送好友列表。你可以把它想象成一次「握手 资源下发」后续聊天不再经过这个入口。// 伪代码示意登录成功后的资源下发 LoginModel lm parseLoginPacket(inputStream); UserModel user loginDao.findByNumAndPass(lm.getUserNum(), lm.getPass()); if (user ! null) { win.getDao().addLoginUser(lm); // 写入服务器端登录表 ArrayListFriendModel friList win.getDao().getFriendsList(lm.getUserNum()); SendModel.sendFriList(lm, friList); // 下发好友列表 }这里的addLoginUser负责记录当前在线会话getFriendsList通过用户编号查出好友及在线状态sendFriList把列表数据写入输出流。注意登录包解析时不要直接在业务线程里做复杂 SQL否则多个用户同时登录时会出现明显的卡顿。2.2 在线直连与服务器中转的两条消息通道聊天消息的发送路径有两条在线直接通讯和服务器代理通讯。在线直接通讯利用登录阶段拿到的对方 IP 和端口客户端直接用新 Socket 把消息发到对端 PC服务器不参与消息内容转发这种方式延迟低适合局域网内网环境。但是如果两台机器之间有防火墙限制或者点对点连接长时间建不起来就自动降级为服务器中转也就是所有消息先发给服务端再由服务端转发给接收方。这个设计思路其实和早期的 QQ 非常相似核心价值在于容错。实现时需要注意点对点直连使用的是客户端之间约定的 TCP 端口这个端口必须与服务器监听端口区分开否则会冲突。判断是否直连成功可以设置连接超时超时后立刻走中转通道保证用户无感知。# 查看端口监听状态确认服务端与点对点端口不冲突 netstat -an | grep -E LISTEN|ESTABLISHEDLinux 下用该命令可以看到当前所有 TCP 连接状态如果你在 Windows 上调试把grep换成findstr。正常运行时服务器端口和点对点端口应该同时处于监听或已建立连接的状态。如果只有服务器端口在监听说明所有消息都走了中转通道需要检查客户端是否拿到了好友的真实 IP。2.3 消息协议字段设计从地址到消息标识项目原文对消息协议提出了明确要求消息格式必须包含「让接收者可以回消息的地址、发信者和即时收件箱的标识」并且允许对信息有效负载进行编码和鉴别。落到实现上我一般会用自定义的文本协议每一行是一个字段用分隔符切分避免引入复杂的序列化框架。常见的协议包可以这样组织字段名类型含义示例typeint消息类型1 登录2 文本3 好友列表2fromUserIdint发送方账号10001toUserIdint接收方账号10002ipString发送方 IP用于回包192.168.1.10portint点对点监听端口9000contentString消息正文hellotimestamplong发送时间戳用于防重放1712345678901// 发送消息时组装协议包 String packet String.join(|, 2, String.valueOf(fromUserId), String.valueOf(toUserId), ip, String.valueOf(port), content, String.valueOf(System.currentTimeMillis()) ); outputStream.write(packet.getBytes(UTF-8));字段之间用|分隔接收端按split(\\|)解析。注意content里如果包含|需要做转义或使用更长的分隔符否则解析会错位。时间戳字段的意义在于幂等——客户端收到消息后可以根据时间戳去重避免重复消息弹出多个窗口。3. 数据库与后端落地Oracle 表结构与服务器端线程实现3.1 五张核心表设计用户、好友、在线状态、登录记录、离线消息数据库选用 Oracle 10g客户端工具是 PLSQL Developer。表设计遵循第三范式把用户、好友关系、在线状态、登录会话、离线消息拆成五张表。其中用户表是主表好友表通过外键关联用户编号在线状态表用typeid区分在线、离线和隐身登录表记录每次会话的 IP 和时间离线信息表则用于存储用户不在线时别人发来的消息。-- 用户表 CREATE TABLE user_info ( user_num NUMBER(10) PRIMARY KEY, user_name VARCHAR2(20) NOT NULL, pass VARCHAR2(20) NOT NULL, desc_info VARCHAR2(100), sex NUMBER(1), birthday DATE, olpic BLOB, ofpic BLOB, mespic BLOB ); -- 好友表 CREATE TABLE friends ( friid NUMBER(10) PRIMARY KEY, user_num NUMBER(10) NOT NULL, frinum NUMBER(10) NOT NULL, CONSTRAINT fk_friends_user FOREIGN KEY (user_num) REFERENCES user_info(user_num) );这里的user_num作为用户编号主键通过序列sq_user.nextval自动生成。好友表用friid做主键user_num和frinum分别表示主人和好友的账号外键约束确保不会引用不存在的用户。头像字段使用BLOB类型存储图片二进制数据这在课程设计中很常见但生产环境更推荐只存文件路径把图片放到文件服务器或对象存储避免数据库膨胀。3.2 ServerSocket 与双监听线程LoginListener 和 MesListener服务器端的实现基于ServerSocket主线程负责侦听客户端连接每接受一个连接就new ServerThread(socket)创建一个线程。原文中提到了LoginListener和MesListener两个线程类前者处理登录逻辑后者处理消息中转和离线存储。这种按职责拆线程的方式比单一线程处理所有包更容易维护。ServerSocket server new ServerSocket(8888); while (true) { Socket socket server.accept(); new Thread(() - { try { InputStream in socket.getInputStream(); OutputStream out socket.getOutputStream(); // 根据首包类型分发给 LoginListener 或 MesListener } catch (IOException e) { e.printStackTrace(); } }).start(); }上面的代码展示了一个最朴素的「每连接一线程」模型适合并发量不大的场景。注意accept()是阻塞调用程序会停留在这一行直到有客户端连接。每个客户端连接进来后需要先读首包判断是登录请求还是消息请求再交给你定义的 handler 处理。实际使用中不要直接在匿名线程里写大段业务逻辑最好封装成ServerThread类便于扩展。3.3 注册和登录的 SQL 实现与注入边界用户注册模块的核心逻辑是先把注册信息封装成对象再插入数据库。原文给了一个很典型的LoginId方法直接拼接字符串执行 SQL。这种做法在毕业设计里能跑通但存在 SQL 注入风险面试时常常会被问到。public void LoginId(String userName, String pass, String desc, int sex, String birthday) { String sql insert into user_info values(sq_user.nextval, userName , pass , desc , sex , to_date( birthday ,yyyy-mm-dd),null,null,null); dbutil.executeDML(sql); }这里把用户名、密码、签名、性别、生日直接拼进 SQL如果userName里包含了单引号比如admin --就会破坏 SQL 结构甚至被用来绕过验证。更稳妥的做法是用PreparedStatement加占位符或者至少对单引号做转义。登录验证模块的逻辑是查询账号密码是否匹配同样建议参数化查询。你可以把这一点写在文档的「安全性分析」里作为与普通课程设计拉开差距的亮点。4. 客户端实现界面事件、好友状态与聊天收发4.1 登录按钮背后的监听逻辑客户端用JFrame构建登录窗口按钮事件通过LoginFrameHandler实现ActionListener接口。用户点击登录时监听器从输入框取账号、密码和在线状态封装成LoginModel然后启动一个线程把登录包发给服务器。为什么不能用主线程发送因为网络阻塞会让界面卡死用户点击后窗口会无响应。public class LoginFrameHandler implements ActionListener { public void actionPerformed(ActionEvent e) { String num userField.getText(); String pass passField.getText(); // 封装登录模型经 Socket 发送 LoginModel lm new LoginModel(num, pass, 12); new Thread(() - socketManager.sendLogin(lm)).start(); } }代码里用new Thread(...).start()将网络发送放到独立线程这样 UI 线程能立刻返回界面不会卡住。12对应在线状态表的「在线」10是离线11是隐身。这个状态值是前后端约定好的常量在客户端和服务器端都要定义一份避免魔法数字散落在业务逻辑中。4.2 好友列表与头像状态更新登录成功后服务器返回好友列表客户端把这些数据填充进 JTable 或 JList。为了体现在线状态客户端需要根据每个好友的在线标识动态切换头像图标。常见做法是提前把离线头像和在线头像加载到内存 Map 里渲染时直接查表取图片避免每次重绘都做磁盘 IO。// 根据在线状态选择头像 ImageIcon icon onlineMap.get(friend.getUserNum()); if (icon null) { icon offlineIcon; } listCellRenderer.setIcon(icon);这里的onlineMap是本地缓存键是好友编号。如果服务器下发好友列表时携带了 IP 和端口客户端还可以维护一张「可直连好友」列表双击头像时优先尝试点对点连接。头像闪动效果可以通过一个定时器周期性切换消息头像和普通头像实现注意定时器线程结束后必须释放否则关掉聊天窗口后仍然在闪。4.3 聊天窗口的发送、接收与离线补拉聊天窗口的核心是发送和接收两个动作。发送时先判断好友是否在线、是否可直连如果可直连则直接用点对点 Socket 发送否则发给服务器中转。接收方收到消息后要判断当前用户是否在聊天界面中如果在则直接打印到文本区否则只在好友条目上闪动头像。离线消息的处理是收到登录成功通知后主动从服务器拉取离线表里的留言。// 接收消息并处理 String line bufferedReader.readLine(); String[] parts line.split(\\|); if (2.equals(parts[0])) { String content parts[5]; chatArea.append(好友说: content \n); }这段代码放在客户端的消息接收线程里每次读到一行就解析按type字段分发。注意readLine()是阻塞的所以接收线程通常用while (true)循环持续读。如果服务器端没有发送换行符readLine()会一直等不到数据这就是常见的「客户端收不到消息」的坑排查时要先用 TCP 调试工具确认服务器确实发出了\n。5. 验证与进阶从课程设计到可工程化的即时通信5.1 用测试案例验证登录与消息闭环系统测试不能只靠手工点界面。至少要准备三类用例正常登录、错误密码登录、离线消息补拉。正常登录要验证好友列表数量正确错误密码要验证返回「登录失败」且服务器端登录表不增加记录。离线消息的测试比较隐蔽建议这样操作先用 A 账号登录B 账号不登录用 A 给 B 发消息再登录 B检查是否收到离线留言。// 测试离线消息补拉 Test public void testOfflineMessage() throws Exception { // 模拟 B 不在线 sendMessage(10001, 10002, Hello, offline); // B 登录后应收到该消息 ListString messages getOfflineMessages(10002); assertTrue(messages.contains(Hello, offline)); }这个测试的核心是验证服务器端在转发失败时把消息写入了离线信息表并在用户登录后清空该表。实际项目中可以用 JUnit 写类似的集成测试启动服务器后通过客户端 API 直接操作避免依赖 GUI 自动化。5.2 线程池替代每连接一线程面试里会追问的细节原代码的new ServerThread(socket)在低并发下没问题但每个连接占用一个线程Java 默认线程栈大小约 1MB连接数一多内存就会吃紧。更合理的做法是用线程池配合Callable或Runnable接口管理异步任务。下面是改造后的服务器主循环ExecutorService pool Executors.newFixedThreadPool(16); while (true) { Socket socket server.accept(); pool.submit(new ClientHandler(socket)); }线程池大小不是随便定的一般按CPU 核心数 1作为最小值。IO 密集型任务可以适当加大线程数但要注意队列积压会带来延迟。面试官如果追问「线程池拒绝策略」你要能答出CallerRunsPolicy和AbortPolicy的区别。这里只是把连接处理放到了池子里心跳检测和消息推送仍然需要你自己实现。5.3 心跳机制与半包处理最后一个必须掌握的坑原系统没有心跳机制如果客户端断电退出服务器无法及时感知连接断开数据库的登录表里会残留僵尸记录。常见做法是每隔 30 秒发送一个type0的心跳包服务器 3 次没收到就主动断开并清理登录状态。另外TCP 是流式协议readLine()可能读完半个消息所以在消息边界处理上自定义协议最好加上\r\n结尾并在解析时按分隔符切割完整包。private String readPacket(BufferedReader reader) throws IOException { StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); if (line.endsWith(/msg)) { // 自定义结束标记 break; } } return sb.toString(); }这段代码是半包处理的简易示例读完一行后检查是否包含结束标记。如果你的消息内容本身可能包含标记就要在协议层增加长度字段先读固定的 4 字节长度再读对应字节数。这个技术点在 Java 网络编程面试中几乎必考也是从「能跑」走向「可用」的关键分水岭。把这几个点补进设计文档里整个项目的完成度会明显上一个台阶。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门