
简介这是一份面向高校计算机网络课程设计的完整聊天室项目报告适合计算机、网络工程等专业学生参考复用。报告围绕Java Socket编程展开涵盖注册登录、实时聊天、文件传输三大功能模块并配有需求分析、总体设计、流程图、关键代码注释等内容可帮助读者快速理解TCP通信、ObjectOutputStream消息封装、多线程用户管理等核心实现。资源为单一docx文档共1个文件大小约283KB内容结构完整适合作为课程报告模板或毕业设计前置练习。已有197人学习浏览。可直接参考其报告结构与写法替换项目细节即可快速成稿。报告日期为2017年代码环境要求JDK 1.8示例中包含聊天窗口构建、在线用户列表、文件流传输等关键代码段对上手复现聊天室项目具有实际指导意义。1. 计算机网络聊天室课程设计一份能跑通的 Java Socket 项目胜过背十遍“自顶向下”一个真正写过 Socket 聊天室的人和只背过“计算机网络自顶向下”的人差的不是记住了多少 TCP 状态而是能不能把 ServerSocket、Socket、ObjectOutputStream 这三样东西串成一个能跑起来的 TCP 应用。这份课程设计报告做的是一个典型 Java 聊天室注册登录、在线列表、多人聊天、文件传输用户数据写进 user.txt消息封装成 Message 对象走 Socket 传输文件传输绕开服务器走客户端直连。它适合两类人一类是计算机网络课程设计选了“聊天室”题目的学生另一类是打算用一个小项目验证 TCP 编程和线程模型的从业者。把这份报告里的源码逻辑拆开重跑一遍比考前背十遍教材可靠得多。2. 需求与模块拆解注册登录、聊天、文件传输三个模块各守边界课程设计报告里写得很清楚“聊天室总体分为三个模块主要包括注册登录、聊天模块、文件传输模块”。这句话看着朴素实际把整个系统的耦合边界划好了。三个模块的数据流完全不同注册登录只碰 user.txt 和登录校验聊天模块走服务器转发文件传输却要绕开服务器直连。如果不在写代码前把这个边界想清楚后面很容易把所有逻辑堆在一个类里改一个地方崩一片。2.1 需求分析user.txt 做账号体系的利与弊原文需求分析里写的是注册时创建一个 File 类载入 user.txt判断用户名是否已经存在有效后写入登录时去 user.txt 读取用户数据验证用户名和密码。这是最简单的人肉数据库没有引入任何外部依赖。好处是课程设计答辩时能逐行说清账号怎么存、怎么查、怎么防重复坏处是密码明文存放而且多线程并发读写文件容易互相踩。处理并发我一般会把读写 user.txt 的操作封装成一个 UserDao并在读写方法上用 synchronized 做串行化// UserDao.java import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.io.IOException; import java.util.List; public class UserDao { private static final String USER_FILE user.txt; // 注册校验用户名存在返回 false不存在则追加写入 public synchronized boolean register(String name, String password) throws IOException { ListString lines Files.readAllLines(Paths.get(USER_FILE), StandardCharsets.UTF_8); for (String line : lines) { if (line.split(,)[0].equals(name)) { return false; // 用户名已存在 } } Files.write(Paths.get(USER_FILE), (name , password \r\n).getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND); return true; } }逻辑说明用 synchronized 修饰 register 方法保证两个客户端同时注册时不会出现“都读到用户名不存在然后都写进去”的情况。文件里每行存“用户名,密码”注意这里密码没有做加密课程设计可以接受但如果将来扩展成真实系统至少要换成哈希存储。参数说明USER_FILE 是相对路径IDEA 和 Eclipse 的工作目录不同会找不到文件。如果你启动后一直报 FileNotFoundException我建议改成绝对路径或者把你的 user.txt 放在项目根目录而不是 src 目录下。Files.readAllLines 第二个参数显式指定 UTF-8是为了避免 Windows 默认 GBK 导致中文用户名乱码。2.2 Message 类型设计一条消息走完全部业务原文源码里大量用到 Message 对象但没给出完整字段定义。从调用代码能还原出来Message 里至少有 type、name、timer、info、fileName、size、clients 这几个字段。clients 是 HashSet 表示这条消息要发给哪些用户。其中 type 是整个聊天室的业务路由核心type含义服务器处理动作0用户上线广播给所有在线用户并刷新在线列表1聊天消息根据 clients 集合定向转发给指定用户2请求发送文件转发给接收方附带文件名和文件大小-1用户下线广播下线通知从在线列表移除这个设计很聪明服务器不需要为每个业务单独定义协议格式只需要读 type 字段然后决定把 Message 原样转发给谁。原文里“系统从 Message 中提取消息类型再根据类型将消息转化后通过 Socket 转发到相应的用户”说的就是这段逻辑。还有一个细节值得注意客户端发送聊天消息时自己屏幕上也会追加一条“我对 XX 说”这是本地 UI 直接处理的不依赖服务器回包。如果等服务器回包才显示网络延迟一上来发送者会有“消息没发出去”的错觉。2.3 线程模型一个客户端一条线程别再 main 线程里 read原文设计说明里有一句关键的话“通过线程来存储用户的 Socket 连接状态接收并处理用户发过来的信息返回处理信息。客户机也通过线程来接收服务器的处理数据做出响应。”这就是典型的每连接一线程模型。服务器主线程只负责 accept()每接受一个客户端连接就 new 一个 Thread 去处理这个 Socket 的输入流。客户端则需要两个线程主线程负责界面和发送消息另开一个 ClientInputThread 专门守着 readObject() 接收服务器转发过来的消息。如果客户端不在独立线程里读数据发送消息后 UI 线程就会阻塞在 read 上窗口直接卡死。这里有个很容易被忽略的点服务器不能只用一个线程循环 read 所有 Socket因为 readObject() 是阻塞的一个客户端的消息处理不完其他客户端的消息就永远轮不到。每连接一线程虽然老派但在课程设计的学生人数规模下完全够用而且比线程池更容易在答辩时讲清楚。3. 核心链路复现从 ServerSocket 到 Message 序列化代码一步不落这一章把聊天室最核心的链路拆开服务器监听、客户端登录、消息转发。原文源码里界面代码占了大头但真正体现 TCP 功底的是 ServerSocket、Socket 和 ObjectOutputStream 怎么配合。按下面的顺序写基本不会跑偏。3.1 服务器端ServerSocket 绑定端口、维护客户端连接池服务器端代码骨架我一般这样写// ChatServer.java import java.io.*; import java.net.*; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ChatServer { private static final int PORT 8888; // 在线客户端表key 是用户名value 是客户端 Socket private static MapString, Socket onlineMap new ConcurrentHashMap(); public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(PORT); System.out.println(服务器已开启端口 PORT); while (true) { Socket socket serverSocket.accept(); // 每个客户端连接单独开线程 new Thread(new ClientHandler(socket)).start(); } } }逻辑说明ServerSocket 的 accept() 是阻塞方法每接到一个客户端连接就返回一个 Socket。如果不开多线程第一个客户端连上后服务器在处理它的消息时第二个客户端就再也连不进来。用 ConcurrentHashMap 而不是 HashMap 存在线客户端是为了避免多个处理线程同时 put 或 remove 时出现并发修改异常。参数说明端口 8888 是我这里随手写的课程设计没有强制要求但建议避开 0 到 1023 的知名端口也不要挑其他程序已经占用的端口。启动报 Address already in use 时先用 netstat 查端口占用或者换个四位数端口。3.2 客户端登录Socket 连接、发送上线消息客户端登录部分的核心代码// ChatClient.java 登录片段 import java.io.*; import java.net.*; import java.text.SimpleDateFormat; import java.util.Date; try { Socket socket new Socket(127.0.0.1, 8888); // 客户端先构造输出流并发送上线消息 ObjectOutputStream oos new ObjectOutputStream(socket.getOutputStream()); Message loginMsg new Message(); loginMsg.setType(0); loginMsg.setName(username); loginMsg.setTimer(new SimpleDateFormat(HH:mm:ss).format(new Date())); oos.writeObject(loginMsg); oos.flush(); } catch (IOException e) { e.printStackTrace(); }逻辑说明new ObjectOutputStream 的构造过程会向底层流写入一个序列化头这个头是接收端 ObjectInputStream 能正常工作的前提。这里的关键约束是如果服务器端先构造 ObjectInputStream客户端必须先构造 ObjectOutputStream 并 flush 一个对象否则两边会在构造流时互相等待形成死锁。参数说明new Socket(127.0.0.1, 8888) 是连接本机服务器。如果服务器在另一台机器上这里要改成服务器的局域网 IP。Message 对象必须 implements Serializable否则 writeObject 会抛 NotSerializableException。这个接口在原文里没有写全但它是序列化传输的硬前提补上才能跑。3.3 转发逻辑读 type定向转发或广播服务器处理消息的方法private void handleMessage(Message msg) throws IOException { switch (msg.getType()) { case 0: // 用户上线 onlineMap.put(msg.getName(), socket); broadcast(msg); break; case 1: // 聊天消息 case 2: // 文件请求 forward(msg); break; case -1: // 用户下线 onlineMap.remove(msg.getName()); broadcast(msg); break; default: break; } }路由逻辑说明type0 时服务器先把用户名和 Socket 关系存起来再广播“XX 上线了”这样其他客户端的在线列表能同步刷新。type1 和 type2 都走 forward区别只是客户端拿到消息后是显示聊天文本还是弹出文件接收确认框。type-1 时只移除在线表不需要再给这个断开的 Socket 转发东西。forward 的转发目标来自 msg.getClients()也就是发送者在界面上勾选的那群人。需要注意转发时要把 msg.name 原样保留否则接收端不知道消息是谁发的。原文里发送者本地会追加一条“我对 B 说”这个记录是本地行为不能依赖服务器回包。这里再强调一次给每个客户端连接创建的 ObjectOutputStream 一定要保存成成员变量整个生命周期复用。如果每次转发都重新 new ObjectOutputStream(socket.getOutputStream())接收端会读到重复的序列化流头然后报错。这个坑在避坑章节会再展开因为它太典型了。4. 文件传输模块不走服务器中转客户端直连才是关键聊天室最容易被老师追问的就是文件传输。原文设计说明里有一句很关键的话“首先通过原有的服务器获得目标客户机的 IP 地址和端口然后在客户机上建立服务器通过要发送的文件的客户机连接接受文件的客户机用 DataInputStream 和 DataOutputStream 来推送输入输出流然后客户机接受并保存。”这就说明文件字节流没有走服务器中转是客户端到客户端的直连。4.1 传输流程请求、确认、建立直连、进度条完整流程拆开是四步发送方在用户列表双击其中一个或多个用户弹出文件选择框选中文件后发一条 type2 的 Message带上文件名和文件大小。服务器把 type2 消息转发给接收方。接收方弹出确认框询问是否接收文件。接收方同意后在自己机器上开一个临时 ServerSocket端口用随机端口再把本机 IP 和临时端口通过服务器回传给发送方。发送方拿到 IP 和端口后新建 Socket 直连接收方用 DataOutputStream 推文件字节接收方用 DataInputStream 收并写到磁盘同时用 JProgressBar 展示进度。这里服务器只当信令通道不碰文件字节。好处是服务器不会因为传大文件而内存暴涨坏处是客户端双方需要能互相访问。课程设计一般都在局域网或本机跑这个前提天然满足。4.2 接收端临时服务器选完路径、先 accept 再回传端口接收端收到文件请求后的处理// 接收端收到 type2 后的处理 int confirm JOptionPane.showConfirmDialog(panel, msg.getName() 想给你发送文件 msg.getFileName() 大小 msg.getSize() KB是否接收); if (confirm JOptionPane.YES_OPTION) { JFileChooser chooser new JFileChooser(); chooser.showSaveDialog(panel); File saveFile chooser.getSelectedFile(); // 在客户机上建立临时服务器0 表示随机端口 ServerSocket tempServer new ServerSocket(0); int tempPort tempServer.getLocalPort(); // 先等待发送方连接再回传端口信息 Socket fileSocket tempServer.accept(); receiveFile(fileSocket, saveFile); // 回传端口给发送方告知可以直接连 Message response new Message(); response.setType(3); response.setName(this.name); response.setInfo(tempPort ); response.setClients(Collections.singleton(msg.getName())); sendMessage(response); }逻辑说明new ServerSocket(0) 是让系统自动分配一个空闲端口避免写死端口导致冲突。这里最重要的顺序是先 accept()再回传端口。如果先回传端口再去 accept发送方可能已经连上并开始写数据而接收端还没进入 accept 状态数据会堆积在系统接收缓冲区小文件能扛住大文件很容易把缓冲区塞满然后连接被重置。参数说明接收端 SaveFile 路径要保证目录存在且可写否则 FileOutputStream 会抛 FileNotFoundException。建议在弹了保存框之后把父目录也校验一遍。4.3 发送端推送DataOutputStream 按字节推别用 ObjectOutputStream发送端拿到 type3 消息里的临时端口后直接连接// 发送端收到 type3 后的推送代码 int tempPort Integer.parseInt(response.getInfo()); String targetIp response.getIp(); Socket fileSocket new Socket(targetIp, tempPort); DataOutputStream dos new DataOutputStream(fileSocket.getOutputStream()); DataInputStream dis new DataInputStream(new FileInputStream(filePath)); byte[] buffer new byte[8192]; int len; long total 0; while ((len dis.read(buffer)) ! -1) { dos.write(buffer, 0, len); dos.flush(); total len; // progressBar 范围是 1-100计算百分比 progressBar.setValue((int) (total * 100 / fileLength)); } dis.close(); dos.close(); fileSocket.close();逻辑说明文件是二进制字节流用 DataOutputStream 按字节推是对的。这里不能图省事把文件内容封装进 Message 再走 ObjectOutputStream因为文件可能很大一次性写入对象会撑爆内存而且序列化对象在接收端还要整体反序列化非常浪费。按 8KB 缓冲区分段读写是最常见的做法。参数说明response.getIp() 需要服务器在转发 type3 时从对应 Socket 的 InetAddress 里取出来附带进去。如果这里写死 127.0.0.1两台机器之间的文件传输会连接失败。buffer 大小用 8192 比较稳妥改到 16384 也可以但不要改成 1MB单次读了太多数据会导致进度条刷新滞后界面感觉像卡死。4.4 传输状态保护isSendFile / isReceiveFile 两个标志位原文关闭按钮的代码里有一段非常关键的判断if(isSendFile || isReceiveFile)正在传输文件时不允许离开。这两个布尔标志是文件传输模块的安全锁。发送方在开始推送文件前把 isSendFile 置 true推送结束后置 false接收方在进入 tempServer.accept() 前把 isReceiveFile 置 true文件保存完后置 false。窗口关闭事件和“关闭”按钮都要检查这两个标志如果为 true 就弹窗提示“正在传输文件中您不能离开”而不是直接 dispose 界面。这个保护如果不做用户传大文件时手滑点了一下关闭Socket 被 GC 或线程中断文件传一半就断了接收方会在磁盘上留下一个残缺文件。这种问题在答辩演示时出现比代码写不出来还尴尬。5. 避坑指南Socket 聊天室最容易翻车的五个细节这一章写的是真实跑这个项目最容易踩的五组坑每一条都是现象、原因、解决连在一起你可以对照自己的代码排查。5.1 现象消息发不出去在线列表正常但聊天内容收不到原因服务器转发消息时多个 ClientHandler 线程共用一个 ObjectOutputStream 写同一个客户端的 Socket。Java 的流对象不是线程安全的两个线程同时 writeObject序列化字节会交错写坏接收端 readObject 抛 StreamCorruptedException或者直接读不到完整对象。解决给每个输出流加锁或者更彻底一点每个客户端连接只用一个线程负责写其他线程把消息丢进队列由写线程统一处理。最简单的同步写法synchronized (oos) { oos.writeObject(msg); oos.flush(); }我一般会把 sendMessage 方法内部就包上 synchronized这样所有往同一个 Socket 写消息的地方都走同一个锁。5.2 现象文件传一半进度条不动然后整个窗口卡死原因接收端选了“接收”并选完保存路径后发送端收到端口信息立刻开始推文件但接收端可能还没执行到 tempServer.accept()或者接收端先回了 type3发送端连过来时 accept 还没准备就绪。连接建立后双方没有做任何握手同步发送端一股脑往流里写接收端读取节奏跟不上缓冲区一满就 connection reset。解决把 accept() 和回传端口信息的顺序颠倒先 accept 再回传保证发送方发起连接时接收方一定在等。如果接收端只有一个文件要收tempServer.accept() 就是阻塞到连接进来为止不存在等不到的问题。另外文件传输应该放到独立线程里不要占用 UI 线程否则网络卡顿时窗口整个失去响应。5.3 现象点关闭窗口后进程还在后台端口被占用原因点击“关闭”按钮后只是设置了 btnNewButton.enabled(false) 并发送 type-1没有关闭 Socket客户端接收线程 CThread 还阻塞在 socket.getInputStream().readObject() 上。Socket 没有 close线程就不会退出Java 进程也退不掉。再次启动服务器时之前残留的进程占着端口直接报 Address already in use: JVM_Bind。解决关闭事件里除了发送 type-1 消息还要显式调 socket.close()。CThread 的 readObject 会因为流关闭而抛 SocketException在 catch 块里把循环标志置 false 并退出线程。服务器端的 ClientHandler catch 块里也要 remove(name) 并关闭对应的 Socket不然服务器那边会保留一堆死连接。5.4 现象中文用户名乱码在线列表出现问号原因user.txt 在 Windows 下默认用系统编码写入中文环境下通常是 GBK而代码里读文件时没有指定编码默认又可能是 UTF-8两边不一致读出来就是乱码。聊天消息走 ObjectOutputStream 序列化字符串内部是 Java 的 UTF-16不受影响所以会出现“聊天正常但账号乱码”这种奇怪组合。解决所有读写 user.txt 的地方都统一用 UTF-8。用 Files.readAllLines 时第二个参数传 StandardCharsets.UTF_8写文件时也显式指定 UTF_8不要用 FileReader/FileWriter 的默认编码。另外还要检查 IDE 编译输出的编码设置Windows 上建议把工程编码整体改成 UTF-8。5.5 现象ObjectOutputStream 反复新建接收端第二次就报 StreamCorruptedException原因ObjectOutputStream 每次 new 出来往底层流写第一个对象前都会先写一个序列化流头固定是 0xAC ED 00 05。如果每次转发消息都 new 一个 ObjectOutputStream 往同一个 Socket 上写接收端的 ObjectInputStream 会认为来了一个全新的对象流但读到的数据却是上一个流的后续字节直接抛 StreamCorruptedException。解决每个 Socket 在连接建立时就创建一个 ObjectOutputStream 并保存成成员变量后续所有 writeObject 都复用这个实例。我在代码里会写一个成员变量 oos初始化后不再 new所有发送消息都调用封装的 sendMessage 方法。记住一个原则一个 Socket 的输出流在整个生命周期里只绑定一个 ObjectOutputStream。6. 验证方法跑通之后用抓包和并发多开确认 TCP 链路真的没问题代码写完能启动只是第一步。我习惯用下面三个手段做验证比单机点几下界面可靠得多。6.1 netstat 看端口和连接状态服务器启动后在命令行执行netstat -an | findstr 8888Linux 上对应是netstat -anp | grep 8888能看到一个 LISTENING 状态的服务器监听以及若干 ESTABLISHED 连接。每多开一个客户端ESTABLISHED 就多一条。如果客户端窗口关了但连接还在 TIME_WAIT说明 socket.close() 没有执行干净进程可能还占着端口。6.2 Wireshark 看三次握手在回环接口抓包过滤条件写 tcp.port 8888。新客户端登录时能看到 SYN、SYNACK、ACK 三次握手关闭时能看到 FIN、ACK、FIN、ACK 四次挥手。这一步能直观验证 TCP 状态机比背教材印象深得多。注意抓回环包要选 lo 接口Windows 上通常是“Npcap Loopback Adapter”。只有 TCP 握手还不够还要看数据包是否有大量重传重传多说明拥塞或接收端处理不过来。6.3 多开客户端做并发和文件传输压力测试最少开三个客户端 A、B、C按照这个清单过一遍A 登录后B 和 C 的在线列表都应该出现 A。A 同时选择 B 和 C 发一条消息B、C 都能收到。A 给 B 传一个 100MB 以上的大文件传输过程中 C 还能正常给 A 发聊天消息证明文件传输没有阻塞聊天线程。传完检查两边文件大小和内容是否一致用 MD5 比对最放心。有条件的话把服务器放到一台机器上客户端分到两台真机或虚拟机确认文件传输的 IP 不是只在 localhost 上成立。这个项目我在本机跑通后又用两台笔记本电脑联机测了一遍才发现接收端回传的 IP 字段不能写死。从那以后我每次写完 Socket 项目都会强制走一遍“端口检查、抓包握手、三端并发、大文件传输”这个流程过了这四关才敢说跑通了。希望帮到你也祝你这门课设拿个好成绩。本文还有配套的精品资源点击获取