简介:一套基于JAVA的局域网聊天室系统,专为毕业设计或课程设计准备,适合计算机相关专业学生参考与学习。资源提供完整源代码与毕业论文,涵盖聊天客户端、服务端及界面交互,能帮助理解Socket通信、多线程处理等关键网络编程技术。压缩包共239个文件,以h头文件、cpp源文件为主,辅以doc论文文档、exe可执行程序及配套资源文件,总大小14.13MB,目录结构清晰便于按模块查阅。目前已有107人参与学习下载。通过源码、论文及可运行程序,读者可以系统掌握从需求分析、系统设计到编码实现的完整流程,是一份可借鉴的局域网通信开发完整资料。
1. JAVA基于局域网的聊天室系统:源代码加论文,课设答辩的一套成型方案
JAVA基于局域网的聊天室系统,是毕业设计和课程设计里生命力最长的题目之一。它的本质是一个C/S架构的即时消息转发程序:在教室某台机器上启动服务端,其他机器上的客户端通过TCP Socket连上来,就能互发文本消息,并附带在线用户列表和系统提示。它够简单,一张Socket时序图能讲清核心;也够厚,多线程、集合安全、IO流、Swing全部踩到,正好覆盖Java课程的主要知识点。
对时间紧的课设学生,源代码加论文这套组合能直接沉淀成答辩材料;对想冲高分的学生,它又留出私聊、文件传输、消息存储三个明确可写的大扩展点。下面按搭环境、跑通、写论文的顺序,把代码、参数和踩过的坑摊开讲。
2. 技术选型与协议设计:Socket加Swing的组合凭什么能沿用十年
2.1 为什么不用Netty和WebSocket:课设选型的核心标准
做局域网聊天室,摆在面前的候选方案有三个:原生Socket、Netty、WebSocket。Netty确实性能强,但它的NIO事件循环、ByteBuf、解码器链,新手没个两周摸不透。课设周期通常只有两到四周,把时间砸在框架上,意味着你的论文只能写"我调通了Netty的demo",而不是"我设计并实现了一个系统"。WebSocket走HTTP升级握手,需要挂Tomcat或嵌入式容器,项目性质就变成了Web应用,论文要解释Servlet、握手协议,反而偏离了Java核心课程要求。原生Socket虽然代码量大一点,但TCP三次握手、阻塞IO、多线程这些问题全部能落在你自己的代码里,答辩时老师顺着问下去,你每一层都讲得清。
界面层的选择同样关键。Swing从JDK 1.0就内置,老机房的教学机器也能流畅跑,不依赖任何外部下载;JavaFX在JDK 8里还要单独配置,JDK 11之后更是和JDK包分离,离线教室装起来非常痛苦;AWT太简陋,画出来的聊天界面不像"系统"。所以这个标题下的主流做法,就是Socket做通信、Swing做界面,两个都是JDK自带的东西,拿到源代码后在任何装了JDK的机器上都能直接编译运行。
| 方案 | 额外依赖 | 涉及知识点 | 课设周期 |
|---|---|---|---|
| Socket + Swing | 无 | TCP、多线程、IO流、事件模型 | 2周内 |
| Netty | 需要 | NIO、EventLoop、ByteBuf | 4周打底 |
| WebSocket + Tomcat | 需要 | HTTP升级、Servlet容器 | 3周且偏Web |
2.2 私有协议:用TYPE字段加分隔符把五类消息装进一行
Socket传输的是字节流,TCP协议本身不帮你划分"一条消息"的边界,所以应用层必须自己定义消息格式。课设里最常见的做法是行协议:每条消息占一行,以换行符结尾,服务端用BufferedReader.readLine()逐行读取,客户端用PrintWriter.println()逐行发送。行内再用竖线分隔字段,格式固定为TYPE|SENDER|CONTENT三部分。
| 消息类型 | 格式示例 | 方向 |
|---|---|---|
| LOGIN | LOGIN | 张三 |
| CHAT | CHAT | 张三 |
| LOGOUT | LOGOUT | 张三 |
| SYSTEM | SYSTEM | 系统 |
| USERLIST | USERLIST | 系统 |
我一般建议坚持用这种简单分隔符,而不是引入JSON。原因有两个:一是课设不需要引入jackson这类依赖,多一个jar包就多一个版本兼容问题;二是"设计了一个应用层私有协议"这句话放进论文里,本身就是一个小亮点,答辩时你可以直接画一张字段表解释为什么选竖线做分隔符。需要注意split("\\|", 3)里的第三个参数3,它告诉JDK只按前两个竖线拆分,这样消息内容里即使出现竖线也不会被错误截断。另外一个硬性约束是内容字段里不能出现换行符,否则readLine()会提前读到半条消息,第5章会专门说这个坑。
3. 服务端实现:ServerSocket端口绑定、线程池与在线用户管理
3.1 服务端入口:绑定端口与线程池参数
服务端要做的事很清晰:监听端口、接受连接、为每个连接创建一个处理线程、维护在线用户表。下面是入口代码,两个类放在同一个包下,在线用户表和广播方法定义为static,方便ClientHandler直接引用。
// ChatServer.java - 聊天室服务端入口 public class ChatServer { // 监听端口:客户端连接时必须使用同一个端口 private static final int PORT = 8888; // 在线用户表:key为用户名,value为对应该客户端的输出流 static final Map<String, PrintWriter> clients = new ConcurrentHashMap<>(); public static void main(String[] args) throws IOException { // 不传IP地址时默认绑定所有网卡,局域网内其他机器才能连进来 try (ServerSocket serverSocket = new ServerSocket(PORT)) { System.out.println("[系统] 聊天室服务端启动,监听端口:" + PORT); // 固定线程池控制并发上限,防止演示时连接频繁开关拖垮服务 ExecutorService pool = Executors.newFixedThreadPool(20); while (true) { // accept() 是阻塞的,每来一个连接就交给线程池处理 Socket socket = serverSocket.accept(); System.out.println("[连接] 新客户端接入:" + socket.getInetAddress()); pool.submit(new ClientHandler(socket)); } } } }代码里有几个参数值得说明。new ServerSocket(PORT)不传IP地址,意味着监听本机所有网卡地址,如果改成绑定127.0.0.1,那局域网内的其他机器永远连不进来,这是第5章第一个坑的根源。线程池固定为20,是因为课设机房通常是40人以内,每人开一两个客户端绰绰有余;如果改用CachedThreadPool,连接频繁开关时会无限创建线程,老机器直接卡死。try-with-resources是JDK 7引入的写法,服务端进程关闭时ServerSocket会自动释放端口,不用手写close()。
3.2 ClientHandler:单连接的消息循环与广播处理
每来一个客户端,线程池就分配一个ClientHandler实例。这个类的run()方法是一个典型的阻塞读循环:readLine()一直在等消息,等到了就按协议拆字段、分类型处理。
// ClientHandler.java - 单个客户端的消息处理线程 public class ClientHandler implements Runnable { private final Socket socket; private final PrintWriter out; private String userName; public ClientHandler(Socket socket) throws IOException { this.socket = socket; // true 表示自动flush:每println一条消息立即发出,不攒缓冲 this.out = new PrintWriter(socket.getOutputStream(), true); } @Override public void run() { // 显式指定UTF-8,避免Windows默认GBK导致的乱码 try (BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(), "UTF-8"))) { String line; while ((line = in.readLine()) != null) { // 协议格式: TYPE|SENDER|CONTENT,limit=3 让内容里可以带竖线 String[] parts = line.split("\\|", 3); if (parts.length < 3) { continue; } String type = parts[0]; String sender = parts[1]; String content = parts[2]; if ("LOGIN".equals(type)) { this.userName = sender; ChatServer.clients.put(sender, out); ChatServer.broadcast("SYSTEM|系统|欢迎 " + sender + " 加入聊天室"); ChatServer.broadcastUserList(); } else if ("CHAT".equals(type)) { ChatServer.broadcast("CHAT|" + sender + "|" + content); } else if ("LOGOUT".equals(type)) { ChatServer.clients.remove(sender); ChatServer.broadcast("SYSTEM|系统|" + sender + " 已退出聊天室"); ChatServer.broadcastUserList(); break; // 退出循环,连接会在finally里关闭 } } } catch (IOException e) { // 客户端断网或强关窗口时,readLine抛异常,在这里善后 if (userName != null) { ChatServer.clients.remove(userName); ChatServer.broadcast("SYSTEM|系统|" + userName + " 掉线了"); ChatServer.broadcastUserList(); } } finally { try { socket.close(); } catch (IOException ignored) { } } } }这段代码的细节决定了课设演示时翻不翻车。PrintWriter第二个参数传true是自动flush,不传的话消息会积在缓冲区里,在线消息要等缓冲满才发出去,演示现场会变成"我说完话半分钟对方才看到"。所有Reader和Writer都显式指定UTF-8,是因为Windows默认字符集是GBK,Linux和macOS是UTF-8,两端不统一就出乱码。split("\\|", 3)的limit参数是3,这样内容字段即使包含竖线也不会被拆散。在线用户表用ConcurrentHashMap而不是HashMap,是因为多个ClientHandler线程会同时读写这张表,HashMap在并发put时可能形成环形链表导致CPU飙到100%,这个问题在java面试题里被问过无数次,写代码时直接避开。
3.3 广播与在线列表:并发修改下的两个隐蔽点
广播就是把一条消息发给在线用户表里的每一个输出流,在线列表则把用户名的集合拼成字符串下发。这两个方法放在ChatServer里,负责全局的转发工作。
// ChatServer.java - 广播与在线列表下发 static void broadcast(String message) { for (PrintWriter writer : clients.values()) { writer.println(message); } } static void broadcastUserList() { String userList = String.join(",", clients.keySet()); broadcast("USERLIST|系统|" + userList); }ConcurrentHashMap的迭代器是弱一致性的,遍历过程中其他线程往表里put或remove,不会抛ConcurrentModificationException,顶多这次广播漏掉刚加入的客户端,对聊天室场景无伤大雅。但这里有个黑匣子要心里有数:PrintWriter不会抛出IOException!println()写到一个已经断开的Socket上时,只是内部置一个错误标志,消息就悄悄丢了。所以当客户端强关窗口而服务端还没读到EOF时,这个"死连接"的writer还留在在线表里,广播会一直对它静默失败。论文的改进方向里写一条"定期心跳检测并清理失效连接",就是这个问题的正解。
4. 客户端实现:Swing线程模型与Socket收发拆成两条线
4.1 不理解EDT,界面假死就会找上你
Swing界面卡死这个问题,几乎每个做Socket聊天室的学生都会遇到,区别只是发生在答辩现场还是自己电脑上。Swing是单线程模型,所有界面绘制和事件回调都运行在EDT(Event Dispatch Thread)上。如果直接在"登录"按钮的点击回调里写while循环读Socket,readLine()一阻塞,整个窗口的绘制事件、鼠标点击全部排队等待,界面就彻底"死"了——点哪儿都没反应,关闭按钮也点不动,只能去任务管理器结束进程。
正确的线程划分是:界面操作留在EDT,Socket接收消息单独开一个后台线程,后台线程拿到消息后通过SwingUtilities.invokeLater()把界面更新任务交还EDT执行。记住这条规则,客户端代码的骨架就稳了。
4.2 连接、收发与登录:客户端核心代码
客户端的网络部分比服务端简单,就是建连接、发协议、收消息三件事。
// ChatClient.java - 客户端核心:连接、接收、发送 public class ChatClient { private Socket socket; private PrintWriter out; private JTextArea messageArea; private String userName; // 登录按钮回调:建立连接并注册用户名 private void connect(String serverIp, int port, String userName) throws IOException { this.userName = userName; socket = new Socket(serverIp, port); out = new PrintWriter(socket.getOutputStream(), true); // 先发一条LOGIN协议通知服务端"我来了" out.println("LOGIN|" + userName + "|"); // 接收消息交给独立线程,避免阻塞Swing的EDT线程 new Thread(this::receiveLoop, "receiver-thread").start(); } // 收消息循环:只在后台线程里运行 private void receiveLoop() { try (BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream(), "UTF-8"))) { String line; while ((line = in.readLine()) != null) { String[] parts = line.split("\\|", 3); if (parts.length < 3) { continue; } // 跨线程更新界面必须通过invokeLater切回EDT String type = parts[0]; String sender = parts[1]; String content = parts[2]; SwingUtilities.invokeLater(() -> showMessage(type, sender, content)); } } catch (IOException e) { SwingUtilities.invokeLater(() -> messageArea.append("[连接已断开]\n")); } } // 发送消息:按钮点击时调用,发出的是CHAT协议 private void sendChat(String text) { out.println("CHAT|" + userName + "|" + text); } }这里的serverIp是服务端那台机器的局域网IP,Windows下用ipconfig查IPv4地址,Linux下用ip addr查,不要填127.0.0.1。接收循环里每次拿到消息都包一层invokeLater(),这是Swing跨线程更新的硬性要求,不包的话轻则界面刷新延迟,重则直接抛异常把接收线程打死。logout时的处理也值得注意:如果先关Socket再退出循环,readLine()会返回null正常结束;如果直接强关窗口,会抛IOException,两条路径都要保证服务端那边能收到善后消息,所以主动退出时记得补发一条LOGOUT协议,而不是直接closeSocket。
4.3 界面组装与回车发送:三个组件的关键设置
聊天室界面通常分成三个区域:中间的消息显示区、右侧的在线用户列表、底部的输入框加发送按钮。
// ChatClient.java - 界面组装 JTextArea messageArea = new JTextArea(); messageArea.setEditable(false); // 消息区只读,像聊天记录 JScrollPane msgScroll = new JScrollPane(messageArea); JList<String> userList = new JList<>(); JScrollPane userScroll = new JScrollPane(userList); JTextField inputField = new JTextField(); JButton sendButton = new JButton("发送"); // 回车键和按钮复用同一个发送动作 inputField.addActionListener(e -> onSend()); sendButton.addActionListener(e -> onSend());setEditable(false)让消息区变成只读,防止用户误改历史记录,也让界面观感上更像一个"聊天记录面板"。JTextField的addActionListener()是Swing内置的回车事件回调,不用自己写KeyListener去判断键值,这是新手最容易写得啰嗦的地方。布局上用BorderLayout:消息区占CENTER,用户列表靠EAST,输入区和按钮放SOUTH。消息区的文本追加建议用一个showMessage(String type, String sender, String content)方法统一处理——CHAT类型显示"张三:内容",SYSTEM类型显示"【系统】内容",这样所有逻辑收拢在一个方法里,界面代码不会乱。
5. 局域网聊天室避坑清单:连不上、乱码、卡死,五个最容易翻车的地方
5.1 服务端能启动、本机连得上,教室另一台机器却超时
现象:服务端正常启动,客户端在服务端本机用127.0.0.1能连上,换到教室另一台机器用服务端的局域网IP就连不上,一直超时。
原因:最常见的有三个。一是服务端把ServerSocket绑定到了回环地址,比如new ServerSocket(port, 50, InetAddress.getByName("127.0.0.1")),这样只有本机能访问;二是Windows防火墙默认拦截外部程序入站连接,Java进程第一次监听端口时会弹窗,没点"允许"就被拦了;三是两台机器不在同一网段,教室的AP隔离策略会让客户端根本找不到服务端。
解决:服务端代码只用new ServerSocket(PORT),不传第三个参数。在服务端机器上用ipconfig查无线网卡的IPv4地址,先让客户端ping这个地址确认网络通。防火墙设置里找到"允许应用通过防火墙",把javaw.exe和java.exe都勾上。如果课堂无线网络开了客户端隔离,直接拿一根网线把服务端接到同一台交换机上,这是物理层面最省事的后悔药。
5.2 中文变乱码:两个字符集不统一的典型症状
现象:服务端控制台打印出来的消息是乱码,比如"浣犲ソ"这种莫名其妙的组合,或者客户端聊天区全是问号。
原因:InputStreamReader和OutputStreamWriter不传字符集参数时,用的是操作系统默认编码。Windows中文版默认GBK,Linux和macOS默认UTF-8,服务端用GBK编码发送,客户端按UTF-8解码,出来就是乱码。这个症状在服务端和客户端跑在不同操作系统的机器上时特别常见。
解决:所有Socket输入输出流的创建代码,一律显式指定UTF-8。服务端ClientHandler里和客户端receiveLoop里,new InputStreamReader(socket.getInputStream(), "UTF-8")两边保持一致。另外还要确认IDE里源文件的保存编码也是UTF-8,否则代码里的中文字符串本身就是乱码的。改完这两处,再重启两端验证一遍。
5.3 消息里的换行把行协议拆坏
现象:客户端从Word或网页里复制一段带换行的文字粘贴到输入框发送,服务端把一条消息拆成了两三条,甚至客户端那边的界面直接错位。
原因:行协议以换行符作为消息边界,readLine()读到\n就认为一条消息结束了。内容里一旦混入\n,协议就被拦腰截断,后半段文本会被当成一条新的逻辑消息解析,导致字段错位。
解决:在发送前的入口处过滤换行符,text.replaceAll("[\\r\\n]+", " ")把换行替换成空格,同时给输入框加一个长度限制(比如200字),从源头上杜绝协议被拆坏。如果你想在论文里体现一点设计深度,可以把"内容字段禁止包含换行符,发送端负责清洗"写进协议设计那一节,说明这一约束的动机和代价。
5.4 用户重名把在线记录顶掉,先来者从此收不到消息
现象:两个客户端用同一个名字登录,第二个登录成功后,第一个客户端界面看起来还连着,但发的消息没人收到,收也收不到别人的消息。
原因:服务端用用户名做Map的key,第二个连接clients.put(sender, out)把第一个连接的输出流覆盖了。广播时遍历Map只拿到新writer,旧writer对应的Socket还活着,但已经不在Map里,消息自然到不了第一个客户端。
解决:登录时先检查clients.containsKey(sender),如果存在就向新连接回一条SYSTEM|系统|用户名已被占用然后关闭该Socket,而不是覆盖旧记录。这是一行代码就能解决的问题,但大多数课程设计源码里都没处理。建议把它写进论文的"系统模块设计"部分,作为登录去重的一个细节,答辩时老师问起在线列表怎么做一致性维护,你直接拿这段设计答。
5.5 端口被占用导致服务端起不来
现象:改完代码重新启动服务端,控制台抛出java.net.BindException: Address already in use: JVM_Bind,服务端直接退出。
原因:上一次运行的服务端进程没有退出。IDE里红色方块按钮没点,或者上次在命令行启动的窗口还挂着,端口就被旧进程占着。8888这个端口本身也是很多常用软件的默认端口,撞车的概率不小。
解决:Windows下先netstat -ano | findstr 8888查到占用端口的PID,然后taskkill /PID <PID> /F强制结束;Linux下用ss -ltnp | grep 8888找到PID再kill。嫌麻烦的话直接把端口改成40000以上的冷门区间,比如45678,撞车概率大幅下降。改端口时记住服务端和客户端是同一个常量,别只改了一头。
6. 从跑通到答辩:论文扩展与演示预案
6.1 论文创新点:三个可选扩展里挑一个写深
把基础聊天室跑通之后,论文的创新点部分不能空着。最常见的做法是从下面三个扩展里挑一个,用一到两周做完,写成独立的一章。
| 扩展方向 | 关键代码点 | 论文可以展开的内容 |
|---|---|---|
| 私聊 | 协议增加TO字段,服务端按目标定向转发 | 协议扩展设计、HashMap定向路由 |
| 文件传输 | 另开一条Socket连接,用DataOutputStream传二进制 | 字节流与字符流的区别、进度显示、断点续传 |
| 历史消息 | JDBC连SQLite或直接写文件 | 数据表设计、消息落盘与查询 |
我一般建议选私聊,因为它和现有架构的耦合最小——协议从三字段变四字段,服务端路由逻辑加一个判断,客户端弹一个会话窗口,一周时间足够做完,答辩时能讲清楚的地方也最多。选完之后写论文要围绕"改动前和改动后"的对比展开:协议字段表怎么变的、服务端转发流程怎么走的、本地测试结果是什么,这三张图就是这一章的核心内容。
6.2 答辩演示预案:三个不装不知道的细节
演示前的准备工作比代码本身更能决定答辩效果。第一,服务端那台机器的IP要设成静态的,教室DHCP随时可能换IP,客户端代码里写死的IP一旦失效,现场就是大型翻车现场。稳妥做法是在客户端加一个登录对话框,用JOptionPane.showInputDialog让用户输入服务器IP,端口做成文本框可改,这样不管网络环境怎么变都能连上。第二,提前准备至少三台设备的联调截图:两台真实机器加一台本机回环,分别截图登录、聊天、掉线三个场景,放进PPT和论文里。第三,演示时主动做一次"强关客户端"的操作——关掉一个客户端窗口,让服务端打出"XXX掉线了"的系统消息,这个动作能直观展示你的代码处理了异常断开的路径,大多数没做心跳检测的版本在这一步会露怯。
我当年第一次演示时,就是在讲台上对着ipconfig查了半天,后来发现是防火墙没放行Java。所以现在凡是带网络的课设,我都会先跑一遍两台真机联调、端口查询、防火墙检查,再正常关机重启服务端验证一次,确认自动释放了端口再上台。这套流程走完,基本就稳了。希望帮到你。
本文还有配套的精品资源,点击获取