ARTICLE DETAIL

资讯详情

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

Java实战:用Socket和多线程从零开发局域网聊天室

Java实战:用Socket和多线程从零开发局域网聊天室 简介这是一份基于Java AWT界面库与Socket网络编程实现的局域网群聊软件源代码包面向正在学习Java网络编程、GUI设计或需要完成课程设计的开发者。资源总共11个文件包括2个Java源文件与6个编译后的class文件以及.project、.classpath和Eclipse预置项配置完整保留了可运行程序的工程结构适合导入IDE后直接查看和调试。压缩包仅9KB但代码覆盖了AWT窗口组件布局、服务器端口监听、客户端连接、输入输出流消息收发、UTF-8编解码以及多线程并发处理等关键环节还通过客户端列表广播实现了群聊功能。已有127人学习下载对入门级开发者而言这是一份能快速读懂局域网即时通讯原理的小型完整样例。通过阅读这两个核心源文件既能学习到AWT构造聊天界面的具体写法也能理解同步机制在管理客户端列表和消息转发时的应用。 我是一个Java程序员平时也带过不少刚入门的新人。有段时间团队里好几个新人都在问同一个问题学了那么多Java语法和框架到底能做什么光是增删改查没意思想做个能跑起来的小东西练手又不知道选什么项目。我当时给的建议是做个局域网聊天软件。说真的这个项目适合练手到什么程度它能让你把Java核心的几个大块全串起来Socket网络编程、多线程、IO流、集合类、Swing或JavaFX界面还有最基础的设计模式。不依赖任何第三方框架不用搭数据库单靠JDK就能跑起来做完之后你会有一种“原来学的东西真能组合出东西来”的实感。而且它在实际工作环境中也有真实需求——办公室内部沟通、实验室局域网协作、机房运维临时通信这类场景做个小工具完全没有问题。这篇文章我就从零开始把这个项目的完整思路、关键代码、常见坑全部分享出来适合已经学完Java基础、但还没做过完整项目的读者也适合准备面试时想拿一个“拿得出手的小项目”做谈资的人。1. 项目整体思路先想清楚再动手1.1 客户端-服务器架构为什么是首选做局域网聊天架构上可以选两条路。第一种是点对点直连也就是两台机器直接建立Socket连接。听起来很简单但实际上很麻烦你要知道对方的IP地址、端口还要先有一方监听、另一方主动连接。更麻烦的是如果三个人以上聊天每台机器都得跟其他人分别建立连接连接数呈指数增长代码复杂度直接爆炸。点对点模式的致命缺陷是无法保存聊天状态新加入的人看不到之前的消息也没法知道当前有哪些人在线。第二种就是我现在要讲的客户端-服务器模型也就是经典的C/S架构。所有客户端都只连接一个中央服务器服务器负责消息的转发和广播。这个方案的好处非常明显客户端只需要知道服务器的IP和端口不需要关心其他客户端的存在消息的统一管理和持久化可以放在服务器端新人上线时服务器可以推送当前在线用户列表和历史消息后续想扩展群文件传输、消息记录查询只需要改服务器代码类比一下点对点就像一群人围在一起七嘴八舌说话每个人都要记住谁是谁。而C/S架构就像一个电话总机所有人只要接上总机就能找到其他人。1.2 技术选型与版本说明技术栈我选了最“原生态”的组合Java Socket 多线程 Swing。为什么不用Netty为什么不用WebSocket因为学习阶段的目标是理解底层原理Netty把线程模型、解码头、粘包拆包处理都封装好了你反而接触不到网络通信的本质。等你用原生Socket写完一遍再去看Netty会发现一切都是那么熟悉。JDK版本我用的是Java 8这个版本稳定、兼容性最强大部分公司的生产环境至今还在用。界面用Swing虽然看起来不够现代但它不依赖外部库JDK自带开箱即用。如果你喜欢更漂亮的界面后期可以换JavaFX但核心的网络代码完全不用动。整个项目结构是这样的LanChat/ ├── src/com/lanchat/ │ ├── server/ │ │ ├── ChatServer.java // 服务器启动入口 │ │ ├── ClientHandler.java // 每个客户端连接的处理线程 │ │ └── MessageBroadcaster.java // 消息广播器 │ ├── client/ │ │ ├── ChatClient.java // 客户端启动入口 │ │ ├── ClientListener.java // 客户端接收消息线程 │ │ └── LoginFrame.java // 登录界面 │ └── common/ │ ├── Message.java // 消息封装类 │ └── Protocol.java // 协议常量定义2. 核心功能设计聊天软件到底在聊什么2.1 通信协议自己定一套“聊天暗号”写网络程序第一步不是写代码而是定义协议。协议就是客户端和服务器之间约定好的消息格式就像两个人约好了一门暗号双方都按这个规则解析内容才不会鸡同鸭讲。我定义的消息格式是JSON字符串格式如下{type: login, username: 张三, message: } {type: chat, from: 张三, to: all, message: 大家好} {type: private, from: 张三, to: 李四, message: 下班一起吃饭} {type: logout, username: 张三, message: } {type: userlist, users: [张三, 李四, 王五]}用JSON的好处是易于扩展、可读性强。Java里处理JSON不需要额外引库其实需要我直接用阿里巴巴的fastjson或者谷歌的Gson两个都可以代码里加一个依赖就行。如果你不想引入第三方库也可以用简单的分隔符串比如用特殊字符串分离各个字段但到了后期你会为这个偷懒付出代价因为解析起来会非常痛苦。type字段是消息类型这是整个协议的枢纽login用户登录服务器接收用户名并注册chat群聊消息服务器广播给所有在线用户private私聊消息服务器只转发给指定用户logout用户退出服务器清理在线列表userlist服务器推送在线用户列表2.2 功能清单与优先级我按开发难度和使用频率给功能排了个优先级功能优先级说明用户登录/退出P0最基本的需求系统得有“人”进来群聊P0所有用户都能看到消息在线用户列表P1让用户知道当前谁在线私聊P2点对点沟通面试时加分项历史消息记录P3服务器保存最近N条消息新人上线能看见文件传输P3大数据传输需要拆包和重组界面美化P2如果时间充裕就好好弄P0和P1是必须要做完的否则这个聊天软件名不副实。P2和P3看个人时间我建议至少把私聊做了因为在实现私聊的过程中你会发现消息的路由逻辑比想象中复杂这个复杂度恰恰是面试官最喜欢的讨论点。3. 代码实现从服务器到客户端的完整落地3.1 服务器端C/S架构的心脏服务器的核心职责就一句话维护在线用户集合收发并转发消息。说穿了就是一张Map和一个循环监听。看这个精简版的服务器代码public class ChatServer { // 保存所有在线用户的输出流key是用户名value是对应的PrintWriter private static MapString, PrintWriter onlineUsers 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(); System.out.println(新客户端连接 socket.getInetAddress()); // 每个客户端连接创建一个线程去处理 new Thread(new ClientHandler(socket)).start(); } } // 广播消息给所有在线用户 public static void broadcast(String message) { for (PrintWriter writer : onlineUsers.values()) { writer.println(message); writer.flush(); } } }这里有两个关键点敲黑板。第一为什么用ConcurrentHashMap而不是HashMap因为server要同时处理多个客户端的读写操作多线程并发访问这个Map时HashMap会抛出ConcurrentModificationException或者更阴险地造成死循环。ConcurrentHashMap使用分段锁机制在并发读写场景下性能更好也更安全。第二为什么要给每个客户端创建一个新线程因为accept()方法会阻塞等一个连接被处理后才能接收下一个连接。如果用单线程客户端A连接后一直挂在那边客户端B就连不上了。每一个ClientHandler就是一个独立的“接线员”各自伺候一个客户互不干扰。3.2 客户端处理器每个连接背后的线程ClientHandler是整个服务器里最关键的一个类。它做的事情是读取客户端发来的消息根据消息类型做不同处理。public class ClientHandler implements Runnable { private Socket socket; private BufferedReader in; private PrintWriter out; private String username; public ClientHandler(Socket socket) { this.socket socket; in new BufferedReader(new InputStreamReader(socket.getInputStream())); out new PrintWriter(socket.getOutputStream(), true); } Override public void run() { try { String message; while ((message in.readLine()) ! null) { JSONObject json JSON.parseObject(message); String type json.getString(type); switch (type) { case login: handleLogin(json); break; case chat: handleChat(json); break; case private: handlePrivate(json); break; case logout: handleLogout(); return; // 结束线程 } } } catch (Exception e) { e.printStackTrace(); } finally { handleLogout(); } } }这里有个细节finally块里一定要调用handleLogout()。很多新人会忘记结果就是客户端异常断开比如直接拔网线、强制关进程时服务器上的在线用户列表里留着脏数据别人看到这个用户一直在线但发消息却石沉大海。处理登录的代码逻辑其实很简单private void handleLogin(JSONObject json) { username json.getString(username); ChatServer.onlineUsers.put(username, out); // 广播上线通知 ChatServer.broadcast({\type\:\system\, \message\:\ username 进入聊天室\}); // 推送新的在线用户列表 pushUserList(); }3.3 客户端界面与逻辑的配合客户端我用Swing写了一个简单的界面。别嫌它丑这个项目重点在网络通信界面只是辅助展示工具。但界面代码的结构很重要我建议用MVC的思路分层界面监听事件事件触发后调用业务逻辑发送消息收到消息后刷新界面。客户端核心类ChatClientpublic class ChatClient { private Socket socket; private BufferedReader in; private PrintWriter out; private String username; private JTextArea messageArea; private DefaultListModelString userListModel; public void connect(String host, int port, String username) throws IOException { socket new Socket(host, port); this.username username; in new BufferedReader(new InputStreamReader(socket.getInputStream())); out new PrintWriter(socket.getOutputStream(), true); // 服务器读线程 new Thread(this::listenToServer).start(); } private void listenToServer() { String message; try { while ((message in.readLine()) ! null) { JSONObject json JSON.parseObject(message); String type json.getString(type); if (userlist.equals(type)) { updateUserList(json.getJSONArray(users)); } else if (chat.equals(type)) { String from json.getString(from); String content json.getString(message); appendMessage(from content); } else if (private.equals(type)) { String from json.getString(from); String content json.getString(message); appendMessage([私聊] from content); } } } catch (IOException e) { appendMessage(连接已断开); } } public void sendChatMessage(String content) { JSONObject json new JSONObject(); json.put(type, chat); json.put(from, username); json.put(message, content); out.println(json.toJSONString()); out.flush(); } }必须用独立的线程去读服务器消息。为什么因为InputStream的readLine()方法是阻塞的如果在Swing的事件线程里直接调用readLine()界面就会卡死用户发一条消息后整个窗口就没反应了。这是Swing开发中最常见的坑之一记住一条铁律网络IO永远不要跑在事件分发线程上否则界面就变成了“死界面”。3.4 用户上下线与列表刷新在线用户列表的刷新逻辑是整个项目里最容易出bug的地方。我来拆解一下完整的流程用户A登录服务器把A加入onlineUsers服务器生成新的用户列表广播给所有人每个客户端收到userlist消息后清空当前的列表重新填充注意第3步必须清空后重新填充而不是在原有列表上添加或删除。为什么不增量更新因为服务器只发最新的全量列表客户端不知道谁退出了只能全量替换。如果某用户异常掉线服务器还没来得及广播logout其他用户的列表里就会有这个脏数据。全量刷新虽然笨但至少能保证每个客户端看到的状态是一致的、最终收敛的。在实际测试的时候我发现有时候用户明明下线了列表里还在。原因找到了客户端的Socket不是自己正常关闭的而是进程被杀掉服务器端没有立即感知到连接断开直到下一次写入操作失败才发现。Socket默认的keepAlive要等两个小时才检测一次太慢了。我的解决策略是服务器每隔30秒发一个心跳ping客户端收到后回复pong两次没回就判定掉线。这是企业级的思路先用起来后面再优化。4. 联调、踩坑与优化4.1 本地联调的套路很多人写完了代码卡在怎么测试这一步。两种方式第一种本机自测。开一个服务器再开两个客户端窗口一个用张三登录一个用李四登录。注意本机测试时你完全可以同时跑多个客户端实例不需要多台电脑。客户端的服务器地址填127.0.0.1或localhost就行。第二种局域网联调。找两台连同一路由器的电脑查一下服务器机器的局域网IP用ipconfigWindows或ifconfigLinux/Mac命令查看客户端连接时填这个IP就行。我在实际操作中的经验是如果连接不上第一件事是关防火墙或者放行8888端口。Windows的防火墙默认会拦截外部设备的入站连接这个是局域网联调遇到最多的问题。防火墙放行端口的方式控制面板 - Windows Defender防火墙 - 高级设置 - 入站规则 - 新建规则 - 端口 - 填写8888 - 允许连接。直接关防火墙也行测试阶段省事务必要时再重新打开。4.2 典型问题与排查技巧我在开发过程中踩过不少坑挑几个有代表性的写出来。问题一客户端发消息时服务器报NullPointerException排查后发现客户端发送的对象里有个字段没设置值JSON解析出来是null服务器拿着null去调用方法就崩了。解决办法是在服务器端做非空校验if (json.getString(message) ! null !json.getString(message).trim().isEmpty()) { // 处理消息 }问题二客户端界面乱码这是编码问题。Swing默认使用平台编码Windows下是GBK而网络传输用的UTF-8两边不一致就会乱码。解决方式是在流上统一指定编码in new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF-8)); out new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), UTF-8), true);客户端和服务器都强制使用UTF-8乱码问题彻底解决。问题三多人同时发消息时偶尔有一条消息收不到这是因为Java中的PrintWriter和BufferedReader在处理并发写入时没有做同步。多个线程同时调用println()写socket流可能会造成数据交错导致接收端读取到半个JSON解析失败。解决办法是在广播方法上加synchronized关键字保证同一时刻只有一个线程在往流里写public static synchronized void broadcast(String message) { for (PrintWriter writer : onlineUsers.values()) { writer.println(message); writer.flush(); } }问题四Socket关闭时没通知服务器直接关窗口客户端进程结束TCP连接会被操作系统关闭但服务器端的readLine()会返回null或者抛出SocketException然后进入finally块执行handleLogout()。这是正常的。但如果客户端调用System.exit()时没有先关闭Socket和流可能会造成服务器端的清理逻辑来不及执行。解决方式是给Swing窗口添加窗口关闭事件监听器frame.addWindowListener(new WindowAdapter() { Override public void windowClosing(WindowEvent e) { client.logout(); // 发送logout消息再关闭socket System.exit(0); } });4.3 后续还可以怎么扩展项目做完之后如果还有时间和精力我非常建议往这几个方向扩展对技术的提升会很明显第一把服务端的消息转发改成异步消息队列。现在用的ConcurrentHashMap方式广播是同步阻塞的用户多了之后性能会下降。可以用Java自带的BlockingQueue做一个生产者-消费者模型客户端线程只负责把消息丢进队列一个专门的消息派发线程去消费队列。第二加入文件传输功能。聊天框里可以发图片、发文件。文件传输比文本复杂的地方在于二进制数据的传输需要定义文件名的编码方式、文件大小的传输格式还要做拆包和重组。一个完整的文件传输方案是先发送一个包含文件名和文件大小的metadata消息再单独建立一条Socket连接传二进制数据传完之后发送完成通知。第三加密通信。局域网内的信息并不安全如果聊天内容涉及敏感信息可以对消息体做AES对称加密密钥在登录时通过非对称加密交换。这块是安全方向的基础入门也是面试时可以深聊的亮点。第四消息持久化。把聊天记录存到本地文件或SQLite里服务器重启之后还能恢复历史记录。这个不难但做完之后整个项目的完整性会提高一个档次。5. 最后聊几点真实感触做完这个项目我对网络编程的理解真正做到“打通”了。以前看那些理论比如TCP三次握手、四次挥手、流的阻塞与非阻塞总觉得是与世隔绝的抽象概念。等到自己写的服务端真的能等到一条消息、转发给另一个客户端的时候这些概念才真正落到了实处。我在给新人做代码评审的时候经常强调的一个重点是网络编程的核心不是写出一份能跑的代码而是要想清楚并发、异常断连、消息丢失这些“不顺利”的情况。一个稳定的网络程序90%的代码是在处理异常和边界情况只有10%在处理正常流程。这也是架构师思维和普通码农思维的差距所在。如果你已经照着这篇文章写完了这个项目可以试着再做一个小改动把Swing换成JavaFX或者把Socket换成NIO又或者加一个简单的登录密码认证。每一次改动都会让你对Java的理解更深一层。转个方向想这篇文章对你最大的价值其实不是给了你一套代码而是让你看到一个看似简单的项目背后要考虑的细节那么多。真正的技术成长就藏在这些细节里。本文还有配套的精品资源点击获取
返回列表