简介基于Java的AWT图形库与Socket网络接口实现的局域网群聊工具面向Java网络编程和GUI界面设计初学者演示了如何用文本框、按钮、文本区域等AWT组件搭建聊天窗口并通过ServerSocket与Socket完成内网消息的收发。RAR压缩包共11个文件包含2个可直接阅读的Java源文件、6个已编译class文件以及.classpath、.project、.prefs等Eclipse工程配置包体仅9KB既可导入IDE查看实现也可直接运行class观察效果源码与编译产物分层清晰。已有127人学习下载适合作为计算机网络课程设计或Java进阶练手项目。源码中服务器端与客户端逻辑分离利用多线程分别处理界面刷新和网络监听避免阻塞消息传输采用UTF-8编码发送方把字符串转为字节流接收方再解码回字符串并在界面显示同时服务器维护在线客户端列表将每条消息广播给所有在线用户。学习后可掌握AWT事件模型、TCP Socket编程、多线程同步与I/O流处理等关键技能并能进一步扩展出私聊、文件传输等高级功能为构建更完整的即时通信应用打下基础。 练手项目我做过不少但让我给刚接触Java网络编程的同学推荐一个能写在简历里、又不至于半年都做不完的项目局域网聊天软件一定排在前列。它不涉及复杂业务但把Socket连接、多线程、IO流、协议设计这些面试高频考点全串起来了而且做完能真真切切看到效果。这篇文章就以我实际写的Java局域网聊天软件为例拆一拆整体设计、通信协议、界面线程和联调踩坑的过程。如果你是在校生准备Java面试补项目经验或者在公司内网想搭一个不依赖外网的内部交流小工具下面这套方案可以直接拿去参考。1. 整体设计为什么用Java写局域网聊天软件1.1 需求范围先定住聊天只做这四件事动手写代码之前我最先做的不是建工程而是把需求范围定死。很多项目烂尾就是因为一开始想得太大什么文件传输、视频通话、历史记录全要做结果主流程还没跑通人就先放弃了。我的这个局域网聊天软件第一版只做四件事用户登录、在线列表、群聊广播、私聊单发。外加一个心跳保活让服务端能及时知道谁下线了。这四个功能覆盖了网络编程最核心的通信场景而且每一步都能单独验证。这个范围也方便面试时讲清楚。面试官问“你这个项目解决了什么问题”你可以直接说在局域网内部署一个轻量聊天服务不需要外网启动服务端后各主机通过TCP连接IM的基本收发都能支持。一句话说清楚比东扯西扯强多了。1.2 技术选型原生Socket、Swing与Netty之间的取舍选型时最容易被问到的就是“为什么不用Netty”。我的理由很直接这个项目是为了把网络编程基础吃透原生Socket能让我清楚每一个accept、read、write背后发生了什么。Netty封装了大量底层细节适合生产环境但如果第一次做就上Netty你可能连线程模型都说不清。所以初版用原生ServerSocket和Socket运行起来后再演进去引入Netty也不迟。界面层我选了JDK自带的Swing而不是JavaFX理由更简单不用额外引依赖一个JDK环境就能编译运行对演示和部署都友好。Swing虽然老但做这种工具型界面足够。通信层使用TCP协议做消息传输UDP协议做服务器自动发现两者各司其职。TCP保证消息可靠不丢UDP负责广播探测这个组合是局域网聊天工具很经典的方案。这里还有一个容易被忽略的点Java跨平台优势在这种局域网工具场景里特别明显。团队里有人用Windows有人用macOS只要都装了JDK就能跑不用为每个系统单独编译。2. 通信层拆解消息协议、TCP长连接、UDP主机发现2.1 消息协议一个JSON对象搞定登录、群聊、私聊网络编程里消息协议设计得越清晰后面写业务代码越省心。我用的协议很简单一条消息就一个JSON对象里面五个字段type表示消息类型from是发送者to是接收者content是内容timestamp是时间戳。type我定义了四个常量1代表登录2代表群聊3代表私聊4代表系统通知。后来做心跳时又加了5不影响老逻辑。public class Message { private int type; // 1-登录 2-群聊 3-私聊 4-系统 5-心跳 private String from; private String to; private String content; private long timestamp; }为什么用JSON而不是Java自带的ObjectOutputStream序列化对象最开始我也想图省事直接传对象后来发现JSON有几个好处一是报文可读性强出问题抓包打开一看就知道是哪条消息二是扩展字段容易后面想加个消息ID去重加个msgId字段就行三是即使客户端换成了别的语言也能对接。缺点是要处理粘包问题不过用DataInputStream的writeUTF和readUTF就绕开了。2.2 服务端转发线程池管理下的TCP长连接服务端核心逻辑不复杂监听端口每来一个客户端就建立一个长连接把所有在线客户端的输出流放到一个并发Map里消息进来后按类型和接收者转发出去。我用了ConcurrentHashMap保存用户名到Socket的映射因为HashMap在并发下扩容会死循环这种共享资源必须用并发容器。public class ChatServer { private static final int PORT 8888; private static final MapString, Socket clients new ConcurrentHashMap(); private static final ExecutorService pool Executors.newFixedThreadPool(20); public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(PORT); System.out.println(聊天服务已启动端口 PORT); while (true) { Socket socket serverSocket.accept(); pool.execute(new ClientHandler(socket)); } } }这里有个经验不要每来一个连接就new Thread。Java原生创建的线程没有复用能力连接多了以后频繁创建销毁线程很伤性能项目一开始就养成用线程池的习惯比较好。我用的固定20线程池对局域网聊天场景完全够用一个客户端连接占一个线程20个并发会话足够小型团队使用。ClientHandler里主要做三件事读第一条消息完成登录注册、循环读消息并转发、连接断开时清理资源。核心转发逻辑后面讲私聊群聊时会细说。2.3 自动发现服务器UDP广播三步走设计上有个很贴心的功能客户端不需要手填服务器IP双击打开就能自动找到局域网里的聊天服务。这个功能靠UDP广播实现流程分三步。第一步客户端启动时向255.255.255.255这个受限广播地址发送一个发现包第二步服务端在UDP端口9999收到这个包后解析出客户端的IP和端口回一个包含自身TCP服务地址的应答包第三步客户端收到应答后拿着这个地址去建立TCP连接。// 客户端广播探测 DatagramSocket ds new DatagramSocket(); ds.setBroadcast(true); byte[] buf CHAT_DISCOVER.getBytes(StandardCharsets.UTF_8); DatagramPacket packet new DatagramPacket(buf, buf.length, InetAddress.getByName(255.255.255.255), 9999); ds.send(packet); ds.setSoTimeout(3000);// 服务端响应广播 DatagramPacket request new DatagramPacket(new byte[1024], 1024); ds.receive(request); InetAddress clientAddr request.getAddress(); int clientPort request.getPort(); byte[] resp CHAT_SERVER:8888.getBytes(StandardCharsets.UTF_8); DatagramPacket response new DatagramPacket(resp, resp.length, clientAddr, clientPort); ds.send(response);注意几个坑广播包默认可能被操作系统当作非法地址拦截发送前必须setBroadcast(true)客户端setSoTimeout(3000)是必须的否则发现失败时会一直阻塞如果路由器禁用了广播选项上保留一个“手动填写服务器IP”的入口自动发现失败时退回到手工连接。我在实际测试中遇到过几次广播模式在跨VLAN时直接失效的情况但同一网段内基本稳定。3. 客户端落地Swing界面与收发线程怎么配合3.1 Swing界面布局与EDT更新规则客户端界面我用了Swing主窗口就三个区域左侧是用户列表右侧上方是消息区右侧下方是输入框和发送按钮。顶部一个菜单栏放“连接”“退出”之类的操作。布局上用BorderLayout中间消息区放JTextArea并设置为不可编辑左侧JList显示在线用户底部JTextField接收输入。做出来的效果类似最简版QQ。这里有个绕不开的规则Swing不是线程安全的所有UI更新必须在事件分发线程EDT里执行。我一开始没在意接收线程里直接msgArea.append()界面经常出现偶发性的卡顿和内容不同步后来改成SwingUtilities.invokeLater才稳定。规则很简单跨线程更新界面一律丢给EDT去跑。new Thread(() - { while (true) { String line in.readUTF(); Message msg JSON.parseObject(line, Message.class); SwingUtilities.invokeLater(() - { if (msg.getType() MessageType.CHAT_ALL) { appendMessage(msg.getFrom() msg.getContent()); } }); } }, receive-thread).start();3.2 客户端的接收线程和发送路径很多新手写客户端时只想着一口气读、写结果卡在read方法上UI动不了。正确的做法是连接建立后立刻单独开一个“接收线程”专职循环调用in.readUTF()阻塞等待服务器的消息发送则可以通过事件回调触发直接在按钮监听器里写数据。接收线程收到消息后通过SwingUtilities.invokeLater把内容交给UI线程更新这样网络上出现的消息永远不会卡住界面。发送消息要加一把锁。为什么如果群聊广播时用户狂点发送多个事件线程可能同时往同一个输出流写数据造成消息内容交错。最简单的方式就是把发送逻辑提取成一个sendMessage(Message msg)方法方法上加synchronized保证一个Socket的输出流同一时刻只有一个线程在写。这个细节很小但在高频消息场景下非常关键。3.3 群聊与私聊的转发判定服务端的转发逻辑是整个业务的核心我把它放在ClientHandler里实现。群聊消息发给所有人私聊消息发给指定的目标用户。判断条件就是这个to字段如果to为空说明是群聊如果to有值就取出来对应的Socket单独写入。if (msg.getType() MessageType.CHAT_GROUP) { for (Map.EntryString, Socket entry : clients.entrySet()) { if (!entry.getKey().equals(msg.getFrom())) { DataOutputStream out new DataOutputStream(entry.getValue().getOutputStream()); out.writeUTF(JSON.toJSONString(msg)); } } } else if (msg.getType() MessageType.CHAT_PRIVATE) { Socket target clients.get(msg.getTo()); if (target ! null) { DataOutputStream out new DataOutputStream(target.getOutputStream()); out.writeUTF(JSON.toJSONString(msg)); } }这里我踩过一个大坑线程里每次转发都new DataOutputStream导致消息时好时坏。后来发现一个Socket的输出流应该和连接生命周期绑定在连接建立时就创建而不是每次写的时候再新建。还有写到一半如果目标连接已经断开会抛IOException影响当前线程需要catch住并顺手把断开用户从clients里移除避免“幽灵连接”长期占用线程。4. 联调踩坑连不上、乱码、掉线逐个排查4.1 连不上服务器的排查思路局域网联调最容易出的问题就是客户端connect不上。先别急着改代码按顺序查第一确认服务端进程真的在监听Windows执行netstat -ano | findstr 8888Linux执行ss -lntp | grep 8888第二确认两台机器能互相ping通第三检查Windows防火墙开发阶段最省事的做法是先直接关闭防火墙等系统部署时才去配置精细的入站规则第四排查公司网络是否有VLAN隔离如果跨网段不通需要把客户端部署到同一个网段内。其中防火墙问题是出现频率最高的。默认情况下Windows会拦截一切未经允许的入站连接即使服务端监听正常客户端也会一直卡在connect timeout。我处理这类问题的标准动作是先用telnet 目标IP 8888测端口如果连接失败多半是防火墙或网段隔离而不是代码逻辑问题。4.2 中文乱码与消息粘包的根源消息乱码绝大多数不是JDK的问题而是两端字符集不一致。解决思路其实很粗暴全链路统一UTF-8代码里、配置文件里、数据库连接里全部指定UTF-8。如果是用OutputStreamWriter包装务必指定new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)不要用平台默认编码。另外推荐直接用DataOutputStream.writeUTF和DataInputStream.readUTF这个API内部用Java的modified UTF-8编码两端配套使用天然不会乱码。粘包问题的根源是TCP是字节流没有消息边界。如果自己用BufferedReader逐行读取消息里出现换行符就抓瞎。我为了避免这个问题直接让客户端和服务端都使用readUTF/writeUTF这个封装会在数据前面加上2字节的长度信息读取时按长度切分等于帮你把帧边界问题处理掉了。如果是用Netty就需要自己设计LengthFieldBasedFrameDecoder那是后话。4.3 心跳与离线检测没有心跳机制的时候客户端直接拔网线或强制关机服务端完全感知不到。那这个用户的Socket连接会一直停留在clients集合里发消息给他就会报IO异常。解决思路是每30秒由客户端发一条心跳消息服务端记录每个客户端最近一次心跳时间超过90秒没收到就判定离线移除连接。服务端可以做一个定时清理线程也可以在转发时判断心跳时间。我初期图简单用一个ScheduledExecutorService定期扫描所有连接的时间戳超时的直接关闭socket并从clients移除。同时在Socket读取线程里设置setSoTimeout(90 * 1000)一旦超过90秒没有数据可读抛SocketTimeoutException代码里捕获后主动断开连接。两种方式二选一就行不用重复做。4.4 局域网联调问题速查表我把实际开发中出现频率最高的问题整理成了速查表遇到类似情况直接对号入座现象可能原因处理办法connect超时防火墙拦截、服务端未启动、跨VLAN隔离netstat确认监听telnet测端口临时关闭防火墙或统一网段消息中文乱码两端字符集不一致全链路统一UTF-8优先使用writeUTF/readUTF消息内容粘连错乱字节流缺少消息边界不要自己拼帧用writeUTF/readUTF或自定义长度字段客户端断网服务器无感缺少心跳机制30秒心跳超过90秒没心跳主动断开并清理UI卡顿/内容不同步在接收线程里直接操作UI用SwingUtilities.invokeLater切回EDT线程Address already in use端口被上次运行占用换端口或找出占用进程kill掉广播找不到服务器路由器禁广播或客户端不在同一网段保留手动填写IP入口广播作为辅助发现手段writeUTF写数据时IO异常目标连接已断开转发前判空转发时catch异常并清理离线用户这个表里的每一条我都踩过特别是广播和防火墙这两块费的时间最多。做局域网项目时环境问题往往比代码问题多但解决一次之后你对TCP/UDP的底层行为理解会明显上一个台阶。最后再说一个小技巧调试这类网络程序时最好准备两台真实机器联调至少也得一台物理机加一台虚拟机。只在localhost上跑顺了不算数因为本机回环地址永远不经过防火墙和交换机很多真实网络问题根本暴露不出来。把客户端丢到真实局域网里你会发现遇到的那些问题才是这个项目真正教给你的东西。手写一遍Socket通信真的比背十篇八股文有用。本文还有配套的精品资源点击获取