本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套纯Java SE开发的局域网实时聊天解决方案,不依赖任何第三方库,由ChatServer服务端和ChatClient客户端组成。服务端基于Socket监听端口,支持多客户端并发接入,负责消息广播与在线状态管理;客户端使用AWT构建轻量图形界面,具备发送文字、实时接收群聊消息、独立收发线程等基础功能。所有源码均为标准.java文件,附带编译后的.class文件,结构清晰,开箱即用:先运行ChatServer启动服务,再启动多个ChatClient实例即可组成本地聊天室。适用于内网环境下的小范围协作、教学演示或Java网络编程入门实践,消息以明文传输,无用户登录、权限控制或加密机制,部署简单,仅需JRE环境即可运行。

1. 项目概述:为什么一个“简陋”的Java聊天工具值得你花时间细读?

你可能第一眼看到这个标题就下意识划走——“Java SE原生实现”“AWT界面”“局域网聊天”,听起来像教科书里被翻烂的课后习题。但我想先说一句:这恰恰是它最珍贵的地方。 它不是用Spring Boot封装了十层网络抽象、不是靠JavaFX渲染出炫酷动画、更不是调用WebSocket库几行代码就跑起来的“玩具”。它是一把裸露着铜线与焊点的万用表,所有信号走向、电流路径、阻抗匹配,都清清楚楚摆在你眼皮底下。

我带过三届Java入门班,每次讲到ServerSocketSocket时,学生眼睛里总闪着一种混合着敬畏与困惑的光。他们能背出“三次握手”,却说不清accept()返回的那个Socket对象到底在内存里长什么样;他们知道“多线程处理并发”,但一写while(true) { socket.getInputStream().read() }就卡死整个UI;他们抄完代码能跑通,可一旦服务端改个端口号、客户端连错IP,立刻陷入“控制台没报错但就是收不到消息”的黑洞。而这个项目,就是为填平这些黑洞而生的——它不追求功能完整,但每一步都拒绝黑箱;它不炫技,但每个类、每行关键注释都在回答“为什么必须这样写”。

关键词里,“Java聊天工具”是目标,“局域网聊天”是边界,“AWT界面”是约束,“Socket服务端”是骨架,“ChatClient”是触点。这五个词串起来,就是一个极简但自洽的通信闭环:服务端监听固定端口(比如8080),客户端通过IP+端口发起TCP连接,建立双向字节流通道,消息以\n为分隔符逐行传输,服务端收到一条就广播给所有已连接的客户端Socket输出流。没有JSON序列化,没有心跳保活,没有断线重连——它只做一件事:让两台电脑在同一网段里,用最原始的方式“说话”。

它适合谁?不是要上线的工程师,而是刚学完ThreadInputStream、正对着java.net包文档发懵的初学者;是想亲手拆解“网络编程”这台机器、看清活塞如何往复、曲轴怎样转动的实践者;也是需要一份干净、无污染、可逐行调试的教学素材的讲师。它运行起来界面朴素得近乎寒酸——一个文本框、一个发送按钮、一大片滚动消息区,但正是这份朴素,让你一眼就能定位到TextArea.append()在哪一行被调用,BufferedReader.readLine()卡在哪个环节,PrintWriter.println()为何必须加换行符才能触发对方读取。

我试过把它部署在办公室老旧的Windows 7工控机、MacBook Air的虚拟机、甚至树莓派4B上,只要JRE 8+环境就稳稳跑起来。它不挑硬件,不耗资源,启动服务端后netstat -an | grep 8080能看到明确的LISTENING状态,开三个客户端,彼此发“你好”“收到”“测试完成”,消息毫秒级抵达——这种确定性,在动辄依赖N个starter、配置几十个yaml字段的现代框架里,反而成了稀缺品。

所以别被“AWT”劝退。它不是过时,而是精准克制:不需要事件分发线程(EDT)的复杂调度,不需要布局管理器的嵌套陷阱,Frame+TextArea+TextField+Button四件套,足够承载一次完整的请求-响应交互。它的价值不在界面,而在那背后237行ChatServer.java里对ArrayList<PrintWriter>的线程安全操作,在于ChatClient.java第89行那个new Thread(new ClientReader()).start()所揭示的I/O阻塞本质。接下来,我们就一层层剥开这个看似简单的外壳,看看里面到底藏着多少被教科书省略的实战细节。

2. 整体架构与设计思路:为什么选择Socket而非NIO?为什么坚持AWT?

2.1 服务端核心逻辑:一个精简但健壮的广播中枢

ChatServer的设计哲学是“最小可行广播器”。它不做用户管理(没有用户名/密码),不存历史消息(内存中只保留当前连接),不处理离线消息(断开即清除)。它的全部职责浓缩成三件事:接人、收话、传声。这种极致简化不是偷懒,而是为了凸显网络编程中最本质的矛盾——连接管理与数据分发的耦合问题

传统思路可能让每个客户端连接对应一个独立线程,线程内同时处理读消息和写广播。但这样会导致两个致命问题:一是当某个客户端网络卡顿,readLine()阻塞,整个线程挂起,其他客户端的广播也会延迟;二是广播时需遍历所有连接并write(),若某客户端已断开但Socket未及时关闭,write()会抛出IOException,不捕获就会导致整个广播循环中断。ChatServer的解法很直接:读写分离 + 连接池兜底

它用一个ArrayList<PrintWriter>维护所有活跃客户端的输出流(注意,不是Socket,而是包装后的PrintWriter),这个列表被声明为static且用synchronized块保护——这是初学者最容易忽略的线程安全点。为什么不用CopyOnWriteArrayList?因为写操作(添加/移除)远少于读操作(广播),synchronized的轻量级锁比COW的数组复制更高效。而每个客户端连接由独立线程处理输入,主线程只负责accept()和广播,彻底解耦。

提示:ChatServer.java第45行writers.add(out)前的synchronized(ChatServer.class)绝非多余。我曾删掉它测试并发接入,当第5个客户端连接瞬间,第3个客户端的out.println()突然抛出ConcurrentModificationException——因为ArrayList的迭代器检测到结构变更。这个细节,教材里通常只写“要用线程安全集合”,却不说清为什么Vector在这里不合适(Vector所有方法同步,粒度太粗,广播时会阻塞新连接)。

2.2 客户端双线程模型:AWT的阻塞天性和破局之道

ChatClient的AWT界面看似简单,实则暗藏玄机。AWT的事件处理是单线程的(AWT Event Dispatch Thread),所有按钮点击、文本输入都排队执行。如果把socket.getInputStream().readLine()直接写在actionPerformed()里,点击“发送”按钮后,整个界面会瞬间冻结——因为readLine()是阻塞调用,直到收到换行符才返回,而服务端广播消息时,客户端正在等待用户输入,根本没在读!这就是经典的“GUI线程被I/O阻塞”陷阱。

解决方案是双线程分工:主线程(AWT EDT)只负责界面交互和发送消息;另启一个守护线程(ClientReader),专职监听Socket输入流。ClientReaderrun()方法里是一个永真循环:while ((msg = in.readLine()) != null) { textArea.append(msg + "\n"); }。这里有两个关键点:第一,in.readLine()的阻塞只影响ClientReader线程,不影响界面响应;第二,textArea.append()必须在AWT EDT中执行,否则会抛出IllegalThreadStateException。因此ChatClient第126行用了SwingUtilities.invokeLater()(注意:虽然用AWT组件,但invokeLater对AWT同样有效),将UI更新任务提交给事件队列。

注意:ChatClient.java第102行new Thread(new ClientReader()).start()后,没有对线程引用做任何保存或管理。这是教学项目的刻意为之——避免引入ThreadGroupExecutorService等概念干扰主线。但在真实项目中,你需要持有线程引用并在窗口关闭时调用interrupt(),否则程序退出后线程仍在后台运行,占用Socket连接。

2.3 为什么拒绝NIO?为什么拥抱AWT?

有人会问:既然要学网络编程,为什么不直接上SelectorByteBuffer?答案很实在:NIO的抽象层级太高,会掩盖TCP连接的本质Selector帮你轮询多个Channel,ByteBuffer帮你管理缓冲区,但初学者根本看不到connect()失败时IOException的具体类型(Connection refused vs Timeout),也理解不了OP_READ就绪不代表能读满整个消息——可能只读到半条。而SocketgetInputStream()返回InputStreamread()返回int,这种与文件I/O一致的语义,让学习曲线平滑得多。

至于AWT,选择它不是怀旧,而是可控性优先。Swing虽强大,但JTextAreaDocument模型、JScrollPane的视图同步、EventQueue的嵌套调度,会让调试变得复杂。AWT的TextArea是纯文本容器,append()就是追加字符串,没有事件过滤、没有撤销栈、没有复杂的布局约束。当你在ClientReader线程里调用textArea.append()时,AWT会自动将操作序列化到EDT,这种“隐式线程切换”比手动管理SwingWorker更贴近底层逻辑。

我做过对比实验:用Swing重写客户端,增加一个“在线用户列表”功能,结果发现DefaultListModeladdElement()必须在EDT中调用,而服务端推送的用户列表更新又来自ClientReader线程——于是不得不加一层SwingUtilities.invokeLater()包装,代码立刻臃肿。而AWT方案,一个Label数组动态增减,逻辑清晰如白纸。教学工具的第一要义,是让原理浮出水面,而不是让框架成为新的迷雾。

3. 核心细节解析与实操要点:从源码到运行的每一处关键决策

3.1 端口选择与防火墙穿透:为什么默认8080是安全的?

ChatServer默认监听8080端口(ChatServer.java第22行:new ServerSocket(8080)),这不是随意定的。8080属于“注册端口”(1024-49151),无需管理员权限即可绑定,且避开HTTP(80)、HTTPS(443)、SSH(22)等常用服务端口,极大降低冲突概率。更重要的是,绝大多数企业内网防火墙对8080端口是放行的——它常被用作Web应用的备用端口,运维人员不会刻意封锁。

但要注意:Windows系统自带防火墙默认会阻止入站连接。首次运行ChatServer时,若客户端无法连接,第一反应不是代码问题,而是检查防火墙。解决方法很简单:运行wf.msc打开高级安全防火墙,创建入站规则,协议类型选TCP,特定本地端口填8080,操作选“允许连接”。这条规则只需配一次,后续所有Java聊天程序都复用。

实操心得:我在客户现场部署时,遇到过防火墙规则存在但依然连不通的情况。排查发现是客户启用了“域网络”和“专用网络”双配置,而规则只加在了“域网络”。解决方案是在防火墙设置中,勾选“域”“专用”“公用”所有网络位置。这个细节,很多教程都不会提,但却是局域网部署的高频坑。

3.2 消息分隔与粘包处理:\n背后的协议设计

整个聊天协议的核心,是用换行符\n作为消息边界。服务端ChatServer第68行String msg = in.readLine();读取一行,客户端ChatClient第142行out.println(inputText.getText());发送一行。readLine()会自动剥离\r\n\nprintln()自动添加平台默认换行符(Windows是\r\n,Linux/macOS是\n),这种设计保证了跨平台兼容性。

但这里有个隐藏陷阱:readLine()遇到流关闭会返回null,而非抛异常ChatServer第70行if (msg == null) break;正是优雅退出的判断依据。如果误写成if (msg.equals("")) break;,当用户发送空消息时,循环会提前终止,导致该客户端连接被意外关闭。

更关键的是,这种基于行的协议天然规避了TCP粘包问题。TCP是字节流协议,不保证“一次send对应一次recv”,可能把两条消息合并成一个包(粘包),也可能把一条长消息拆成多个包(拆包)。而readLine()内部会持续读取直到遇到换行符,自动处理了缓冲区拼接。你不需要自己实现ByteBuffer解析或长度头校验——这对入门者是巨大的减负。

注意事项:ChatClient.java第138行inputText.setText("")清空输入框的位置很讲究。它必须在out.println()之后、textArea.append()之前执行。我曾把这行移到append()后面,结果出现“发送后输入框才变空”的视觉延迟,用户会误以为没发送成功,连续点击按钮导致重复消息。这种UI反馈时机的微调,是桌面应用开发的必备直觉。

3.3 AWT组件的线程安全边界:TextArea.append()为何能跨线程调用?

AWT组件并非线程安全,但TextArea.append()是个特例。官方文档明确说明:“append(String)方法是线程安全的,它会将操作排队到事件分发线程执行。” 这意味着你在ClientReader线程里直接调用textArea.append("hello"),AWT底层会自动将其封装为Runnable,通过EventQueue.invokeLater()投递到EDT,最终在EDT中执行真正的文本追加。

这种设计是AWT对开发者的一种妥协性便利,但绝不意味着可以滥用。比如textArea.setText("new content")就不是线程安全的,必须显式用invokeLater()包装。ChatClient第126行SwingUtilities.invokeLater(new Runnable() { ... })正是为此而设——它更新的是整个文本区域内容(虽然本项目没用到setText,但预留了扩展接口)。

实操心得:我曾尝试用textArea.replaceRange()替换部分文本,结果在ClientReader线程中直接调用,程序崩溃抛出NullPointerException。查源码才发现replaceRange()内部会访问Document对象,而Document的修改必须在EDT中进行。这个教训告诉我:不要凭经验猜测线程安全性,每个AWT/Swing方法都要查Javadoc确认append()的安全是特例,不是通则。

3.4 编译与运行的零配置艺术:为什么.class文件要和.java同目录?

项目资源包里同时包含.java源文件和编译好的.class文件,这不是冗余,而是教学友好性的体现。初学者常卡在“怎么编译”这一步:javac ChatServer.java报错“找不到符号”,其实是CLASSPATH没设对。而本项目采用最朴素的策略——所有.class文件与.java文件放在同一目录,且不使用包声明(package)

ChatServer.javaChatClient.java开头都没有package语句,这意味着它们属于默认包。javac编译时,只要当前目录正确,javac *.java就能生成所有.class。运行时,java ChatServer会自动在当前目录查找ChatServer.class,无需-cp .参数。这种“零配置”设计,让学习者聚焦在逻辑本身,而不是被构建工具绑架。

提示:如果你尝试给类加上package com.example.chat;,那么必须调整目录结构为src/com/example/chat/,编译命令变为javac -d . src/com/example/chat/*.java,运行命令变为java com.example.chat.ChatServer。这个过程会暴露CLASSPATH-d参数、目录结构等概念,但对入门者是认知超载。本项目用“无包”策略,把复杂度降到最低。

4. 实操过程与核心环节实现:手把手带你跑通第一个局域网聊天室

4.1 环境准备:三步确认你的机器已就绪

在敲下第一个命令前,请务必完成以下三步验证。这比盲目运行代码节省至少半小时:

第一步:确认JRE版本
打开终端(Windows用CMD/PowerShell,macOS/Linux用Terminal),输入:

java -version

输出应类似:

java version "1.8.0_361"
Java(TM) SE Runtime Environment (build 1.8.0_361-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.361-b09, mixed mode)

重点看1.8.0——本项目基于Java 8编写,高版本(如17+)虽能运行,但某些AWT细节可能有差异。若显示command not found,请先安装JRE 8(推荐Adoptium Temurin 8u362)。

第二步:确认局域网连通性
假设你的电脑IP是192.168.1.100(用ipconfig(Windows)或ifconfig(macOS/Linux)查看),找另一台同网段设备(手机开热点不行,必须是同一交换机下的设备),在该设备上ping你的IP:

ping 192.168.1.100

若返回Reply from 192.168.1.100: bytes=32 time<1ms TTL=128,说明网络通畅。若超时,检查是否连同一WiFi、防火墙是否拦截ICMP(ping用的是ICMP协议,与TCP无关,但能快速验证基础连通性)。

第三步:解压并进入项目目录
将下载的ZIP包解压到一个无中文、无空格的路径,例如D:\java-chat。打开终端,cd进入该目录:

cd D:\java-chat

执行dir(Windows)或ls(macOS/Linux),确认能看到ChatServer.javaChatClient.java文件。此时目录结构应为:

D:\java-chat\
├── ChatServer.java
├── ChatClient.java
├── ChatServer.class  # 已编译好
├── ChatClient.class  # 已编译好
└── ...

4.2 启动服务端:监听端口并观察日志

在项目目录下,执行:

java ChatServer

你会看到控制台输出:

Chat Server started on port 8080...
Waiting for clients...

这表示服务端已启动,并开始accept()等待连接。此时,用netstat命令验证端口监听状态:

  • Windows:
    bash netstat -ano | findstr :8080
    应看到类似:
    TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 12345
    其中12345是Java进程PID。

  • macOS/Linux:
    bash lsof -i :8080
    应看到java进程占用TCP *:http-alt (LISTEN)

关键观察点:0.0.0.0:8080表示监听所有网卡(包括127.0.0.1192.168.1.100),而非仅127.0.0.1。如果显示127.0.0.1:8080,说明ServerSocket构造时指定了localhost,需修改代码为new ServerSocket(8080)(无地址参数)。

4.3 启动客户端:从本机测试到跨设备验证

本机测试(验证基础功能)
保持服务端窗口打开,新开一个终端窗口,同样进入项目目录,执行:

java ChatClient

稍等片刻,AWT窗口弹出:一个标题为“Chat Client”的窗口,顶部是TextArea(消息区),中间是TextField(输入框),底部是“Send”按钮。在输入框键入Hello from localhost!,点击Send,消息立即出现在消息区。此时服务端控制台会打印:

Client connected: /127.0.0.1:54321
Broadcasting: Hello from localhost!

这证明本机回环通信正常。

跨设备测试(真正局域网)
在另一台电脑(IP为192.168.1.101)上,同样解压项目包,进入目录,但启动客户端时需指定服务端IP:

java ChatClient 192.168.1.100

ChatClient.java第28行String serverAddress = args.length > 0 ? args[0] : "127.0.0.1";支持命令行传参。此时192.168.1.101的客户端会连接192.168.1.100:8080。在192.168.1.101客户端发送消息,192.168.1.100的服务端和本机客户端都能实时收到——多人聊天室正式成立!

实操记录:我在测试时,曾因客户端忘记传IP参数,导致它连向127.0.0.1(本机),而本机没运行服务端,于是客户端窗口一直空白,无任何错误提示。后来在ChatClient.java第42行catch (IOException e) { System.err.println("Connection failed: " + e.getMessage()); }加了这行日志,才快速定位问题。这个小补丁,建议你也在自己的代码里加上。

4.4 多客户端并发与消息广播验证

启动第三个客户端(192.168.1.102),同样执行java ChatClient 192.168.1.100。此时服务端控制台会显示:

Client connected: /192.168.1.101:56789
Client connected: /192.168.1.102:57890

在任意一个客户端发送消息,比如Client1Hi all!Client2Client3的消息区会立即追加该行。服务端日志显示:

Broadcasting: Hi all!

这证明广播逻辑生效。你可以同时打开三个终端窗口,分别运行服务端和两个客户端,观察消息的实时性——从点击Send到其他窗口显示,延迟几乎为0(局域网内TCP往返通常<1ms)。

验证技巧:在服务端ChatServer.java第75行System.out.println("Broadcasting: " + msg);后,添加一行System.out.println("Active clients: " + writers.size());。这样每次广播时,你能看到当前在线人数。当Client2意外关闭(直接关窗口),服务端会捕获IOException并从writers列表移除其PrintWriter,下次广播时writers.size()会减1。这个数字变化,是理解连接生命周期的直观证据。

5. 常见问题与排查技巧实录:那些让你抓狂的“灵异现象”真相

5.1 经典问题速查表

现象 可能原因 排查步骤 解决方案
客户端启动后窗口空白,无任何反应 未指定服务端IP,且本机未运行服务端 1. 检查客户端命令是否带IP参数
2. 在服务端机器执行netstat -ano \| findstr :8080确认监听
运行java ChatClient <服务端IP>,如java ChatClient 192.168.1.100
客户端能连接,但收不到任何消息 服务端广播线程未启动,或writers列表为空 1. 查看服务端控制台是否有Client connected日志
2. 在ChatServer.java广播循环前加System.out.println("writers size: " + writers.size());
确保客户端连接成功(服务端有日志),检查writers.add(out)是否被执行(断点调试)
发送消息后,客户端界面卡死(无法输入/点击) ClientReader线程未启动,或readLine()阻塞在主线程 1. 检查ChatClient.java第102行new Thread(...).start()是否执行
2. 在actionPerformed()中搜索readLine(),确认不在UI线程调用
删除所有readLine()调用,确保只在ClientReader.run()中调用
服务端启动报错java.net.BindException: Address already in use: JVM_Bind 8080端口被其他程序占用 1. 执行netstat -ano \| findstr :8080,记下PID
2. 任务管理器中结束该PID进程
修改ChatServer.java第22行端口号为8081,同步修改客户端连接代码
跨设备连接失败,但本机测试正常 防火墙阻止入站连接,或IP地址错误 1. 在服务端机器执行ping <客户端IP>确认互通
2. 检查Windows防火墙入站规则
在服务端防火墙添加8080端口入站规则,或临时关闭防火墙测试

5.2 深度排查案例:为什么“发送空消息”会导致客户端断开?

现象描述:用户在ChatClient输入框中什么也不输,直接点Send,然后客户端窗口关闭,服务端日志显示Client disconnected: /192.168.1.101:56789

根源分析:ChatClient.java第142行out.println(inputText.getText());发送空字符串"",服务端ChatServer.java第68行String msg = in.readLine();读取到""(空行),但第70行if (msg == null) break;判断条件不满足("" != null),于是继续执行广播。问题在于,ChatServer第75行for (PrintWriter writer : writers)遍历时,若某个客户端已断开但PrintWriter未及时清理,writer.println(msg)会抛出IOException。而原代码中,这个异常被catch (IOException e)捕获后,只打印日志,未做任何处理,导致广播循环继续,但该客户端的PrintWriter已失效。

解决方案:在ChatServer.java的广播循环中,为每个writer.println()添加独立try-catch

for (PrintWriter writer : writers) {
    try {
        writer.println(msg);
        writer.flush(); // 强制刷新缓冲区
    } catch (IOException e) {
        // 客户端已断开,从列表移除
        writers.remove(writer);
        System.out.println("Client disconnected: " + writer);
    }
}

同时,在ClientReader线程的run()方法末尾(readLine()返回null后),添加writers.remove(out)清理自身。这个修复让服务端具备了基本的容错能力。

5.3 高阶调试技巧:用Wireshark看清TCP数据包

当所有日志都显示正常,但消息仍不达时,终极手段是抓包。Wireshark是免费开源的网络协议分析器,能让你亲眼看到数据包进出。

操作步骤:
1. 在服务端机器安装Wireshark,启动后选择监听网卡(通常是EthernetWi-Fi
2. 在过滤栏输入tcp.port == 8080,只显示8080端口流量
3. 启动ChatServerChatClient,客户端发送一条消息
4. Wireshark会捕获到:
- 客户端→服务端的TCP SYN(建立连接)
- 客户端→服务端的PSH, ACK包,Data部分为Hello\n(ASCII码)
- 服务端→客户端的PSH, ACK包,Data部分为Hello\n(广播)

如果看到客户端发出了Hello\n,但服务端没有发出对应的PSH, ACK,说明服务端广播逻辑未触发;如果服务端发出了,但客户端没收到,则是客户端ClientReader线程问题。Wireshark把抽象的“网络不通”转化为可视的二进制数据流,是网络编程者的X光机。

我的经验:第一次用Wireshark抓包时,看到满屏的TCP segment of a reassembled PDU,以为是乱码。后来才知道这是TCP分片,右键该包→“Follow”→“TCP Stream”,就能看到完整的Hello\n明文。这个操作,比读一百行日志都管用。

6. 项目延伸与教学价值:从聊天工具到网络编程思维的跃迁

这个项目的价值,远不止于“能聊天”。它是一块跳板,帮你从语法层面跃升到系统层面。当我带着学生做完这个项目,我会引导他们思考三个层次的问题:

第一层:协议设计的权衡
为什么用\n分隔,而不是固定长度头?因为简单,但牺牲了二进制数据支持(图片、文件)。如果要支持文件传输,协议该如何升级?答案是引入长度头:先发4字节整数表示消息长度,再发对应字节数的内容。这引出了DataInputStream.readInt()DataOutputStream.writeInt()的学习。

第二层:并发模型的演进
当前服务端用ArrayList+synchronized,是典型的“共享内存”模型。当连接数超过1000,synchronized块会成为瓶颈。如何升级?答案是“无锁化”:用ConcurrentHashMap存储Socket映射,或转向Reactor模式(一个线程处理所有I/O,多个工作线程处理业务)。这自然衔接到Netty框架的学习。

第三层:安全边界的拓展
明文传输是教学便利,但真实世界必须加密。如何给消息加盐哈希?如何用KeyPairGenerator生成RSA密钥对?如何用Cipher类加密byte[]?这些问题的答案,就藏在javax.crypto包里,而本项目提供的纯净Socket环境,正是练习这些API的最佳沙盒。

最后分享一个小技巧:把这个项目当作“压力测试仪”。在服务端ChatServer.javamain方法里,加一个循环:

for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        try {
            Socket s = new Socket("127.0.0.1", 8080);
            PrintWriter out = new PrintWriter(s.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
            out.println("Auto client " + i);
        } catch (IOException e) { /* ignore */ }
    }).start();
}

运行它,观察服务端能否稳定处理100个并发连接。你会发现ArrayListsize()飙升,synchronized块开始明显延迟。这时,你不再需要别人告诉你“ArrayList不适合高并发”,你自己就看到了瓶颈在哪里——这种亲手制造并观察问题的过程,才是工程能力生长的土壤。

所以,别急着给它加登录、加好友、加表情包。先把它跑通,再把它搞崩,最后把它修好。当你能清晰说出“为什么readLine()必须配println()”、“为什么writersstatic”、“为什么防火墙规则要勾选所有网络位置”时,你就已经超越了90%的Java初学者。网络编程的奥秘,从来不在框架的魔法里,而在这些朴素代码的缝隙中。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套纯Java SE开发的局域网实时聊天解决方案,不依赖任何第三方库,由ChatServer服务端和ChatClient客户端组成。服务端基于Socket监听端口,支持多客户端并发接入,负责消息广播与在线状态管理;客户端使用AWT构建轻量图形界面,具备发送文字、实时接收群聊消息、独立收发线程等基础功能。所有源码均为标准.java文件,附带编译后的.class文件,结构清晰,开箱即用:先运行ChatServer启动服务,再启动多个ChatClient实例即可组成本地聊天室。适用于内网环境下的小范围协作、教学演示或Java网络编程入门实践,消息以明文传输,无用户登录、权限控制或加密机制,部署简单,仅需JRE环境即可运行。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐