简介这是一份面向计算机网络课程设计的局域网聊天程序完整文档资料适合高校计算机与软件工程专业学生参考。资料基于P2P技术采用C#与Visual Studio 2010开发围绕点对点数据交换展开实现任意多点间的数据共享与聊天并覆盖用户注册登录、聊天、文件传输、好友管理等功能模块。文档从需求分析、总体设计到详细设计均有完整论述包含软件层次模型、数据流程图、服务器与客户端程序实现、功能函数等章节可作为课程设计报告撰写与程序开发的对照模板。压缩包共1个文件为doc格式课程设计说明书体积约161KB容量虽小但章节结构完整。该资料已有559人学习下载。通过这份说明书可以快速掌握P2P聊天程序的系统架构、模块划分和Socket编码思路节省从零构思报告的时间尤其适合临近课程设计提交、需要完整参考样例的同学。1. 局域网聊天程序课设一个挂名 P2P、实为服务器中转的 Java Socket 项目计算机网络课设周最怕的不是题目难而是题目看起来都会、写起来全不会。这份《软件聊天程序》就是典型的计算机网络课设点对点数据交换P2P用 Java Socket API 实现局域网聊天。先泼一盆冷水它名义上叫 P2P实际是标准的服务器转发模式——所有消息先到服务器再由服务器分发给目标客户端在线状态、昵称管理、聊天记录也都挂在服务器端维护。这说明白一个道理课设题目怎么写不重要重要的是把 TCP 协议原理、Socket 编程流程讲清楚、跑通。这份资料适合要对计算机网络课程设计交作业、或者想快速搞懂 C/S 架构聊天程序的人照着里面的 Server.java、Client.java 和功能函数能在一个晚上复现出一个带用户管理、私聊、强制踢人的局域网聊天室。2. 架构与线程模型TCP 连接、双线程循环和 ip2nickname 哈希表2.1 为什么选 TCP 而不是 UDP聊天场景下消息可以慢但不能丢。UDP 丢包重传要自己写确认机制课设周期就十天没必要给自己挖坑。TCP 自带可靠传输、流量控制和拥塞控制Socket 编程的接口又简单accept 之后拿到一条双向流write 和 read 就行底层细节全被协议栈遮住了。这份课设在需求分析里明确写了“采用 TCP/IP 连接方式”同时还要求了解 TCP、UDP 协议的原理——所以答辩时大概率会被问到“为什么不用 UDP”准备一句话聊天消息要求有序可靠TCP 的字节流语义天然满足。服务器端监听用的是 ServerSocket默认构造绑定一个端口例如 new ServerSocket(9999)客户端用 new Socket(ip, port) 发起连接。三条 TCP 握手由协议栈完成应用层只需要关注 accept 返回的 Socket 对象。选端口时注意避开 1-1024 的保留端口1024 以上随便挑课设里用 9999 或 6666 都很常见。2.2 双线程模型与共享数据清单整个系统的线程模型是这门课设最容易讲明白、也最容易翻车的地方。先把这个模型的轮廓描出来后面走代码时不迷路。服务器端有两个角色线程一个是主线程也就是 Server 类继承 Thread 后自己启动的那个 run()死循环执行 server.accept()每接进来一个客户端就创建一个新的 Service 线程专职伺候这个连接另一个是 Service 线程每个在线客户端对应一个。也就是说服务器端是“1 个 accept 线程 N 个业务线程”accept 线程只负责接客业务线程负责读该客户端发来的每一行消息、广播给其他人。客户端稍微简化一点Client 类也是一个 Thread循环里检查有没有待发送消息有就写出去同时创建一个 Listener 线程死循环 readLine 监听服务器推过来的所有消息包括公告、私聊内容和在线列表刷新通知。共享数据是另一个关键点。服务器端维护了五个集合onLineUsers 装着所有已连接的 SocketonLineUsersList 是 AWT 的 List 组件用于显示在线用户chatContentList 显示聊天记录ip2nickname 是 Hashtable保存 IP 地址和昵称的映射allService 装所有 Service 线程。这五个对象被 accept 线程、各个 Service 线程、以及界面事件线程同时访问典型的多线程共享可变数据场景。Hashtable 本身是线程安全的get/put 不会出现脏读ArrayList 则没有内部同步多个线程同时 add/remove 时可能出问题。不过课设规模小并发量低实际跑起来出问题的概率不大——我复现时更关注的是后文要讲的 AWT 组件跨线程更新问题那才是真正会在演示时翻车的点。这里要把两个对象的角色分清楚ip2nickname 是数据层负责“IP 和昵称的对照”onLineUsers 是连接层负责“有哪些 Socket 活着”。所有广播逻辑都要同时操作这两个集合先遍历 onLineUsers 拿到每个 Socket再用 ip2nickname 查昵称拼成在线列表。动任何一个集合前先想清楚另一个集合要不要同步改——这是这份源码设计上的隐含约定改代码时最容易漏。3. 服务器端实现ServerSocket 监听循环、踢人与强制改名的完整走读3.1 Server.run() 的连接接入流程服务器端主循环看起来简单但每一步都埋了设计决策值得一行一行读。核心逻辑在 run() 方法里直接看最关键的接入片段while(!isClosed) { Socket client null; if(!server.isClosed()) { client server.accept(); // 阻塞等待新连接 } String clientIP client.getInetAddress().getHostAddress(); // 同一个 IP 已经连过拒绝第二次连接 if(ip2nickname.get(clientIP)!null) { ChatTookit.sendInfo(client,已经有客户端从该 ip 地址连接服务器本次连接将退出); break; } onLineUsers.add(client); // 没收到自定义昵称前先用 IP 当昵称占位 ip2nickname.put(clientIP,clientIP); onLineUsersList.add(clientIP); chatContentList.add(clientIP 来了); // 给所有在线客户端推送1 在线列表2 上线提示 ChatTookit.sendInfoToAll(onLineUsers,ChatTookit.getAllNickname(ip2nickname)); ChatTookit.sendInfoToAll(onLineUsers,clientIP 来了); // 每个连接配一个专属 Service 线程 Service service new Service(client,ip2nickname,onLineUsersList,chatContentList,onLineUsers,chatContent); this.allService.add(service); }逻辑不复杂accept 阻塞等连接拿到 Socket 后立即取 IP 地址先查 ip2nickname 判重然后加入在线 Socket 集合、写入哈希表、刷新界面列表最后广播两条消息。注意广播顺序不是随意的先发在线列表前缀 NICKNAME:再发“xx 来了”的提示。客户端收到 NICKNAME: 开头的消息就整体刷新用户列表后到的提示才会显示在正确的位置上。几个值得留意的设计边界。第一判重用的是 IP 地址而不是端口或连接 ID意味着同一台机器只能开一个客户端实例演示时想开两个窗口对比是不行的第二个会被提示“已经有一个客户端从该 IP 连接”。第二break写在接入分支里一旦触发会把整个 accept 循环退出而不是继续服务其他客户端这个位置应该用 continue 更合理源码这么写算一个瑕疵。第三所有共享集合的改动都在 accept 线程内完成onLineUsers.add 和 ip2nickname.put 之间没有加锁理论上高并发下 ArrayList 扩容会出问题课设这个规模注意不到。3.2 send / changeNickname / tickSocket管理操作的三个入口服务器端除接入外还有三个管理操作群发消息、强制改名、踢人。它们都由界面按钮触发通过服务器面板上持有的 Server 实例调用。先看 send 方法它同时处理群发和私聊两种消息靠前缀区分public void send(String word) throws IOException { if (word.startsWith(SPECIAL:)) { word word.substring(8); String toNickname word.substring(0, word.indexOf($SPECIAL$)); word word.substring(word.indexOf($SPECIAL$) 9); // 先按昵称查到 IP再按 IP 查 Socket Socket client ChatTookit.getSocketByIP(onLineUsers, ChatTookit.getIP(ip2nickname, toNickname)); ChatTookit.sendInfo(client, 系统管理员 悄悄对你说 word); } else { chatContentList.add(系统管理员 word); ChatTookit.sendInfoToAll(onLineUsers, 系统管理员 word); } chatContentList.select(chatContentList.getItemCount()-1); }私聊消息的格式是SPECIAL:昵称$SPECIAL$内容。先去掉 SPECIAL: 前缀再从剩下的字符串里按$SPECIAL$分成接收者昵称和消息内容两部分。注意这里的下标计算substring(8)是硬编码的因为 SPECIAL: 正好 8 个字符indexOf($SPECIAL$) 9是跳过这个分隔符本身的 9 个字符。整个解析依赖精确的字符串位置任何一端消息格式发错下标就错位要么消息发错人要么直接 StringIndexOutOfBoundsException——这就是后面要讲的协议约定问题。踢人走 tickSocket 方法核心步骤是反查三个集合再一一剔除String ip ChatTookit.getIP(ip2nickname, nickname); Socket client ChatTookit.getSocketByIP(onLineUsers, ip); Service service ChatTookit.getServiceByIP(allService, ip); this.allService.remove(service); this.onLineUsers.remove(client); this.ip2nickname.remove(ip); ChatTookit.sendInfo(client, 你已经被系统管理员踢出聊天室); client.close(); ChatTookit.updateOnLineUsersList(onLineUsersList, ip2nickname); ChatTookit.sendInfoToAll(onLineUsers, 系统管理员 将 nickname 踢出聊天室); ChatTookit.sendInfoToAll(onLineUsers, ChatTookit.getAllNickname(ip2nickname));这里有一个必须注意的坑踢人只做了 client.close()并没有打断 Service 线程正在执行的 readLine()。close 后阻塞的读操作会抛 SocketExceptionService 线程如果没捕异常就静默退出了但如果 Service 线程恰好处于写消息的状态可能还会再往已关闭的流上写一次触发 IOException。演示时踢完人立刻发群聊消息有概率在服务器控制台冒出一条红色异常——不影响功能但答辩时被老师看到会扣印象分。改进办法是在 Service 里加一个 stopFlag 标志位tickSocket 里先置位再 close。强制改名的 changeNickname 方法同样依赖 ChatTookit 的辅助函数先查重、再反查 IP、更新哈希表、刷新所有客户端的列表。核心逻辑不难难点在于查重逻辑用的是ip2nickname.contains(newName)Hashtable 的 contains 方法是按 value 查询的也就是查昵称是否已存在——这个用法没毛病但很容易误写成 containsKey。课设答辨时能解释清这个方法名和语义的差别反而是加分项。4. 客户端实现与消息协议NICKNAME 广播、$SPECIAL$ 私聊与轮询发送4.1 Client.run() 的发送循环与 Listener 线程客户端实现比服务器端简单得多但它的线程模型很有代表性一个线程负责发一个线程负责收两个线程通过实例变量 word 传递数据。看代码public void run() { BufferedReader br null; PrintStream ps null; br new BufferedReader(new InputStreamReader(client.getInputStream())); ps new PrintStream(client.getOutputStream(), true); // 独立线程监听服务器消息 listener new Listener(br, this.onLineUsersList, this.chatContentList); while(!isClosed) { if(this.word.length()0) { ps.println(this.word); // 有待发消息就发送 this.word; } else { sleep(100); // 没有消息就睡 100ms } } listener.destroy(); client.close(); }这里实现了一个典型的轮询发送机制send() 方法只是把字符串存进 word 字段真正写网络发生在 run() 循环里每 100ms 检查一次 word 是否为空。为什么不用阻塞队列或者直接同步写因为界面线程AWT 事件线程和网络线程之间需要解耦界面点按钮时不能阻塞等待网络 I/O。用 word 变量加轮询是最简单的一种“生产者-消费者”模型缺陷是实时性最差有 100ms 延迟但聊天场景完全感知不到。PrintStream 构造时第二个参数传了 true表示自动 flushprintln 调用后立即把缓冲区刷到网络。这里有个习惯要注意很多初学者用 PrintWriter 时忘记 flush导致消息卡在缓冲区半天发不出去现象就是“我发了消息对方收不到但过一会儿突然一起冒出来”。这项目的原班人马直接用 PrintStream 的自动刷新天然躲过了这个坑。Listener 类的代码在说明书里没有贴全但从构造参数可以推断它的职责拿着 BufferedReader 死循环 readLine()收到以 NICKNAME: 开头的行就刷新在线列表收到“系统管理员 悄悄对你说”之类的普通行就追加到聊天记录。它本质上是客户端的消息分发器所有服务器推送的消息都从这里进 UI。4.2 消息协议的三个约定前缀整个系统没有用对象序列化也没有定义复杂协议就是朴素的文本行协议每条消息以换行符结尾用 println/readLine 成对收发。三类消息靠行首前缀区分这是整个聊天系统能协作的基石整理成表格前缀或格式方向含义NICKNAME:开头后跟空格分隔的昵称列表服务器 → 客户端刷新全部在线用户列表纯文本如系统管理员xxx服务器 → 所有客户端群聊公告SPECIAL:昵称$SPECIAL$内容客户端/服务器 → 服务器私聊消息管理员专用端口普通文本行客户端 → 服务器客户端发出的普通群聊消息客户端上行协议更简单登录后发的第一行消息是昵称服务器用这行内容更新 ip2nickname 里该 IP 对应的昵称后续每一行就是群聊内容。getIP 方法里有个小细节如果昵称包含字符会把之前的部分当作昵称——这是因为服务器端刷新用户列表的格式是“昵称IP 地址”把 IP 拼在 后便于查看解析时再切掉尾巴。私聊协议是最容易踩坑的地方它只实现了服务器端管理员主动私聊某个用户普通客户端之间没有互发私聊的入口。如果课程设计报告里写了“用户与用户之间的信息交互和文件交互”而代码里只看到管理员私聊答辩前要想好怎么解释客户端之间没有直接通信文件交互如果没实现就别往报告里写。实话实说是课设答辩的最优解硬吹功能反而容易被追问细节问到卡壳。5. 常见问题排查同 IP 多开、中文乱码与踢人不生效的几个坑5.1 第二个客户端永远登不进来现象同一台电脑上开了两个客户端窗口填同样的服务器 IP第二个客户端的聊天框里显示“已经有客户端从该 ip 地址连接服务器本次连接将退出”登录不了了。原因服务器端判重逻辑用的是客户端 IP 地址作为唯一标识ip2nickname 这个 Hashtable 的 key 是 IP同一个 IP 只允许一条记录第二个连接直接走 break 退出整个 accept 循环。解决把判重单位从 IP 换成 Socket 或临时分配的客户端 ID。最省事的改法是在 Server.run() 里不要再按 IP 判重改为维持一个“在线 Socket 集合”新连接先比对 Socket 对象是否已存在——但这不解决本质问题。正确的思路是用UUID.randomUUID().toString()给每个连接分配唯一 ID哈希表 key 用 ID显示名保留“昵称IP”。演示需要多开时用多台真机或者虚拟机桥接网络一个 IP 一台机器绕开这个限制。5.2 中文聊天记录在另一台电脑上变成乱码现象服务器是英文 Windows客户端是中文系统或者反过来聊天记录里中文全部变成???英文正常。原因BufferedReader 构造时只写了new InputStreamReader(client.getInputStream())没指定字符集用的是 JVM 默认编码。不同系统默认编码不一致中文 Windows 是 GBK英文系统是 UTF-8两边编码对不上就乱码。解决所有流式读取和写入的地方显式指定同一字符集。服务端读用new InputStreamReader(client.getInputStream(), UTF-8)写用new PrintStream(client.getOutputStream(), true, UTF-8)客户端同样统一用 UTF-8。JDK 6 时代 UTF-8 已经是跨平台事实标准指定后中文不会再出问题。5.3 踢人后对方还能继续发消息现象管理员点“踢出”提示框也弹了但被踢的用户在界面上继续输入、继续发聊天室里还能看到他的消息。原因tickSocket 只执行了 client.close()该客户端对应的 Service 线程并没有被中断或标记停止。readLine() 阻塞在已经关闭的 socket 上会抛异常但 Service 如果没有正确退出逻辑异常可能只是打印后 continue线程没有销毁又有可能重新进入读循环。解决在 Service 类里加一个 volatile boolean running 标志循环条件从while(true)改成while(running)。tickSocket 里先service.stopService()再 close socket。这一步把“连接关闭”和“线程停止”解耦是客户端上下线管理里最容易被忽略的收尾动作。另一个连带问题被踢的用户重新登录后ip2nickname 里旧记录没删干净新连接可能被误判为重复登录。踢人流程里必须把三个集合同步清理别只删一个。5.4 服务器启动后客户端连接直接被拒绝现象双击启动服务器端客户端填写 IP 和端口后点登录报ConnectException: Connection refused但服务器端明明显示在运行。原因Windows 防火墙拦截了 ServerSocket 监听的端口。第一次运行服务器程序时系统弹防火墙提示如果点了取消端口就不会对外放开。另一种可能是服务器监听的是 127.0.0.1 回环地址客户端填了局域网 IP压根没连到。解决先确认服务器面板显示的 IP 不是 127.0.0.1 或 localhost如果是就改成 getLocalHost 或可配置的对外网卡地址。防火墙问题在 Windows 防火墙高级设置的入站规则里放行对应端口或者开发阶段临时关掉防火墙验证。排查时多用一条命令在另一台机器上telnet 服务器IP 端口能通说明监听正常连不上说明要么防火墙拦截要么监听了回环地址。5.5 在线列表刷新延迟或列表越变越长现象用户下线后其他客户端的在线列表里仍然能看到他的名字反复上下线几次后列表里多出一堆重复项。原因客户端列表收到的 NICKNAME: 消息是一次性全量刷新但服务器端退出连接时只调用了 remove没有给所有在线客户端再群发一次新的 NICKNAME: 列表。Service 线程结束时如果只是把 Socket 从列表移除ip2nickname 里查不到该 IP 后后续即使广播了全量列表也不会包含这个用户——问题在于广播本身没触发。解决在 Service 线程 catch 到连接关闭异常时必须补上三行代码从 onLineUsers 移除自己、从 ip2nickname 移除自己的 IP、调用 sendInfoToAll 重新广播在线列表和下线提示。原设计把退出清理逻辑完全遗漏了这是课设源码里最大的功能缺口复现时顺手补上演示效果好一大截。6. 验证与改造从课设到小型 IM 的两个进阶方向课设验收的标准动作是“演示 答辨”。这项目的演示建议用至少两台机器一台跑 Server 一个客户端另一台跑一个客户端。准备一个清单逐项过登录时所有端都能看到“新用户上线”提示和在线列表刷新群发消息全端可见管理员私聊只有目标用户能看到强制改名后所有端列表同步更新踢人后目标端弹提示并断开连接。每项过完在服务器端的聊天记录列表能看到对应的日志条目——这个“记录留痕”的设计是答辩时展示系统完整度的利器务必在演示前把聊天记录依次积攒几条像样的会话记录。如果时间富余我建议做两个方向的改造任何一个都能在答辩时拉开差距。第一个方向是解决断线感知问题加心跳机制Service 线程每 30 秒向客户端发送一个PING行客户端收到后回PONG服务器连续三次没收到就判定掉线自动清理该用户的 Socket 和哈希表记录。这比被动等待 readLine 抛异常要可靠得多也正好呼应了计算机网络课里“保活机制”的知识点。第二个方向是把省出来的线程资源吃掉原版每个连接一个 Service 线程500 个在线用户就有 500 个线程虽然课设规模不会到这一步但改进成线程池维护连接是实打实的好习惯。用ExecutorService提交任务线程数配置成Runtime.getRuntime().availableProcessors() * 2管理踢人时用一个 ConcurrentHashMap 把连接 ID 映射到 Future 句柄需要踢人时调用 future.cancel(true)。这样改完代码的含金量完全不像是课设级别。从那以后我每次拿到课设源码第一件事不是打开 IDE 运行而是先画一张线程和共享数据的清单确认哪些对象被哪些线程修改、谁负责清理、哪个集合和哪个集合要同步更新再去看代码。这项目要是你只粘代码跑通就交能拿及格分把上面几个坑亲手修一遍把心跳和线程池加进去再把消息协议梳理成一张表放进课程设计说明书这就能往优秀方向走了。希望帮到你。本文还有配套的精品资源点击获取