项目介绍:
项目涉及技术:
时间获取方案之JDK8之前的Date API:
// 创建Date对象获取时间
Date date = new Date();
// 创建SimpleDateFormat对象指定格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 格式化时间
String formattedDate = sdf.format(date);
时间获取方案之JDK8的LocalDateTime:
// 获取LocalDateTime对象
LocalDateTime now = LocalDateTime.now();
// 创建DateTimeFormatter指定格式
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化时间
String formattedDateTime = now.format(dtf);
字符串高效操作之StringBuilder:
// 创建StringBuilder对象
StringBuilder sb = new StringBuilder();
// 拼接字符串
sb.append("张三").append("李四").append("王五");
// 转为String类型
String result = sb.toString();
解决浮点型运算失真的BigDecimal:
// 创建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();
系统整体架构及开发逻辑
服务端核心功能
服务端开发步骤及关键代码
// 服务端启动类
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<>();
}
// 常量类
public class Constant {
public static final int PORT = 6666; // 服务端端口
}
// 线程类处理客户端消息
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
为键,昵称为值)。
服务端接收消息的类型及处理思路
Socket
输入流读取类型编号,通过switch
分支判断并执行对应逻辑。// 服务端接收消息类型的核心逻辑
DataInputStream dis = new DataInputStream(socket.getInputStream());
int type = dis.readInt(); // 读取消息类型编号
switch (type) {
case 1:
// 处理登录消息
break;
case 2:
// 处理群聊消息
break;
// 其他消息类型...
}
服务端接收登录消息的处理
Socket
和昵称存入onlineSockets
集合,标记客户端上线。// 处理登录消息
String nickname = dis.readUTF(); // 读取昵称
Server.onlineSockets.put(socket, nickname); // 存入在线集合
更新全部客户端在线人数列表的方法
onlineSockets
的values
)。Socket
,通过输出流向每个客户端发送更新消息:
// 更新在线人数列表的方法
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
中移除,并触发在线列表更新。// 客户端下线时移除在线记录
Server.onlineSockets.remove(socket);
updateClientOnlineList(); // 重新更新在线列表
整体流程
onlineSockets
。updateClientOnlineList()
方法,向所有客户端推送更新后的在线列表。解决下线操作的bug
下线时需更新所有客户端的在线人数列表,需重新调用更新在线人数的方法。此时map
集合中已移除下线客户端信息,遍历剩余socket
推送更新后的列表(消息类型为1号)。
// 下线时调用更新在线人数列表的方法
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
(包括发送者自身),确保消息在所有客户端面板展示。
读取客户端发送的文本消息
从数据输入流中读取客户端的文本消息:
DataInputStream dis = new DataInputStream(socket.getInputStream());
String message = dis.readUTF(); // 读取客户端发送的群聊内容
拼装消息内容
socket
从map
集合中获取对应的用户名String senderName = Server.onlineSockets.get(socket); // onlineSockets为类型的map
LocalDateTime
和DateTimeFormatter
处理时间LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String timeStr = dtf.format(now); // 格式化时间为字符串
StringBuilder
组合昵称、时间和消息内容,添加格式符优化展示StringBuilder sb = new StringBuilder();
sb.append(senderName) // 发送者昵称
.append(" ")
.append(timeStr) // 发送时间
.append("\r\n") // 换行
.append(message) // 消息内容
.append("\r\n"); // 消息间换行
String fullMessage = sb.toString(); // 转换为字符串
转发消息给所有在线客户端
遍历所有在线socket
,发送拼装好的消息(消息类型为2号):
private 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();
}
}
}
添加死循环处理多次消息
线程需通过死循环持续接收消息,避免只处理一次后终止:
while (true) { // 死循环:持续监听客户端消息
int msgType = dis.readInt(); // 读取消息类型
if (msgType == 2) { // 处理2号群聊消息
String message = dis.readUTF();
sendMsgToAll(buildFullMessage(socket, message)); // 拼装并转发消息
}
// 可扩展处理其他消息类型(如3号私聊消息)
}
开发客户端的准备与思路:服务端模块已开发完成,接下来需开发客户端,客户端初始仅有界面,需与服务端对接。开发从登录界面开始,遵循用户思维和线性思维,即按照用户操作流程推进。
登录界面的初始操作
entryButton.addActionListener(e -> {
// 获取昵称
String nickname = nicknameInput.getText();
nicknameInput.setText(""); // 清空输入框
if (nickname != null && !nickname.isEmpty()) {
try {
login(nickname); // 调用登录方法
dispose(); // 关闭登录窗口
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
登录方法的创建与完善
login
方法,避免代码臃肿:private 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);
}
Constant
中定义,方便后续修改:public 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
定义为全局变量:
private Socket socket; // 登录界面的全局Socket变量,用于保存与服务端的连接
聊天界面的初始化
Socket
管道,并调用无参构造器初始化界面:public 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
管道用于后续接收在线人数列表、发送和接收消息等操作。// 登录成功后跳转至聊天界面
ChatFrame chatFrame = new ChatFrame(nickname, socket);
chatFrame.setVisible(true);
this.dispose(); // 销毁当前登录窗口
明确客户端核心任务:登录后需实时读取服务端发送的两类消息:
采用多线程处理消息收发:
ClientReaderThread
)负责持续接收服务端消息,避免阻塞主线程创建客户端消息读取线程类:
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();
}
}
}
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);
}
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);
}
}
// 聊天界面构造方法中启动读取线程
public ChatFrame(String nickname, Socket socket) {
this.nickname = nickname;
this.socket = socket;
// 启动消息读取线程
new ClientReaderThread(socket, this).start();
}
接收群聊消息逻辑
// 读取群聊消息
String message = dis.readUTF();
// 将消息更新到窗口
win.setMessageToWindow(message);
setMessageToWindow
方法,将消息追加到展示区域:public void setMessageToWindow(String message) {
msgArea.append(message);
}
发送群聊消息功能
// 为发送按钮绑定点击事件
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();
}
}
功能测试
多人测试与优化方向
192.168.25.70
),连接到同一服务端进行多人测试。GitHub:https://github.com/Andy123211/chat-system/tree/master