
简介Chat是一款基于Java开发的局域网聊天软件面向具备Java基础、希望理解网络通信与桌面应用整合的开发者与学习者用于在本地网络环境中实现文字交流与文件传输。压缩包共7个文件约14KB以Java源码文件为核心辅以Eclipse工程配置.project、.classpath、.settings下的prefs、.gitignore、LICENSE与README说明工程结构轻量便于直接导入IDE阅读与运行。已有238人学习下载适合作为小型实战案例参考。代码围绕Socket与ServerSocket建立客户端与服务器连接借助多线程处理并发聊天与文件传输使用Swing或JavaFX构建聊天窗口与文件选择对话框并通过java.io流完成文件读写同时涉及事件监听、对象序列化、异常处理与日志记录等知识点。读者可从中梳理局域网通信的完整链路理解消息收发与文件传输的实现思路并借鉴其工程组织方式作为Java网络编程与GUI练习的起点。1. 局域网聊天工具一个 Java 桌面项目能解决什么实际问题办公室内网断了外网微信登不上钉钉消息发不出去但同事就在隔壁工位——这种场景下一个能在局域网里直接跑起来的聊天工具就是刚需。这个项目用 Java 写了一个局域网聊天软件核心能力两条文字聊天和文件传输。没有服务器依赖没有云端账号启动即用同一网段内互相发现、互相通信。适合谁一是需要在内网环境做即时沟通的小团队二是想拿一个完整 Socket 编程 Swing 界面 文件 IO 的 Java 实战项目来练手的开发者。它不复杂但五脏俱全网络通信、多线程、GUI、文件分块传输该有的都有。下面从架构到代码到改造把这个项目拆开讲清楚。2. 架构与目录一个桌面聊天工具的模块划分2.1 技术选型与整体架构这个项目的技术栈很朴素Java SE 标准库没有引入 Netty、Spring 这类框架。通信层用java.net.Socket和ServerSocket做 TCP 长连接界面层用 SwingJFrame、JTextArea、JList等文件传输走同一条 TCP 通道做分块读写多线程用Thread或ExecutorService处理并发连接。选 TCP 而不是 UDP 的原因很直接文字消息不能丢文件传输更不能丢包TCP 的可靠传输省去了自己实现重传和排序的麻烦。整体架构是典型的 C/S 混合模式——每个客户端既是客户端也是服务端。启动时先开一个监听端口等待别人连接同时可以主动去连别人的 IP。这样不需要一个中心服务器任意两个节点之间都能直连。常见做法是维护一个在线用户列表每个连接对应一个Socket和一个读线程发消息时遍历目标连接写入数据。LanChat/ ├── src/ │ ├── main/ │ │ ├── ChatApp.java // 程序入口启动界面和监听 │ │ ├── ui/ │ │ │ ├── MainFrame.java // 主窗口 │ │ │ └── ChatPanel.java // 聊天面板 │ │ ├── net/ │ │ │ ├── ChatServer.java // 监听端口、接受连接 │ │ │ ├── ChatClient.java // 主动连接、发送消息 │ │ │ └── MessageHandler.java // 消息解析与分发 │ │ ├── file/ │ │ │ └── FileTransfer.java // 文件分块收发 │ │ └── model/ │ │ ├── User.java // 用户信息 │ │ └── Message.java // 消息实体 │ └── resources/ │ └── config.properties // 端口、昵称等配置 └── pom.xml / build.gradle这个目录结构是我按常见 Java 桌面项目习惯整理的实际项目可能略有差异但模块职责基本对应。net包管通信ui包管界面file包管文件传输model包放数据实体职责清晰改起来不会牵一发动全身。2.2 通信协议设计消息怎么区分类型文字和文件走同一条 TCP 流必须有个协议头来区分。最简单可靠的做法是自定义一个定长头部前 4 个字节存消息类型1文字2文件开始3文件数据4文件结束接下来 4 个字节存数据长度后面跟实际内容。这样接收端先读 8 字节头解析出类型和长度再按长度读正文不会出现粘包问题。// 消息协议常量 public class Protocol { public static final int TYPE_TEXT 1; // 文字消息 public static final int TYPE_FILE_START 2; // 文件传输开始 public static final int TYPE_FILE_DATA 3; // 文件数据块 public static final int TYPE_FILE_END 4; // 文件传输结束 public static final int HEADER_SIZE 8; // 4字节类型 4字节长度 } // 发送一条文字消息 public void sendText(String text) throws IOException { byte[] content text.getBytes(StandardCharsets.UTF_8); ByteBuffer buffer ByteBuffer.allocate(Protocol.HEADER_SIZE content.length); buffer.putInt(Protocol.TYPE_TEXT); // 写入类型 buffer.putInt(content.length); // 写入长度 buffer.put(content); // 写入正文 outputStream.write(buffer.array()); outputStream.flush(); }ByteBuffer在这里比DataOutputStream更顺手因为可以一次性拼好头部和正文再写出减少系统调用次数。putInt默认大端序收发两端保持一致就行。参数上注意content.length是字节数不是字符数中文按 UTF-8 编码后一个字符占 3 字节用String.length()会算错必须用getBytes().length。2.3 多线程模型每个连接一个读线程界面不能卡所以网络读写必须放在后台线程。常见做法是每接受一个连接就起一个读线程循环阻塞读消息读到后通过SwingUtilities.invokeLater切回 EDT 更新界面。发送操作可以在调用线程直接写但要注意多个线程同时写同一个OutputStream会串数据得加锁或者用单线程发送队列。// 每个连接对应一个读线程 class ReaderTask implements Runnable { private final Socket socket; private final MessageHandler handler; ReaderTask(Socket socket, MessageHandler handler) { this.socket socket; this.handler handler; } Override public void run() { try (DataInputStream in new DataInputStream(socket.getInputStream())) { while (!socket.isClosed()) { int type in.readInt(); // 阻塞读类型 int length in.readInt(); // 阻塞读长度 byte[] data new byte[length]; in.readFully(data); // 确保读满 handler.dispatch(type, data); // 分发处理 } } catch (IOException e) { // 连接断开从在线列表移除 handler.onDisconnect(socket); } } }readFully是关键普通read(byte[])可能只读到一部分就返回文件传输时会导致数据错位。dispatch里根据类型分派文字消息直接拼到聊天区文件数据块写入临时文件。onDisconnect负责清理资源、刷新用户列表。这里有个细节——socket.isClosed()在对方异常断开时不一定立刻为 true更稳的判断是捕获EOFException或SocketException后退出循环。3. 跑起来从零启动两个客户端互发消息和文件3.1 环境准备与编译运行需要 JDK 8 及以上项目如果是 Maven 结构直接mvn clean package打出 jar如果是普通 Java 项目用javac编译后java运行。Swing 界面在无头环境跑不了必须在有图形界面的机器上启动。两台机器要在同一局域网防火墙放行监听端口默认常见是 8888 或 9999以配置文件为准。# Maven 项目打包 mvn clean package -DskipTests # 启动第一个客户端监听 8888 java -jar target/lan-chat.jar --port 8888 --name Alice # 启动第二个客户端监听 8889连接第一个 java -jar target/lan-chat.jar --port 8889 --name Bob --connect 192.168.1.10:8888参数含义--port是本机监听端口同一台机器跑两个实例必须不同--name是显示昵称--connect是主动连接的目标地址。如果项目没有命令行参数解析就在config.properties里改端口和昵称效果一样。启动后主窗口会显示在线用户列表选中用户即可发消息。3.2 文件传输的完整流程文件传输分三步发送方先发TYPE_FILE_START携带文件名和文件大小然后循环读文件块每块发TYPE_FILE_DATA最后发TYPE_FILE_END。接收方收到 START 时创建临时文件收到 DATA 时追加写入收到 END 时关闭流并重命名。块大小一般取 4KB 到 64KB太小系统调用频繁太大占内存8KB 是个稳妥的默认值。// 发送文件 public void sendFile(File file) throws IOException { // 1. 发送文件开始头文件名|文件大小 String meta file.getName() | file.length(); byte[] metaBytes meta.getBytes(StandardCharsets.UTF_8); sendFrame(Protocol.TYPE_FILE_START, metaBytes); // 2. 分块发送文件内容 byte[] chunk new byte[8192]; try (FileInputStream fis new FileInputStream(file)) { int read; while ((read fis.read(chunk)) ! -1) { byte[] data Arrays.copyOf(chunk, read); // 最后一块可能不满 sendFrame(Protocol.TYPE_FILE_DATA, data); } } // 3. 发送结束标记 sendFrame(Protocol.TYPE_FILE_END, new byte[0]); } private void sendFrame(int type, byte[] data) throws IOException { ByteBuffer buf ByteBuffer.allocate(8 data.length); buf.putInt(type); buf.putInt(data.length); buf.put(data); synchronized (outputStream) { // 防止多线程写串 outputStream.write(buf.array()); outputStream.flush(); } }Arrays.copyOf处理最后一块不满 8KB 的情况否则会把缓冲区里的脏数据也发出去。synchronized (outputStream)保证文字消息和文件块不会交叉写入——如果用户一边传文件一边打字没有这把锁接收端解析头部就会错位这是血泪经验。接收端对应逻辑是START 时用FileOutputStream打开临时文件DATA 时write(data)END 时close()并提示用户保存位置。3.3 界面与消息刷新的线程安全Swing 的所有组件更新必须在事件调度线程EDT上做。网络线程读到消息后直接textArea.append()是典型的翻车点——偶尔正常压力一大就界面卡死或抛异常。正确做法是包一层SwingUtilities.invokeLater。// 网络线程中收到文字消息后更新界面 public void onTextReceived(String from, String text) { SwingUtilities.invokeLater(() - { chatArea.append([ from ] text \n); chatArea.setCaretPosition(chatArea.getDocument().getLength()); // 自动滚到底 }); }setCaretPosition让滚动条自动跟到最新消息不加的话新消息来了用户还得手动往下拉。这个细节虽小但直接影响使用体验。用户列表的刷新同理DefaultListModel的增删也要在 EDT 上操作。4. 避坑与排查局域网聊天项目最容易翻车的几个点4.1 连不上先查防火墙再查网段现象是输入对方 IP 点连接一直超时或拒绝。原因通常两个一是目标机器防火墙没放行监听端口Windows 默认会弹窗询问点了拒绝就静默拦截二是两台机器不在同一网段比如一个连公司 WiFi 一个插网线网关不同。解决办法是先ping通再telnet 目标IP 端口测端口不通就检查防火墙入站规则把 Java 程序或对应端口加白名单。跨网段的情况需要路由器支持普通局域网工具不处理 NAT 穿透。4.2 中文乱码编码两端必须一致现象是发出去的中文对方收到变成问号或方块。原因是发送端用平台默认编码Windows 中文版是 GBK接收端按 UTF-8 解。解决很简单所有getBytes和new String显式指定StandardCharsets.UTF_8Swing 的JTextArea本身用 Unicode 存储不受影响问题只出在网络字节流这一层。文件名的传输同理meta字符串编码要统一。4.3 文件传一半卡住缓冲区没 flush 或读不满现象是大文件传到某个位置双方都卡死。原因一是发送端没flush数据积在缓冲区二是接收端用read(byte[])没循环读满就解析下一帧导致头部错位。解决是发送端每帧写完flush接收端用DataInputStream.readFully读定长头部和正文。另外文件超过 2GB 时int长度会溢出需要改用long存文件大小这是大文件场景必须提前考虑的。4.4 关窗口后进程不退线程没设守护或没关 Socket现象是点了关闭按钮界面没了但任务管理器里 Java 进程还在。原因是监听线程和读线程还在阻塞等待非守护线程会阻止 JVM 退出。解决办法是在窗口关闭事件里遍历所有 Socket 调close()或者把网络线程设为setDaemon(true)。更规范的做法是加一个shutdown()方法统一释放资源在WindowListener.windowClosing里调用。5. 二次开发从能用到好用几个改造方向拿到这个项目后最值得先改的是消息协议的可扩展性。现在的 8 字节头只有类型和长度想加发送者 ID、时间戳、消息序号就得改协议。我一般会把头部扩成固定 JSON 或者加一个扩展字段长度这样后续加已读回执消息撤回不用再动底层读写逻辑。改的时候注意新旧版本兼容——如果两端版本不一致老客户端解析新头部会错位稳妥做法是头部第一个字节放协议版本号收到不认识的版本直接提示升级。第二个改造点是用户发现。现在靠手动输 IP体验很差。局域网内可以用 UDP 广播实现自动发现每个客户端启动时向255.255.255.255的约定端口发一个广播包携带自己的昵称和 TCP 监听端口其他客户端收到后自动加入列表。UDP 广播只在同一网段有效正好匹配这个工具的使用场景。代码上就是开一个DatagramSocket监听广播端口收到包解析后更新DefaultListModel注意去重和超时剔除。第三个方向是消息持久化。现在聊天记录关了就没了加一个本地 SQLite 或者简单的按天文本文件追加启动时加载最近记录。表结构不用复杂messages(id, from_user, to_user, content, type, timestamp)一张表够用文件传输只存路径和元信息不存二进制。查询时按时间倒序取最近 200 条渲染到界面避免一次性加载太多卡顿。验证改造是否成功我的习惯是开三个客户端做交叉测试A 发文字给 B、B 传文件给 C、C 同时给 A 发消息观察是否有串包、乱序、界面卡顿。压力测试可以写个脚本模拟连续发 1000 条消息和 10 个并发文件传输看内存和线程数是否稳定。改造过程中每改一个模块就单独跑一遍双端联调别攒一堆改动一起测出了问题定位成本翻倍。从那以后我每次动网络协议相关的代码都强制先跑一遍文字文件混合并发的冒烟测试再提交这个习惯帮我省了不少回滚的麻烦。希望帮到你。本文还有配套的精品资源点击获取