一、前置技术
-
项目介绍:
- 项目为局域网沟通软件,类似内网通,核心功能包括昵称输入、聊天界面展示在线人数(实时更新)、群聊,也可扩展私聊、登录注册、聊天记录存储等功能,结尾附GitHub链接。
-
项目涉及技术:
- 包括GUI界面编程、网络通信、面向对象编程,以及字符串处理、时间获取等相关API。
-
时间获取方案之JDK8之前的Date API:
- 通过创建Date对象获取此刻时间,但其格式为美式且不直观,需用SimpleDateFormat进行格式化。
java// 创建Date对象获取时间 Date date = new Date(); // 创建SimpleDateFormat对象指定格式 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 格式化时间 String formattedDate = sdf.format(date);
-
时间获取方案之JDK8的LocalDateTime:
- LocalDate获取年月日,LocalTime获取时分秒,LocalDateTime获取年月日时分秒,通过now()方法获取对象,支持纳秒级精度,且为不可变对象,线程安全,格式化需用DateTimeFormatter。
java// 获取LocalDateTime对象 LocalDateTime now = LocalDateTime.now(); // 创建DateTimeFormatter指定格式 DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 格式化时间 String formattedDateTime = now.format(dtf);
-
字符串高效操作之StringBuilder:
- String因是不可变对象,大量拼接时性能差,而StringBuilder是可变对象,基于数组容器操作,通过append()方法高效拼接,支持链式编程,最后需用toString()转为String类型。
java// 创建StringBuilder对象 StringBuilder sb = new StringBuilder(); // 拼接字符串 sb.append("张三").append("李四").append("王五"); // 转为String类型 String result = sb.toString();
-
解决浮点型运算失真的BigDecimal:
- 用于解决小数运算结果失真问题,需通过字符串构造器或valueOf()方法(内部使用字符串构造器)创建对象,提供加减乘除等方法,除法时若结果除不尽需指定保留位数和舍入模式(如四舍五入)。
java// 创建BigDecimal对象 BigDecimal a = new BigDecimal("0.1"); BigDecimal b = BigDecimal.valueOf(0.2); // 加法运算 BigDecimal sum = a.add(b); // 除法运算(保留2位小数,四舍五入) BigDecimal divide = a.divide(b, 2, RoundingMode.HALF_UP); // 转为double类型 double result = sum.doubleValue();
二、AI获取客户端界面:
- 项目需求分析:项目为局域网类沟通软件开发,启动界面只需输入聊天昵称,进入后显示在线人数,具备群聊功能,实时更新在线人数,先实现核心群聊功能,后续可扩展私聊等功能。
- 技术选型:涉及GUI编程技术(swing)、网络编程、面向对象设计以及Java提供的常用API。
- 项目步骤规划:第一步创建名为"it-chat"的模块;第二步获取系统所需界面(登录界面和聊天界面);第三步定义App启动类,创建并展示进入界面对象。
- 获取登录界面:通过通义千问大模型生成局域网聊天进入界面代码,包含昵称输入框、进入和取消按钮,将代码复制到IDEA中,修改类名、处理乱码(将字体改为楷体),并测试界面能否启动。
- 获取聊天界面:同样通过AI生成群聊界面代码,包含在线人数展示框、消息展示框、消息发送框和发送按钮,复制到IDEA后修改类名、调整窗口宽度、添加关闭窗口退出程序代码等,测试界面启动情况。
- 定义启动类:在SRC下新建App类,在main方法中创建聊天进入界面对象并展示,完成界面准备工作,下面将从进入界面开始开发,串起所有功能。
三、架构分析
-
系统整体架构及开发逻辑
- 客户端与服务端通过管道连接实现通信,客户端发送的信息(如登录昵称、群聊消息)需经服务端转发给其他在线客户端。
- 开发顺序:先分析系统整体架构,开发服务端,再完善客户端功能。
-
服务端核心功能
- 接收客户端的管道连接,支持多客户端同时接入。
- 接收客户端发送的登录消息(含昵称)和群聊消息。
- 存储所有在线客户端的socket管道,用于消息转发。
- 收到登录消息后,更新所有客户端的在线人数列表。
- 收到群聊消息后,将消息转发给所有在线客户端。
-
服务端开发步骤及关键代码
- 创建服务端项目,例如命名为"it-chat-server"。
- 创建服务端启动类,负责启动服务端并等待客户端连接
java// 服务端启动类 public class Server { public static void main(String[] args) { try { // 注册端口,端口从常量类获取 ServerSocket serverSocket = new ServerSocket(Constant.PORT); System.out.println("服务端启动成功,等待客户端连接..."); while (true) { // 等待客户端连接,获取管道 Socket socket = serverSocket.accept(); System.out.println("一个客户端连接成功!"); // 将管道交给独立线程处理 new ServerReaderThread(socket).start(); // 将管道暂存(后续需结合登录消息存储昵称) // 此处仅为示意,实际需在接收登录消息后完善 onlineSockets.put(socket, "未知用户"); } } catch (IOException e) { e.printStackTrace(); } } // 定义Map集合存储在线客户端管道及对应昵称 public static Map<Socket, String> onlineSockets = new HashMap<>(); }
- 定义常量类存储端口信息
java// 常量类 public class Constant { public static final int PORT = 6666; // 服务端端口 }
- 创建线程类处理客户端管道通信
java// 线程类处理客户端消息 public class ServerReaderThread extends Thread { private Socket socket; public ServerReaderThread(Socket socket) { this.socket = socket; } @Override public void run() { // 后续将实现接收登录消息、群聊消息等逻辑 System.out.println("线程开始处理客户端:" + socket); } }
四、服务端(在线人数模块)
-
内容承接 :已完成服务端基础开发,包括创建项目、接收客户端Socket管道并交由独立线程处理,同时准备了
Map
集合(onlineSockets
)用于存储在线客户端的Socket
及对应昵称(Socket
为键,昵称为值)。 -
服务端接收消息的类型及处理思路
- 消息类型:登录消息、群聊消息、私聊消息、图片消息等。
- 协议设计:客户端需先发送消息类型编号(如1代表登录、2代表群聊、3代表私聊),服务端通过编号区分处理。
- 核心逻辑:服务端从
Socket
输入流读取类型编号,通过switch
分支判断并执行对应逻辑。
java// 服务端接收消息类型的核心逻辑 DataInputStream dis = new DataInputStream(socket.getInputStream()); int type = dis.readInt(); // 读取消息类型编号 switch (type) { case 1: // 处理登录消息 break; case 2: // 处理群聊消息 break; // 其他消息类型... }
-
服务端接收登录消息的处理
- 读取昵称:当消息类型为1时,通过输入流读取客户端发送的昵称。
- 存储在线信息:将当前
Socket
和昵称存入onlineSockets
集合,标记客户端上线。
java// 处理登录消息 String nickname = dis.readUTF(); // 读取昵称 Server.onlineSockets.put(socket, nickname); // 存入在线集合
-
更新全部客户端在线人数列表的方法
- 方法功能:向所有在线客户端推送最新的在线用户列表。
- 实现步骤:
- 获取所有在线用户的昵称(
onlineSockets
的values
)。 - 遍历所有在线
Socket
,通过输出流向每个客户端发送更新消息:- 先发送消息类型(1代表在线列表更新)。
- 发送用户数量,再逐个发送用户名。
- 获取所有在线用户的昵称(
java// 更新在线人数列表的方法 private void updateClientOnlineList() { // 获取所有在线用户名 Collection<String> allNicknames = Server.onlineSockets.values(); // 遍历所有在线Socket管道 for (Socket clientSocket : Server.onlineSockets.keySet()) { try { DataOutputStream dos = new DataOutputStream(clientSocket.getOutputStream()); dos.writeInt(1); // 消息类型:在线列表更新 dos.writeInt(allNicknames.size()); // 发送用户数量 for (String nickname : allNicknames) { dos.writeUTF(nickname); // 逐个发送用户名 } dos.flush(); // 刷新数据 } catch (IOException e) { e.printStackTrace(); } } }
-
客户端下线的处理
- 当客户端断开连接(抛出异常),服务端需将其
Socket
从onlineSockets
中移除,并触发在线列表更新。
java// 客户端下线时移除在线记录 Server.onlineSockets.remove(socket); updateClientOnlineList(); // 重新更新在线列表
- 当客户端断开连接(抛出异常),服务端需将其
-
整体流程
- 服务端接收消息类型编号,判断为登录消息(1)。
- 读取昵称并存储到
onlineSockets
。 - 调用
updateClientOnlineList()
方法,向所有客户端推送更新后的在线列表。 - 客户端接收消息后,更新本地展示的在线用户列表。
五、服务端(聊天信息转发模块)
-
解决下线操作的bug
下线时需更新所有客户端的在线人数列表,需重新调用更新在线人数的方法。此时
map
集合中已移除下线客户端信息,遍历剩余socket
推送更新后的列表(消息类型为1号)。java// 下线时调用更新在线人数列表的方法 updateOnlineUserList(); // 更新在线人数列表的方法逻辑 private void updateOnlineUserList() { // 获取当前在线用户列表(已移除下线用户) Collection<String> usernames = onlineSockets.values(); // 遍历所有在线socket,推送更新后的列表 for (Socket socket : onlineSockets.keySet()) { DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); dos.writeInt(1); // 1号消息:更新在线人数 dos.writeUTF(String.join(",", usernames)); dos.flush(); } }
-
接收群聊消息并转发的整体逻辑
服务端线程接收2号类型的群聊消息后,需转发给所有在线
socket
(包括发送者自身),确保消息在所有客户端面板展示。 -
读取客户端发送的文本消息
从数据输入流中读取客户端的文本消息:
javaDataInputStream dis = new DataInputStream(socket.getInputStream()); String message = dis.readUTF(); // 读取客户端发送的群聊内容
-
拼装消息内容
-
获取发送者昵称 :通过当前
socket
从map
集合中获取对应的用户名javaString senderName = Server.onlineSockets.get(socket); // onlineSockets为<Socket, String>类型的map
-
获取并格式化时间 :使用
LocalDateTime
和DateTimeFormatter
处理时间javaLocalDateTime now = LocalDateTime.now(); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String timeStr = dtf.format(now); // 格式化时间为字符串
-
拼接消息 :使用
StringBuilder
组合昵称、时间和消息内容,添加格式符优化展示javaStringBuilder sb = new StringBuilder(); sb.append(senderName) // 发送者昵称 .append(" ") .append(timeStr) // 发送时间 .append("\r\n") // 换行 .append(message) // 消息内容 .append("\r\n"); // 消息间换行 String fullMessage = sb.toString(); // 转换为字符串
-
-
转发消息给所有在线客户端
遍历所有在线
socket
,发送拼装好的消息(消息类型为2号):javaprivate void sendMsgToAll(String fullMessage) { for (Socket socket : Server.onlineSockets.keySet()) { try { DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); dos.writeInt(2); // 2号消息:群聊消息 dos.writeUTF(fullMessage); dos.flush(); // 刷新输出流 } catch (IOException e) { e.printStackTrace(); } } }
-
添加死循环处理多次消息
线程需通过死循环持续接收消息,避免只处理一次后终止:
javawhile (true) { // 死循环:持续监听客户端消息 int msgType = dis.readInt(); // 读取消息类型 if (msgType == 2) { // 处理2号群聊消息 String message = dis.readUTF(); sendMsgToAll(buildFullMessage(socket, message)); // 拼装并转发消息 } // 可扩展处理其他消息类型(如3号私聊消息) }
六、客户端(登录开发)
-
开发客户端的准备与思路:服务端模块已开发完成,接下来需开发客户端,客户端初始仅有界面,需与服务端对接。开发从登录界面开始,遵循用户思维和线性思维,即按照用户操作流程推进。
-
登录界面的初始操作
- 给登录界面的"进入"按钮(后改为"登录"按钮)绑定点击事件监听器,代码使用匿名内部类或Lambda表达式简化实现:
javaentryButton.addActionListener(e -> { // 获取昵称 String nickname = nicknameInput.getText(); nicknameInput.setText(""); // 清空输入框 if (nickname != null && !nickname.isEmpty()) { try { login(nickname); // 调用登录方法 dispose(); // 关闭登录窗口 } catch (IOException ex) { ex.printStackTrace(); } } });
- 点击按钮后,从输入框获取昵称,判断非空后执行登录逻辑,并关闭登录窗口。
-
登录方法的创建与完善
- 将登录相关代码独立为
login
方法,避免代码臃肿:
javaprivate void login(String nickname) throws IOException { // 连接服务端 socket = new Socket(Constant.SERVER_IP, Constant.SERVER_PORT); // 发送登录信息 DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); dos.writeInt(1); // 消息类型为登录(1代表登录) dos.writeUTF(nickname); // 发送昵称 dos.flush(); // 刷新缓冲区 // 登录成功后进入聊天界面 new ClientChatFrame(nickname, socket); }
- 服务端的IP和端口在常量类
Constant
中定义,方便后续修改:
javapublic class Constant { public static final String SERVER_IP = "127.0.0.1"; // 服务器IP public static final int SERVER_PORT = 666; // 服务器端口,需与服务端保持一致 }
- 将登录相关代码独立为
-
发送登录消息给服务端 :连接成功后,通过
DataOutputStream
向服务端发送消息类型(1代表登录)和昵称,且不能关闭流和管道,否则会中断后续通信。 -
进入聊天界面的准备 :登录成功后,启动聊天界面(
ClientChatFrame
类)。需将昵称和Socket
管道传给聊天界面,因此在登录界面将Socket
定义为全局变量:javaprivate Socket socket; // 登录界面的全局Socket变量,用于保存与服务端的连接
-
聊天界面的初始化
- 聊天界面通过有参构造器接收昵称和
Socket
管道,并调用无参构造器初始化界面:
javapublic class ClientChatFrame extends JFrame { private String nickname; private Socket socket; public ClientChatFrame(String nickname, Socket socket) { this(); // 调用无参构造器初始化界面 this.nickname = nickname; this.socket = socket; setTitle(nickname + "的聊天窗口"); // 在窗口标题展示昵称 } public ClientChatFrame() { // 初始化界面组件的代码 initComponents(); } }
- 聊天界面将昵称展示在窗口标题上,方便用户识别当前登录账号,同时保存
Socket
管道用于后续接收在线人数列表、发送和接收消息等操作。
- 聊天界面通过有参构造器接收昵称和
七、客户端(在线人数展示)
- 回顾登录界面跳转逻辑:登录成功后,将昵称和Socket管道传递给聊天界面,并销毁登录窗口,避免资源占用。关键代码逻辑如下(示意):
java
// 登录成功后跳转至聊天界面
ChatFrame chatFrame = new ChatFrame(nickname, socket);
chatFrame.setVisible(true);
this.dispose(); // 销毁当前登录窗口
-
明确客户端核心任务:登录后需实时读取服务端发送的两类消息:
- 在线人数更新消息(类型1)
- 群聊消息(类型2)
-
采用多线程处理消息收发:
- 独立线程(
ClientReaderThread
)负责持续接收服务端消息,避免阻塞主线程 - 主线程负责处理用户交互(如发送消息)
- 独立线程(
-
创建客户端消息读取线程类:
java
public class ClientReaderThread extends Thread {
private Socket socket;
private ChatFrame chatFrame; // 持有聊天界面对象
public ClientReaderThread(Socket socket, ChatFrame chatFrame) {
this.socket = socket;
this.chatFrame = chatFrame;
}
@Override
public void run() {
try (DataInputStream dis = new DataInputStream(socket.getInputStream())) {
while (true) {
int type = dis.readInt(); // 读取消息类型
if (type == 1) {
// 处理在线人数更新
updateClientOnlineUserList(dis);
} else if (type == 2) {
// 处理群聊消息(下节课实现)
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 在线人数更新方法实现:
java
private void updateClientOnlineUserList(DataInputStream dis) throws IOException {
int count = dis.readInt(); // 读取在线用户数量
String[] onlineUsers = new String[count];
// 循环读取所有在线用户名
for (int i = 0; i < count; i++) {
onlineUsers[i] = dis.readUTF();
}
// 调用聊天界面方法更新UI
chatFrame.updateOnlineUsers(onlineUsers);
}
- 聊天界面更新UI组件:
java
public class ChatFrame extends JFrame {
private JList<String> onlineUserList; // 展示在线用户的列表组件
// 更新在线用户列表
public void updateOnlineUsers(String[] users) {
DefaultListModel<String> model = new DefaultListModel<>();
for (String user : users) {
model.addElement(user);
}
onlineUserList.setModel(model);
}
}
- 线程启动与数据传递:在聊天界面初始化时启动读取线程,并传递Socket和界面对象:
java
// 聊天界面构造方法中启动读取线程
public ChatFrame(String nickname, Socket socket) {
this.nickname = nickname;
this.socket = socket;
// 启动消息读取线程
new ClientReaderThread(socket, this).start();
}
- 功能测试验证 :
- 启动服务端后,多客户端登录测试在线人数同步展示
- 关闭任一客户端,验证其他客户端在线列表实时移除该用户
八、客户端(群聊功能)
-
接收群聊消息逻辑
- 接收消息类型为2的群聊消息,通过输入流读取服务端发送的UTF格式消息(包含发送者、时间等信息)。
- 将消息展示到界面面板,核心代码如下:
java// 读取群聊消息 String message = dis.readUTF(); // 将消息更新到窗口 win.setMessageToWindow(message);
- 在窗口类中实现
setMessageToWindow
方法,将消息追加到展示区域:
javapublic void setMessageToWindow(String message) { msgArea.append(message); }
-
发送群聊消息功能
- 为发送按钮绑定点击事件,获取输入框内容并清空,通过输出流向服务端发送消息。
- 先发送消息类型2,再发送具体的群聊内容,核心代码如下:
java// 为发送按钮绑定点击事件 sendButton.addActionListener(e -> { String message = inputField.getText(); inputField.setText(""); // 清空输入框 sendMessageToServer(message); }); // 发送消息到服务端 private void sendMessageToServer(String message) { try (DataOutputStream dos = new DataOutputStream(socket.getOutputStream())) { dos.writeInt(2); // 发送群聊消息类型 dos.writeUTF(message); // 发送消息内容 dos.flush(); } catch (IOException e) { e.printStackTrace(); } }
-
功能测试
- 启动服务端和多个客户端,测试不同用户登录后发送消息的接收情况。
- 例如,用户"张三"发送"我好慌哦",用户"王麻子"发送"麻子你在干啥",验证所有客户端能否正常接收消息。
- 测试中发现换行显示存在小问题,但不影响核心功能使用。
-
多人测试与优化方向
- 修改客户端连接的IP地址(如
192.168.25.70
),连接到同一服务端进行多人测试。 - 可优化的方向包括:消息自动滚动到底部、完善换行显示、支持发送图片、美化界面等。
- 修改客户端连接的IP地址(如
bash
GitHub:https://github.com/Andy123211/chat-system/tree/master