当前位置: 首页 > news >正文

通信网络编程5.0——JAVA

项目总览

这是一个基于 Java Swing 和 Socket 编程多用户聊天室项目,支持以下功能:

  • ✅ 用户注册 / 登录

  • ✅ 群聊消息广播

  • ✅ 私聊消息(支持离线存储)

  • ✅ 用户在线状态实时更新

  • ✅ 图文消息扩展(预留结构)

  • ✅ 心跳机制保持连接

  • ✅ 离线消息重发

项目结构一览

文件名作用
Client.java客户端主入口
Server.java服务器主入口
ChatUI.java聊天界面(群聊窗口)
TextUI.java客户端列表窗口
ChatMsg.java聊天消息封装类
LoginRegistMsg.java登录/注册消息封装
ClientUser.java用户实体类
ClientMsg.java消息父类,定义常量
JTableModel.java客户端列表数据模型
StatusCellRenderer.java在线状态颜色渲染器

网络通信协议

所有消息都基于自定义二进制协议,格式如下:

字段含义
ctrlType消息类型(10 注册,11 登录,12 群聊,13 私聊,14添加好友,15移除好友)
senderIdLen发送者 ID 长度
senderId发送者 ID
receiverIdLen接收者 ID 长度(群聊为 -1)
receiverId接收者 ID(群聊为 null)
msgType消息内容类型(20 文本,21 图片,22 文件)
textMsgLen消息内容长度
textMsg消息内容

客户端相关

Client

public class Client {// 客户端类,用于创建客户端对象并与服务器进行通信Socket socket;// 客户端套接字String ip;// 服务器的 IP 地址int port;// 服务器的端口号InputStream in;// 输入流,用于接收服务器的消息OutputStream out;// 输出流,用于向服务器发送消息private ClientUser user;// 用户对象private ChatUI ui;private Map<String, ClientUser> userMap = new HashMap<>();public Client(String ip, int port) {//初始化客户端对象this.ip = ip;// 初始化服务器的 IP 地址this.port = port;// 初始化服务器的端口号}// 连接服务器的方法public void connectServer() {try {socket = new Socket(ip, port);// 创建客户端套接字并连接到服务器in = socket.getInputStream();// 获取输入流out = socket.getOutputStream();// 获取输出流System.out.println("连接服务器成功");} catch (IOException e) {throw new RuntimeException("连接服务器失败: " + e.getMessage());}}// 启动客户端的方法public void startClient() {connectServer();// 连接服务器String[] options = {"注册", "登录"};// 提供注册和登录选项// 弹出对话框让用户选择操作int choice = JOptionPane.showOptionDialog(null, "请选择操作", "注册/登录",JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]);// 获取用户名String userName = JOptionPane.showInputDialog("请输入用户名:");// 检查用户名是否为空,若为空则提示用户重新输入while (userName == null || userName.trim().isEmpty()) {userName = JOptionPane.showInputDialog("用户名不能为空,请重新输入:");}userName = userName.trim();// 去除用户名前后的空格// 获取密码String password = JOptionPane.showInputDialog("请输入密码:");// 检查密码是否为空,若为空则提示用户重新输入while (password == null || password.trim().isEmpty()) {password = JOptionPane.showInputDialog("密码不能为空,请重新输入:");}password = password.trim();// 去除密码前后的空格int ctrlType = choice == 0 ? CtrlType.REGISTER : CtrlType.LOGIN;// 根据用户选择确定控制类型// 创建登录注册消息对象LoginRegistMsg msg = new LoginRegistMsg(ctrlType, userName.length(), userName, password.length(), password);msg.sendMsg(out);// 发送登录注册消息try {int result = in.read(); // 接收服务器返回的结果if (result == 1) {// 注册或登录成功JOptionPane.showMessageDialog(null, choice == 0 ? "注册成功" : "登录成功");user = new ClientUser(0, userName, password); // 创建用户对象List<String> clientList = new ArrayList<>(); // 这里需要根据实际情况获取客户端列表ui = new ChatUI(userName, out, clientList, userMap); // 创建聊天界面readMsg(ui.msgShow); // 开始读取服务器消息// 处理离线消息List<ChatMsg> offlineMessages = user.getOfflineMessages();if (!offlineMessages.isEmpty()) {for (ChatMsg offlineMsg : offlineMessages) {// 在聊天界面显示离线消息ui.msgShow.append(offlineMsg.getSenderId() + ": " + offlineMsg.getContentAsString() + "\n");}user.clearOfflineMessages(); // 清空离线消息列表}// 心跳包,保持连接new Thread(() -> {try {while (true) {Thread.sleep(3000);// 每 3 秒发送一次心跳包if (socket != null && !socket.isClosed()) {try {out.write(0);// 发送心跳包out.flush();// 刷新输出流} catch (IOException e) {break;}}}} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复中断状态}}).start();} else {// 注册或登录失败JOptionPane.showMessageDialog(null, choice == 0 ? "注册失败,用户名已存在" : "登录失败,用户名或密码错误");startClient(); // 重新尝试}} catch (IOException e) {e.printStackTrace();}}// 读取服务器消息的方法public void readMsg(JTextArea msgShow) {new Thread(() -> {// 创建一个新线程来读取服务器消息try {while (true) {int senderNameLength = in.read();// 读取发送者用户名的长度if (senderNameLength == -1) {break;}byte[] senderNameBytes = new byte[senderNameLength];// 创建字节数组,用于存储发送者用户名in.read(senderNameBytes);// 读取发送者用户名String senderName = new String(senderNameBytes);// 将字节数组转换为字符串if (senderName.equals("CLIENT_LIST")) {int listLength = in.read();// 读取客户端列表的长度byte[] listBytes = new byte[listLength];// 创建字节数组,用于存储客户端列表in.read(listBytes);// 读取客户端列表String clientListStr = new String(listBytes);// 将字节数组转换为字符串System.out.println("Received client list: " + clientListStr); // 添加日志输出String[] clientEntries = clientListStr.split(";");// 分割客户端列表字符串List<String> newClientList = new ArrayList<>();// 存储新的客户端列表for (String entry : clientEntries) {if (!entry.isEmpty()) {String[] parts = entry.split(",");// 分割客户端信息String clientName = parts[0];// 获取客户端名称boolean isOnline = parts[1].equals("1");// 获取客户端在线状态// 获取或创建用户对象ClientUser user = userMap.computeIfAbsent(clientName, k -> new ClientUser(0, clientName, ""));user.online = isOnline;// 设置用户在线状态newClientList.add(clientName);// 将客户端名称添加到新的客户端列表中// 更新 ChatUI 中的用户在线状态if (ui != null) {ui.updateUserOnlineStatus(clientName, isOnline);}}}if (ui != null) {ui.updateClientList(newClientList);// 更新 ChatUI 中的客户端列表}// 更新 TextUI 的客户端列表if (ui instanceof ChatUI) {((ChatUI) ui).updateUserOnlineStatusForTextUI(newClientList);}continue;}int msgLength = in.read();// 读取消息内容的长度byte[] msgBytes = new byte[msgLength];// 创建字节数组,用于存储消息内容in.read(msgBytes);// 读取消息内容String message = new String(msgBytes);// 将字节数组转换为字符串// 创建聊天消息对象ChatMsg msg = new ChatMsg(CtrlType.GROUP_CHAT, senderName.length(), senderName, -1, null, MsgType.TEXT, message.length(), message);msgShow.append(senderName + ": " + msg.getContentAsString() + "\n");// 在聊天界面显示消息}} catch (IOException e) {msgShow.append("与服务器断开连接\n");// 在聊天界面显示与服务器断开连接的消息}}).start();}// 主方法,程序入口public static void main(String[] args) {Client client = new Client("127.0.0.1", 6667);client.startClient();}
}

 这是客户端的主类,负责与服务器建立连接、处理用户的登录注册、接收和发送消息等功能。

  • 连接服务器:通过 connectServer 方法创建 Socket 对象并连接到指定的服务器 IP 和端口,同时获取输入输出流。
  • 登录注册:使用 JOptionPane 弹出对话框让用户选择注册或登录,并输入用户名和密码。根据用户选择创建 LoginRegistMsg 对象并发送给服务器,根据服务器返回的结果处理注册或登录成功或失败的情况。
  • 接收消息:在 readMsg 方法中,使用一个新线程不断从输入流中读取消息。如果收到客户端列表更新消息,则更新客户端列表和用户在线状态;否则,创建 ChatMsg 对象并在聊天界面显示消息。
  • 心跳包:使用一个新线程每 3 秒发送一次心跳包,保持与服务器的连接。

 

ClientMsg

public class ClientMsg {// 属性:发送者ID 消息类型 消息长度 接收者ID长度  接收者ID 消息内容// 注册消息: 控制类型 账号长度 账号 密码长度 密码// 登录消息: 控制类型 账号长度 账号 密码长度 密码// 群聊:    控制类型 发送者ID长度 发送者ID 消息类型 ... 消息长度 消息内容// 私聊:    控制类型 发送者ID长度 发送者ID 消息类型 ...  接收者ID长度 接收者ID 消息长度 消息内容// 添加好友: 控制类型 发送者ID长度 发送者ID  接收者ID长度 接收者ID// 移除好友: 控制类型 发送者ID长度 发送者ID  接收者ID长度 接收者ID/*** 功能类型:* 10 注册* 11 登录* 12 群聊* 13 私聊* 14 添加好友* 15 移除好友*/int ctrlType;//控制类型int senderIdLen;//发送者ID长度String senderId;//发送者IDint passwordLen;//密码长度String password;//密码int receiverIdLen;//接收者ID长度String receiverId;//接收者IDint msgType;//消息类型:int textMsgLen;//文本消息长度String textMsg;//文本消息内容int imageWidth;//图片消息 宽度int imageHeight;//图片消息 高度int imageMsgLen = imageHeight * imageWidth;//图片消息长度byte[] imageMsg;//图片消息内容int fileNameLen;//文件名长度String fileName;//文件名int fileMsgLen;//文件消息长度byte[] fileMsg;//文件消息内容}class CtrlType {//控制类型public static final int REGISTER = 10;//注册public static final int LOGIN = 11;//登录public static final int GROUP_CHAT = 12;//群聊public static final int PRIVATE_CHAT = 13;//私聊public static final int ADD_FRIEND = 14;//添加好友public static final int REMOVE_FRIEND = 15;//移除好友
}class MsgType {//消息类型public static final int TEXT = 20;//文本public static final int IMAGE = 21;//图片public static final int FILE = 22;//文件
}

 这是一个抽象类,定义了客户端消息的基本属性和控制类型、消息类型的常量。

  • 控制类型:定义了注册、登录、群聊、私聊、添加好友、移除好友等功能的控制类型。
  • 消息类型:定义了文本、图片、文件等消息类型。

ChatMsg

public class ChatMsg extends ClientMsg {// 群聊:   功能类型 发送者ID长度 发送者ID 消息类型 ... 消息长度 消息内容// 私聊:   功能类型 发送者ID长度 发送者ID 消息类型 ...  接收者ID长度 接收者ID 消息长度 消息内容// 文本消息//群聊-私聊-文本消息public ChatMsg(int ctrlType, int senderIdLen, String senderId, int receiverIdLen, String receiverId, int msgType, int textMsgLen, String textMsg) {this.ctrlType = ctrlType; // 控制类型this.senderIdLen = senderIdLen; // 发送者ID长度this.senderId = senderId; // 发送者IDthis.receiverIdLen = receiverIdLen; // 接收者ID长度this.receiverId = receiverId; // 接收者IDthis.msgType = msgType; // 消息类型this.textMsgLen = textMsgLen; // 文本消息长度this.textMsg = textMsg; // 文本消息内容addContent(textMsg.getBytes()); // 将文本消息内容添加到content列表this.time = System.currentTimeMillis(); // 设置消息发送时间戳}//群聊-私聊-图片消息public ChatMsg(int ctrlType, int senderIdLen, String senderId, int receiverIdLen, String receiverId, int msgType, int imageWidth, int imageHeight, int imageMsgLen, byte[] imageMsg) {this.ctrlType = ctrlType; // 控制类型this.senderIdLen = senderIdLen; // 发送者ID长度this.senderId = senderId; // 发送者IDthis.receiverIdLen = receiverIdLen; // 接收者ID长度this.receiverId = receiverId; // 接收者IDthis.msgType = msgType; // 消息类型this.imageWidth = imageWidth; // 图片消息宽度this.imageHeight = imageHeight; // 图片消息高度this.imageMsgLen = imageMsgLen; // 图片消息长度this.imageMsg = imageMsg; // 图片消息内容addContent(imageMsg); // 将图片消息内容添加到content列表this.time = System.currentTimeMillis(); // 设置消息发送时间戳}//群聊-私聊-文件消息public ChatMsg(int ctrlType, int senderIdLen, String senderId, int receiverIdLen, String receiverId, int msgType, int fileNameLen, String fileName, int fileMsgLen, byte[] fileMsg) {this.ctrlType = ctrlType; // 控制类型this.senderIdLen = senderIdLen; // 发送者ID长度this.senderId = senderId; // 发送者IDthis.receiverIdLen = receiverIdLen; // 接收者ID长度this.receiverId = receiverId; // 接收者IDthis.msgType = msgType; // 消息类型this.fileNameLen = fileNameLen; // 文件名长度this.fileName = fileName; // 文件名this.fileMsgLen = fileMsgLen; // 文件消息长度this.fileMsg = fileMsg; // 文件消息内容addContent(fileMsg); // 将文件消息内容添加到content列表this.time = System.currentTimeMillis(); // 设置消息发送时间戳}// 将字节数组添加到消息内容列表的方法private void addContent(byte[] bytes) {for (byte b : bytes) {content.add(b);}}List<Byte> content = new ArrayList<>(); // 消息的内容long time; // 消息发送的时间戳// 获取消息内容字符串表示的方法public String getContentAsString() {byte[] bytes = new byte[content.size()];// 创建字节数组,用于存储消息内容for (int i = 0; i < content.size(); i++) {bytes[i] = content.get(i);}SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 创建日期格式化对象String timeStr = sdf.format(new Date(time));// 将时间戳格式化为字符串String contentStr;switch (msgType) {case MsgType.TEXT:contentStr = new String(bytes);// 文本消息,将字节数组转换为字符串break;case MsgType.IMAGE:contentStr = "[图片消息]";// 图片消息,显示固定格式break;case MsgType.FILE:contentStr = "[文件消息: " + fileName + "]";// 文件消息,显示文件名break;default:contentStr = "未知类型消息";}return contentStr + " - " + timeStr;}// 添加获取发送者ID的方法public String getSenderId() {return senderId;}}

 继承自 ClientMsg,用于表示聊天消息,包括文本、图片和文件消息。

  • 构造函数:根据不同的消息类型(文本、图片、文件)提供不同的构造函数,将消息内容添加到 content 列表中,并记录消息发送时间。
  • getContentAsString 方法:将消息内容转换为字符串表示,并添加时间戳。根据消息类型显示不同的内容格式。

ClientUser

public class ClientUser {//用户类,表示一个用户对象int id;//用户唯一表示符String name;// 用户的名称String password;// 用户的密码boolean online;// 用户是否在线的标志List<ClientUser> friends;// 用户的好友列表List<ChatMsg> offlineMessages;// 用户的离线消息列表public ClientUser(int id, String name, String password) {//初始化用户对象this.id = id;// 初始化用户的唯一标识符this.name = name;// 初始化用户的名称this.password = password;// 初始化用户的密码this.online = false;// 初始状态为离线this.friends = new ArrayList<>();// 初始化好友列表this.offlineMessages = new ArrayList<>();// 初始化离线消息列表}public void addFriend(ClientUser friend) {// 向用户的好友列表中添加一个好友if (!friends.contains(friend)) {// 检查好友列表中是否已经包含该好友friends.add(friend);// 如果不包含,则添加该好友}}public void receiveMessage(ChatMsg message) {// 处理用户接收消息的逻辑if (online) {// 检查用户是否在线// 在线直接处理消息} else {offlineMessages.add(message);// 离线则将消息添加到离线消息列表}}// 获取离线消息列表public List<ChatMsg> getOfflineMessages() {return offlineMessages;}// 清空离线消息列表public void clearOfflineMessages() {offlineMessages.clear();}}

 表示一个用户对象,包含用户的基本信息、好友列表和离线消息列表。

  • 添加好友:通过 addFriend 方法向好友列表中添加一个好友,避免重复添加。
  • 接收消息:在 receiveMessage 方法中,如果用户在线则直接处理消息,否则将消息添加到离线消息列表中。

ChatUI

// 聊天界面类,继承自 JFrame,用于显示聊天窗口和处理聊天相关操作
public class ChatUI extends JFrame {public JTextArea msgShow = new JTextArea();// 消息显示区域private JTableModel del;// 自定义的表格模型private JTable table;// 表格组件,用于显示客户端列表private Map<String, ClientUser> userMap; // 用户映射,存储客户端的用户信息private JButton sendButton; // 新增成员变量,用于保存发送按钮private JTextArea msgInputArea; // 新增成员变量,用于保存输入框// 构造函数,用于服务器端的聊天界面public ChatUI(String title, List<Socket> clientSockets, List<String> clientList, Map<String, ClientUser> userMap) {super(title);this.userMap = userMap;setupUI(clientList); // 初始化界面ChatListener cl = new ChatListener(); // 创建聊天监听器cl.clientSockets = clientSockets;setupListener(cl);// 设置监听器}// 构造函数,用于客户端的聊天界面public ChatUI(String title, OutputStream out, List<String> clientList, Map<String, ClientUser> userMap) {super(title);this.userMap = userMap;setupUI(clientList);// 初始化界面clientListener cl = new clientListener();// 创建客户端监听器cl.out = out;setupListener(cl);// 设置监听器}// 初始化界面的方法private void setupUI(List<String> clientList) {setSize(600, 400);  // 设置窗口大小setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置窗口关闭操作setLayout(new BorderLayout());// 设置布局管理器// 消息显示区域JScrollPane scrollPane = new JScrollPane(msgShow);scrollPane.setPreferredSize(new Dimension(780, 300));JPanel chatPanel = new JPanel();chatPanel.setLayout(new BorderLayout());chatPanel.add(scrollPane, BorderLayout.CENTER);JPanel msgInput = new JPanel();// 初始化消息输入框msgInputArea = new JTextArea(); // 明确设置输入区域的 JTextArea 为可编辑msgInputArea.setEditable(true);JScrollPane scrollPane1 = new JScrollPane(msgInputArea);scrollPane1.setPreferredSize(new Dimension(400, 80));msgInput.add(scrollPane1);sendButton = new JButton("发送"); // 初始化发送按钮msgInput.add(sendButton);msgInput.setPreferredSize(new Dimension(0, 120));chatPanel.add(msgInput, BorderLayout.SOUTH);msgShow.setEditable(false);// 设置消息显示区域为不可编辑// 集成TextUI的功能del = new JTableModel(clientList, userMap);table = new JTable(del);// 设置自定义渲染器,用于显示在线状态的颜色table.getColumnModel().getColumn(1).setCellRenderer(new StatusCellRenderer());JScrollPane tableScrollPane = new JScrollPane(table);tableScrollPane.setPreferredSize(new Dimension(150, 400));JButton button = new JButton("开始聊天");JPanel tablePanel = new JPanel();tablePanel.setLayout(new BorderLayout());tablePanel.add(tableScrollPane, BorderLayout.CENTER);tablePanel.add(button, BorderLayout.SOUTH);// 按钮点击事件button.addActionListener(e -> {int row = table.getSelectedRow();// 获取当前选中的行if (row != -1) {System.out.println("按钮点击,选中的客户端: " + table.getValueAt(row, 0).toString());}});// 表格选择事件table.getSelectionModel().addListSelectionListener(e -> {if (!e.getValueIsAdjusting()) {int row = table.getSelectedRow(); // 获取当前选中的行if (row != -1) {System.out.println("选中的行数是:" + row);String selectedClient = table.getValueAt(row, 0).toString();System.out.println(selectedClient);// 创建一个新的窗口,显示选中客户端的私聊界面JFrame frame = new JFrame(selectedClient);frame.setSize(400, 400);frame.setLayout(new BorderLayout());// 消息显示区域JTextArea privateMsgShow = new JTextArea();privateMsgShow.setEditable(false);JScrollPane privateScrollPane = new JScrollPane(privateMsgShow);frame.add(privateScrollPane, BorderLayout.CENTER);// 输入区域和发送按钮JPanel privateMsgInput = new JPanel();JTextArea privateMsgInputArea = new JTextArea();privateMsgInputArea.setEditable(true);JScrollPane privateInputScrollPane = new JScrollPane(privateMsgInputArea);privateInputScrollPane.setPreferredSize(new Dimension(300, 80));privateMsgInput.add(privateInputScrollPane);JButton privateSendButton = new JButton("发送");privateMsgInput.add(privateSendButton);privateMsgInput.setPreferredSize(new Dimension(0, 120));frame.add(privateMsgInput, BorderLayout.SOUTH);frame.setVisible(true);}}});add(chatPanel, BorderLayout.CENTER);// 将聊天面板添加到窗口的中央位置add(tablePanel, BorderLayout.EAST);// 将表格面板添加到窗口的右侧位置// 让输入区域的 JTextArea 获取焦点msgInputArea.requestFocusInWindow();setVisible(true); // 显示窗口}// 设置监听器的方法private void setupListener(ActionListener listener) {if (sendButton == null) {System.out.println("未找到发送按钮");}if (msgInputArea == null) {System.out.println("未找到输入框");}if (sendButton != null && msgInputArea != null) {sendButton.addActionListener(listener);// 为发送按钮添加监听器if (listener instanceof ChatListener) {ChatListener cl = (ChatListener) listener;cl.showMsg = msgShow;// 设置消息显示区域cl.msgInput = msgInputArea;// 设置消息输入框cl.userName = getTitle();// 设置用户名} else if (listener instanceof clientListener) {clientListener cl = (clientListener) listener;cl.showMsg = msgShow; // 设置消息显示区域cl.msgInput = msgInputArea; // 设置消息输入框cl.userName = getTitle();// 设置用户名}}}// 更新客户端列表的方法public void updateClientList(List<String> newClientList) {del.clientList = newClientList;// 更新表格模型中的客户端列表del.fireTableDataChanged();// 通知表格数据已更改,刷新表格显示}// 更新用户在线状态的方法public void updateUserOnlineStatus(String userName, boolean isOnline) {ClientUser user = userMap.get(userName); // 获取对应的用户对象if (user != null) {user.online = isOnline;// 设置用户的在线状态del.fireTableDataChanged(); // 通知表格数据已更改,刷新表格显示}}// 更新 TextUI 的用户在线状态public void updateUserOnlineStatusForTextUI(List<String> newClientList) {for (String clientName : newClientList) {ClientUser user = userMap.get(clientName);// 获取对应的用户对象if (user != null) {updateUserOnlineStatus(clientName, user.online);// 更新用户的在线状态}}}
}// 服务器端聊天监听器,实现 ActionListener 接口
class ChatListener implements ActionListener {public List<Socket> clientSockets;// 客户端套接字列表JTextArea showMsg;// 消息显示区域JTextArea msgInput; // 消息输入框String userName; // 用户名// 处理按钮点击事件public void actionPerformed(ActionEvent e) {String text = msgInput.getText().trim();// 获取输入框中的文本,并去除前后空格if (text.isEmpty()) return;// 创建聊天消息对象ChatMsg message = new ChatMsg(CtrlType.GROUP_CHAT, userName.length(), userName, -1, null, MsgType.TEXT, text.length(), text);showMsg.append(userName + ": " + message.getContentAsString() + "\n");// 在消息显示区域显示消息msgInput.setText("");// 清空输入框for (Socket cSocket : clientSockets) {try {OutputStream out = cSocket.getOutputStream();// 获取客户端套接字的输出流out.write(userName.getBytes().length);out.write(userName.getBytes());out.write(text.getBytes().length);out.write(text.getBytes());out.flush();} catch (IOException ex) {ex.printStackTrace();}}}
}// 客户端聊天监听器,实现 ActionListener 接口
class clientListener implements ActionListener {JTextArea showMsg;// 消息显示区域JTextArea msgInput;// 消息输入框String userName; // 用户名OutputStream out;// 输出流,用于向服务器发送消息// 处理按钮点击事件public void actionPerformed(ActionEvent e) {String text = msgInput.getText().trim();// 获取输入框中的文本,并去除前后空格if (text.isEmpty()) return;// 创建聊天消息对象ChatMsg message = new ChatMsg(CtrlType.GROUP_CHAT, userName.length(), userName, -1, null, MsgType.TEXT, text.length(), text);showMsg.append(userName + ": " + message.getContentAsString() + "\n");// 在消息显示区域显示消息msgInput.setText(""); // 清空输入框try {out.write(userName.getBytes().length);out.write(userName.getBytes());out.write(text.getBytes().length);out.write(text.getBytes());out.flush();} catch (IOException ex) {showMsg.append("发送消息失败\n");ex.printStackTrace();}}
}

继承自 JFrame,用于显示聊天窗口和处理聊天相关操作。

  • 界面初始化:在 setupUI 方法中,创建消息显示区域、输入框、发送按钮和客户端列表表格。设置表格的自定义渲染器,用于显示用户在线状态。
  • 事件监听:为发送按钮和表格选择事件添加监听器。发送按钮点击时,创建 ChatMsg 对象并将消息发送给服务器;表格选择时,创建一个新的私聊窗口。
  • 更新列表和状态:通过 updateClientList 和 updateUserOnlineStatus 方法更新客户端列表和用户在线状态。

 

TextUI

// 文本界面类,继承自 JFrame,用于显示客户端列表和处理相关操作
public class TextUI extends JFrame {private JTableModel del;// 自定义的表格模型private JTable table;// 表格组件private Map<String, ClientUser> userMap; // 用户映射,存储客户端的用户信息// 构造函数,初始化文本界面public TextUI(List<String> clientList, Map<String, ClientUser> userMap) {this.userMap = userMap;setSize(200, 500);// 设置窗口大小setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置窗口关闭操作del = new JTableModel(clientList, userMap);// 初始化表格模型table = new JTable(del);// 初始化表格组件// 设置自定义渲染器table.getColumnModel().getColumn(1).setCellRenderer(new StatusCellRenderer());// 创建滚动面板,用于显示表格JScrollPane scrollPane = new JScrollPane(table);scrollPane.setPreferredSize(new Dimension(400, 400));add(scrollPane, BorderLayout.CENTER);// 将滚动面板添加到窗口的中央位置JButton button = new JButton("开始聊天");add(button, BorderLayout.SOUTH);// 将按钮添加到窗口的底部位置// 按钮点击事件监听器button.addActionListener(e -> {int row = table.getSelectedRow();// 获取当前选中的行if (row != -1) {// 打印选中的客户端信息System.out.println("按钮点击,选中的客户端: " + table.getValueAt(row, 0).toString());}});// 表格选择事件监听器table.getSelectionModel().addListSelectionListener(e -> {if (!e.getValueIsAdjusting()) { // 避免多次触发int row = table.getSelectedRow();// 获取当前选中的行if (row != -1) {System.out.println("选中的行数是:" + row);System.out.println(table.getValueAt(row, 0).toString());// 创建一个新的窗口,显示选中客户端的聊天界面JFrame frame = new JFrame(table.getValueAt(row, 0).toString());frame.setSize(400, 400);frame.setVisible(true);}}});//setVisible(true);}// 更新客户端列表的方法public void updateClientList(List<String> newClientList) {del.clientList = newClientList;// 更新表格模型中的客户端列表del.fireTableDataChanged();// 通知表格数据已更改,刷新表格显示}// 更新用户在线状态的方法public void updateUserOnlineStatus(String userName, boolean isOnline) {ClientUser user = userMap.get(userName);// 获取对应的用户对象if (user != null) {user.online = isOnline;// 设置用户的在线状态del.fireTableDataChanged(); // 通知表格数据已更改,刷新表格显示}}
}

继承自 JFrame,用于显示客户端列表和处理相关操作。

  • 界面初始化:创建一个表格用于显示客户端列表,设置表格的自定义渲染器,添加一个 “开始聊天” 按钮。
  • 事件监听:为按钮和表格选择事件添加监听器,点击按钮或选择表格行时,输出选中的客户端信息并创建一个新的窗口。
  • 更新列表和状态:通过 updateClientList 和 updateUserOnlineStatus 方法更新客户端列表和用户在线状态。

 

LoginRegistMsg

public class LoginRegistMsg extends ClientMsg {// 注册消息: 功能类型 账号长度 账号 密码长度 密码// 登录消息: 功能类型 账号长度 账号 密码长度 密码// 构造函数,初始化登录注册消息public LoginRegistMsg(int ctrlType, int senderIdLen, String senderId, int passwordLen, String password) {this.ctrlType = ctrlType;this.senderIdLen = senderIdLen;this.senderId = senderId;this.passwordLen = passwordLen;this.password = password;}// 无参构造函数public LoginRegistMsg() {}// 从输入流中读取登录注册消息的方法public void readMsg(InputStream is) {try {this.ctrlType = is.read();// 读取控制类型this.senderIdLen = is.read();// 读取发送者ID长度byte[] senderIdBytes = new byte[senderIdLen];// 创建字节数组,用于存储发送者IDis.read(senderIdBytes);// 读取发送者IDthis.senderId = new String(senderIdBytes);// 将字节数组转换为字符串this.passwordLen = is.read();// 读取密码长度byte[] passwordBytes = new byte[passwordLen];// 创建字节数组,用于存储密码is.read(passwordBytes);// 读取密码this.password = new String(passwordBytes);// 将字节数组转换为字符串}catch (IOException e){e.printStackTrace();}}// 向输出流发送登录注册消息的方法public void sendMsg(OutputStream os) {try {os.write(ctrlType);// 发送控制类型os.write(senderIdLen); // 发送发送者ID长度os.write(senderId.getBytes());// 发送发送者IDos.write(passwordLen);// 发送密码长度os.write(password.getBytes()); // 发送密码os.flush();// 刷新输出流} catch (IOException e) {e.printStackTrace();}}}

继承自 ClientMsg,用于处理用户的登录注册消息。

  • 构造函数:提供有参和无参构造函数,用于初始化登录注册消息。
  • 读取消息:通过 readMsg 方法从输入流中读取登录注册消息的各个字段。
  • 发送消息:通过 sendMsg 方法将登录注册消息发送到输出流。

服务器相关

Server

public class Server {// 聊天服务器类,用于创建服务器对象并处理客户端连接和消息int port = 6667;// 服务器监听的端口号ServerSocket ss;// 服务器套接字List<Socket> clientSockets = new CopyOnWriteArrayList<>();// 客户端套接字列表,使用 CopyOnWriteArrayList 保证线程安全Map<Socket, String> clientNames = new HashMap<>();// 客户端套接字与用户名的映射Map<String, ClientUser> userMap = new HashMap<>();// 用户名与用户对象的映射private int nextUserId = 1;// 下一个用户的 IDprivate ChatUI ui;// 服务器端的聊天界面private TextUI clientListUI;// 创建一个 ThreadPoolExecutor 线程池private ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, // 核心线程数1000, // 最大线程数60L, // 线程空闲时间TimeUnit.SECONDS,new LinkedBlockingQueue<>(100) // 任务队列);// 初始化服务器的方法public void initServer() {try {ss = new ServerSocket(port);// 创建服务器套接字并绑定到指定端口System.out.println("服务器启动,等待客户端连接...");List<String> clientList = new ArrayList<>(userMap.keySet());// 获取当前在线用户的用户名列表ui = new ChatUI("服务器聊天界面", clientSockets, clientList, userMap);// 初始化服务器端的聊天界面clientListUI = new TextUI(clientList, userMap); // 初始化客户端列表界面} catch (IOException e) {throw new RuntimeException("服务器启动失败: " + e.getMessage());}}// 监听客户端连接的方法public void listenerConnection() {new Thread(() -> {// 创建一个新线程来监听客户端连接while (true) {// 持续监听try {Socket socket = ss.accept();// 接受客户端连接clientSockets.add(socket);// 将客户端套接字添加到列表中System.out.println("客户端已连接:" + socket.getInetAddress().getHostAddress());// 将客户端处理任务提交到线程池threadPool.submit(() -> {handleLoginRegist(socket);// 处理客户端登录注册handleClientMessages(socket);// 处理客户端消息});} catch (IOException e) {System.err.println("接受客户端连接失败: " + e.getMessage());}}}).start();}// 处理客户端登录的方法private void handleLoginRegist(Socket socket) {try {InputStream is = socket.getInputStream();// 获取客户端套接字的输入流LoginRegistMsg msg = new LoginRegistMsg();// 创建登录注册消息对象msg.readMsg(is);// 从输入流中读取登录注册消息OutputStream os = socket.getOutputStream();// 获取客户端套接字的输出流if (msg.ctrlType == CtrlType.REGISTER) {// 处理注册请求if (userMap.containsKey(msg.senderId)) {os.write(0); // 注册失败,用户名已存在} else {ClientUser user = new ClientUser(nextUserId++, msg.senderId, msg.password);// 创建新用户对象user.online = true;// 设置用户为在线状态userMap.put(msg.senderId, user);// 将用户添加到用户映射中clientNames.put(socket, msg.senderId);// 将客户端套接字与用户名进行映射os.write(1); // 注册成功broadcastSystemMessage(msg.senderId + " 加入了聊天室");// 广播系统消息,告知用户加入聊天室ui.msgShow.append("系统消息: " + msg.senderId + " 加入了聊天室\n");// 在服务器端聊天界面显示系统消息ui.updateUserOnlineStatus(msg.senderId, true); // 更新在线状态clientListUI.updateUserOnlineStatus(msg.senderId, true); // 更新 TextUI 的在线状态updateClientList();// 更新客户端列表broadcastClientList();// 广播客户端列表updateClientListUI();// 更新客户端列表界面}} else if (msg.ctrlType == CtrlType.LOGIN) {// 处理登录请求ClientUser user = userMap.get(msg.senderId);if (user != null && user.password.equals(msg.password)) {user.online = true;// 设置用户为在线状态clientNames.put(socket, msg.senderId);// 将客户端套接字与用户名进行映射os.write(1); // 登录成功broadcastSystemMessage(msg.senderId + " 加入了聊天室");// 广播系统消息,告知用户加入聊天室ui.msgShow.append("系统消息: " + msg.senderId + " 加入了聊天室\n");// 在服务器端聊天界面显示系统消息ui.updateUserOnlineStatus(msg.senderId, true); // 更新在线状态clientListUI.updateUserOnlineStatus(msg.senderId, true); // 更新 TextUI 的在线状态updateClientList();// 更新客户端列表broadcastClientList();// 广播客户端列表updateClientListUI();// 更新客户端列表界面// 发送离线消息sendOfflineMessages(socket, user);} else {os.write(0); // 登录失败,用户名或密码错误}}os.flush();} catch (IOException e) {System.err.println("处理登录注册失败: " + e.getMessage());closeClientSocket(socket);}}// 发送离线消息的方法private void sendOfflineMessages(Socket socket, ClientUser user) {try {OutputStream os = socket.getOutputStream();// 获取客户端套接字的输出流List<ChatMsg> offlineMessages = user.getOfflineMessages();// 获取用户的离线消息列表for (ChatMsg message : offlineMessages) {os.write((message.senderId+"(离线消息)").getBytes().length);// 发送发送者用户名的长度os.write((message.senderId+"(离线消息)").getBytes());// 发送发送者用户名os.write(message.textMsg.getBytes().length);// 发送消息内容的长度os.write(message.textMsg.getBytes());// 发送消息内容}user.clearOfflineMessages();// 清空用户的离线消息列表} catch (IOException e) {e.printStackTrace();}}// 处理客户端消息的方法private void handleClientMessages(Socket socket) {try {InputStream is = socket.getInputStream();// 获取客户端套接字的输入流while (true) {int nameLen = is.read();// 读取发送者用户名的长度if (nameLen == -1) { // 客户端关闭连接break;}if (nameLen == 0) {continue;}byte[] nameBytes = new byte[nameLen];// 创建字节数组,用于存储发送者用户名is.read(nameBytes);// 读取发送者用户名String senderName = new String(nameBytes);// 将字节数组转换为字符串int msgLen = is.read();// 读取消息内容的长度if (msgLen <= 0) {continue;}byte[] msgBytes = new byte[msgLen];// 创建字节数组,用于存储消息内容is.read(msgBytes);// 读取消息内容String message = new String(msgBytes);// 将字节数组转换为字符串ChatMsg msg = new ChatMsg(CtrlType.GROUP_CHAT, senderName.length(), senderName, -1, null, MsgType.TEXT, message.length(), message);// 处理私聊消息if (message.startsWith("@")) {int spaceIndex = message.indexOf(' ');// 查找消息中第一个空格的位置if (spaceIndex > 1) {String recipientName = message.substring(1, spaceIndex);// 获取接收者用户名String privateMessage = message.substring(spaceIndex + 1);// 获取私聊消息内容sendPrivateMessage(senderName, recipientName, privateMessage);// 发送私聊消息ui.msgShow.append(senderName + "(私聊给" + recipientName + "): " + msg.getContentAsString() + "\n");// 在服务器端聊天界面显示私聊消息continue;}}// 群聊消息broadcastMessage(senderName, message);// 广播群聊消息ui.msgShow.append(senderName + ": " + msg.getContentAsString() + "\n");// 在服务器端聊天界面显示群聊消息}} catch (IOException e) {// 客户端断开连接} finally {closeClientSocket(socket);// 关闭客户端套接字String userName = clientNames.get(socket);// 获取客户端对应的用户名if (userName != null) {ClientUser user = userMap.get(userName);// 获取用户对象if (user != null) {user.online = false;// 设置用户为离线状态ui.updateUserOnlineStatus(userName, false); // 更新离线状态clientListUI.updateUserOnlineStatus(userName, false); // 更新 TextUI 的离线状态}}closeClientSocket(socket);// 关闭客户端套接字updateClientListUI(); // 更新客户端列表并通知 TextUI 刷新updateClientList();// 更新客户端列表broadcastClientList();// 广播客户端列表}}// 发送私聊消息的方法private void sendPrivateMessage(String senderName, String recipientName, String message) {ClientUser sender = userMap.get(senderName);ClientUser recipient = userMap.get(recipientName);if (sender == null || recipient == null) {return;}ChatMsg msg = new ChatMsg(CtrlType.PRIVATE_CHAT, senderName.length(), senderName, recipientName.length(), recipientName, MsgType.TEXT, message.length(), message);if (recipient.online) {// 接收者在线,直接发送消息for (Map.Entry<Socket, String> entry : clientNames.entrySet()) {if (entry.getValue().equals(recipientName)) {try {OutputStream os = entry.getKey().getOutputStream();os.write((senderName + "(私聊)").getBytes().length);os.write((senderName + "(私聊)").getBytes());os.write(message.getBytes().length);os.write(message.getBytes());os.flush();} catch (IOException e) {e.printStackTrace();}break;}}} else {// 接收者离线,存储离线消息recipient.receiveMessage(msg);// 通知发送者for (Map.Entry<Socket, String> senderEntry : clientNames.entrySet()) {// 遍历客户端套接字与用户名的映射if (senderEntry.getValue().equals(senderName)) {// 找到发送者的客户端套接字try {OutputStream senderOs = senderEntry.getKey().getOutputStream();// 获取发送者的输出流senderOs.write("系统消息".getBytes().length);// 发送系统消息标识的长度senderOs.write("系统消息".getBytes());// 发送系统消息标识String offlineMsg = recipientName + " 当前不在线,消息将在对方上线后送达";// 构建离线消息提示信息senderOs.write(offlineMsg.getBytes().length);// 发送离线消息提示信息的长度senderOs.write(offlineMsg.getBytes());// 发送离线消息提示信息senderOs.flush();// 刷新输出流ui.msgShow.append("系统消息: " + offlineMsg + "\n");// 在服务器端聊天界面显示系统消息break;} catch (IOException e) {System.err.println("通知发送者失败: " + e.getMessage());}}}}}//    private void sendPrivateMessage(String senderName, String recipientName, String message) {
//        ClientUser sender = userMap.get(senderName);// 获取发送者用户对象
//        ClientUser recipient = userMap.get(recipientName);// 获取接收者用户对象
//
//        if (sender == null || recipient == null) {
//            return;// 如果发送者或接收者不存在,则返回
//        }
//
//        // 创建私聊消息
//        ChatMsg privateMsg = new ChatMsg(CtrlType.PRIVATE_CHAT, senderName.length(), senderName, recipientName.length(), recipientName, MsgType.TEXT, message.length(), message);
//
//        // 如果接收者在线,直接发送
//        if (recipient.online) {
//            for (Map.Entry<Socket, String> entry : clientNames.entrySet()) {// 遍历客户端套接字与用户名的映射
//                if (entry.getValue().equals(recipientName)) {// 找到接收者的客户端套接字
//                    try {
//                        OutputStream os = entry.getKey().getOutputStream();// 获取接收者的输出流
//                        os.write((senderName + "(私聊)").getBytes().length);// 发送发送者用户名(私聊标识)的长度
//                        os.write((senderName + "(私聊)").getBytes());// 发送发送者用户名(私聊标识)
//                        os.write(message.getBytes().length);// 发送消息内容的长度
//                        os.write(message.getBytes());// 发送消息内容
//                        os.flush();// 刷新输出流
//
//                        /*
//                        // 向发送者显示已发送
//                        for (Map.Entry<Socket, String> senderEntry : clientNames.entrySet()) {
//                            if (senderEntry.getValue().equals(senderName)) {
//                                OutputStream senderOs = senderEntry.getKey().getOutputStream();
//                                senderOs.write(("你(私聊给" + recipientName + ")").getBytes().length);
//                                senderOs.write(("你(私聊给" + recipientName + ")").getBytes());
//                                senderOs.write(message.getBytes().length);
//                                senderOs.write(message.getBytes());
//                                senderOs.flush();
//                                break;
//                            }
//                        }
//                        */
//
//                        return;
//                    } catch (IOException e) {
//                        System.err.println("发送私聊消息失败: " + e.getMessage());
//                    }
//                }
//            }
//        } else {
//            // 接收者不在线,存储离线消息
//            recipient.receiveMessage(privateMsg);// 调用接收者的 receiveMessage 方法,存储离线消息
//
//            // 通知发送者
//            for (Map.Entry<Socket, String> senderEntry : clientNames.entrySet()) {// 遍历客户端套接字与用户名的映射
//                if (senderEntry.getValue().equals(senderName)) {// 找到发送者的客户端套接字
//                    try {
//                        OutputStream senderOs = senderEntry.getKey().getOutputStream();// 获取发送者的输出流
//                        senderOs.write("系统消息".getBytes().length);// 发送系统消息标识的长度
//                        senderOs.write("系统消息".getBytes());// 发送系统消息标识
//                        String offlineMsg = recipientName + " 当前不在线,消息将在对方上线后送达";// 构建离线消息提示信息
//                        senderOs.write(offlineMsg.getBytes().length);// 发送离线消息提示信息的长度
//                        senderOs.write(offlineMsg.getBytes());// 发送离线消息提示信息
//                        senderOs.flush();// 刷新输出流
//                        ui.msgShow.append("系统消息: " + offlineMsg + "\n");// 在服务器端聊天界面显示系统消息
//                        break;
//                    } catch (IOException e) {
//                        System.err.println("通知发送者失败: " + e.getMessage());
//                    }
//                }
//            }
//
//
//
//        }
//    }// 广播群聊消息的方法private void broadcastMessage(String senderName, String message) {for (Socket clientSocket : clientSockets) {// 遍历所有客户端套接字try {if (clientNames.get(clientSocket).equals(senderName)) {continue; // 不回发给发送者}OutputStream os = clientSocket.getOutputStream();// 获取客户端套接字的输出流os.write(senderName.getBytes().length);// 发送发送者用户名的长度os.write(senderName.getBytes());// 发送发送者用户名os.write(message.getBytes().length);// 发送消息内容的长度os.write(message.getBytes());// 发送消息内容os.flush();// 刷新输出流} catch (IOException e) {System.err.println("广播消息失败: " + e.getMessage());closeClientSocket(clientSocket);// 关闭客户端套接字}}}// 广播系统消息的方法private void broadcastSystemMessage(String message) {for (Socket clientSocket : clientSockets) {// 遍历所有客户端套接字try {OutputStream os = clientSocket.getOutputStream();// 获取客户端套接字的输出流os.write("系统消息".getBytes().length);// 发送系统消息标识的长度os.write("系统消息".getBytes());// 发送系统消息标识os.write(message.getBytes().length);// 发送系统消息内容的长度os.write(message.getBytes());// 发送系统消息内容os.flush();// 刷新输出流} catch (IOException e) {System.err.println("广播系统消息失败: " + e.getMessage());closeClientSocket(clientSocket);// 关闭客户端套接字}}}// 关闭客户端套接字的方法private void closeClientSocket(Socket socket) {try {String userName = clientNames.get(socket);// 获取客户端的用户名if (userName != null) {clientNames.remove(socket);// 从 clientNames 中移除该客户端套接字与用户名的映射ClientUser user = userMap.get(userName);// 获取该用户对象if (user != null) {user.online = false;// 设置用户为离线状态}broadcastSystemMessage(userName + " 离开了聊天室");// 广播系统消息,通知其他用户该用户离开ui.msgShow.append("系统消息: " + userName + " 离开了聊天室\n");// 在服务器端聊天界面显示系统消息updateClientList(); // 更新客户端列表显示broadcastClientList();updateClientListUI();}socket.close();// 关闭客户端套接字clientSockets.remove(socket);// 从 clientSockets 中移除该客户端套接字} catch (IOException e) {System.err.println("关闭客户端连接失败: " + e.getMessage());}}// 更新客户端列表显示的方法private void updateClientList() {List<String> newClientList = new ArrayList<>(userMap.keySet());ui.updateClientList(newClientList);clientListUI.updateClientList(newClientList);}// 更新客户端列表并通知 TextUI 刷新private void updateClientListUI() {List<String> clientList = new ArrayList<>(userMap.keySet());clientListUI.updateClientList(clientList);}// 广播客户端列表的方法private void broadcastClientList() {StringBuilder clientListStr = new StringBuilder();// 创建一个字符串构建器,用于构建客户端列表字符串for (Map.Entry<String, ClientUser> entry : userMap.entrySet()) {// 遍历用户映射,获取每个用户的信息String clientName = entry.getKey();// 获取用户名ClientUser user = entry.getValue();// 获取用户对象// 将用户名和在线状态(1 表示在线,0 表示离线)添加到字符串构建器中,用逗号分隔clientListStr.append(clientName).append(",").append(user.online ? "1" : "0").append(";");}String listToSend = clientListStr.toString();// 将构建好的客户端列表字符串转换为字节数组byte[] listBytes = listToSend.getBytes();System.out.println("Sending client list: " + listToSend); // 添加日志输出for (Socket socket : clientSockets) {// 遍历所有客户端套接字try {OutputStream out = socket.getOutputStream();// 获取该套接字的输出流,用于向客户端发送客户端列表out.write("CLIENT_LIST".getBytes().length);// 发送特殊标识 "CLIENT_LIST" 的长度out.write("CLIENT_LIST".getBytes());// 发送特殊标识 "CLIENT_LIST"out.write(listBytes.length);// 发送客户端列表字节数组的长度out.write(listBytes);// 发送客户端列表字节数组out.flush();// 刷新输出流,确保消息已发送} catch (IOException e) {System.err.println("广播客户端列表失败: " + e.getMessage());}}}// 启动服务器的方法public void start() {initServer();// 初始化服务器//ui = new ChatUI("服务端", clientSockets);// 创建服务器端的聊天界面ui.setVisible(true);// 设置聊天界面可见listenerConnection();// 开始监听客户端连接}// 主方法,程序入口public static void main(String[] args) {Server server = new Server();// 创建服务器对象server.start();}
}

 这是服务器的主类,负责监听客户端连接、处理用户的登录注册、消息转发等功能。

  • 服务器初始化:在 initServer 方法中,创建 ServerSocket 对象并绑定到指定端口,创建服务器端的聊天界面和客户端列表界面。
  • 监听客户端连接:在 listenerConnection 方法中,使用一个新线程不断监听客户端连接,将客户端处理任务提交到线程池。
  • 处理登录注册:在 handleLoginRegist 方法中,从输入流中读取登录注册消息,根据消息类型处理注册或登录请求,更新用户信息和在线状态,广播系统消息和客户端列表。
  • 处理客户端消息:在 handleClientMessages 方法中,从输入流中读取客户端发送的消息,如果是私聊消息则调用 sendPrivateMessage 方法发送给指定用户,否则调用 broadcastMessage 方法广播群聊消息。
  • 广播消息:通过 broadcastSystemMessagebroadcastClientList 和 broadcastMessage 方法分别广播系统消息、客户端列表和群聊消息。
  • 发送离线消息:在 sendOfflineMessages 方法中,将用户的离线消息发送给客户端,并清空离线消息列表。

辅助类

JTableModel

// 自定义的表格模型类,继承自 AbstractTableModel,用于显示客户端列表和在线状态
public class JTableModel extends AbstractTableModel {List<String> clientList;// 客户端列表Map<String, ClientUser> userMap;// 用户映射,存储客户端的用户信息// 构造函数,初始化客户端列表和用户映射public JTableModel(List<String> clientList, Map<String, ClientUser> userMap) {this.clientList = clientList;this.userMap = userMap;}@Overridepublic int getRowCount() { // 获取表格的行数return clientList.size();}@Overridepublic int getColumnCount() {// 获取表格的列数return 2;}@Overridepublic String getColumnName(int column) {// 获取指定列的列名switch (column) {case 0:return "ID";case 1:return "在线状态";}return null;}@Overridepublic Object getValueAt(int rowIndex, int columnIndex) { // 获取指定单元格的值String clientName = clientList.get(rowIndex);// 获取当前行对应的客户端名称ClientUser user = userMap.get(clientName);// 获取对应的用户对象if (user == null) {//System.out.println("User not found for name: " + clientName);return null;}if (columnIndex == 0) {//System.out.println("Getting username: " + clientName);return clientName;} else if (columnIndex == 1) {//System.out.println("Getting online status for " + clientName + ": " + (user.online ? "在线" : "离线"));return user != null && user.online ? "在线" : "离线";}return null;}
}

继承自 AbstractTableModel,用于显示客户端列表和在线状态。

  • 表格模型:实现了表格的基本方法,如获取行数、列数、列名和单元格值。根据用户的在线状态显示 “在线” 或 “离线”。

 

StatusCellRenderer

// 自定义的表格单元格渲染器,继承自 DefaultTableCellRenderer,用于设置在线状态列的显示样式
public class StatusCellRenderer extends DefaultTableCellRenderer {@Overridepublic Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {// 调用父类的方法获取默认的单元格渲染组件Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);// 只处理“在线状态”列if (column == 1) {// 获取单元格的值String status = value.toString();if ("在线".equals(status)) {c.setForeground(Color.BLUE);} else if ("离线".equals(status)) {c.setForeground(Color.RED);}// 设置字体为粗体c.setFont(c.getFont().deriveFont(Font.BOLD));}return c;}
}

继承自 DefaultTableCellRenderer,用于设置在线状态列的显示样式。

  • 自定义渲染器:根据单元格的值(在线或离线)设置字体颜色为蓝色或红色,并将字体设置为粗体。

 

ClientConnect

public class ClientConnect {Socket socket;String clientID;// 发送者IDString password;// 发送者密码ArrayList<ClientMsg> clientMsgList = new ArrayList<>();// 发送者消息列表public ClientConnect(Socket socket, String clientID, String password) {this.socket = socket;this.clientID = clientID;this.password = password;}
}

用于存储客户端的连接信息和消息列表。

运行效果

群聊

私聊

离线

 

总结

这是一个完整的网络聊天系统的实现,包括客户端与服务器的通信、用户的登录注册、消息的发送与接收、离线消息的处理等功能。该系统使用 Java 的 Socket 编程实现网络通信,使用 Swing 库实现图形用户界面。同时,通过多线程和线程池的使用,提高了系统的并发处理能力。在实际应用中,还可以进一步扩展该系统,如添加文件传输、图片显示、群聊管理等功能。

http://www.dtcms.com/a/277523.html

相关文章:

  • 新手向:使用Python从PDF中高效提取结构化文本
  • LeetCode经典题解:21、合并两个有序链表
  • 【基础算法】倍增
  • Qt:编译qsqlmysql.dll
  • React强大且灵活hooks库——ahooks入门实践之常用场景hook
  • NoSQL 介绍
  • day052-ansible handler、roles与优化
  • Spring AI 项目实战(十七):Spring + AI + 通义千问星辰航空智能机票预订系统(附完整源码)
  • SDN软件定义网络架构深度解析:分层模型与核心机制
  • Datawhale AI 夏令营【更新中】
  • java虚拟线程
  • 面试150 从中序与后序遍历构造二叉树
  • Maven项目没有Maven工具,IDEA没有识别到该项目是Maven项目怎么办?
  • html案例:编写一个用于发布CSDN文章时,生成有关缩略图
  • 【拓扑排序+dfs】P2661 [NOIP 2015 提高组] 信息传递
  • 线下门店快速线上化销售四步方案
  • 在i.MX8MP上如何使能BlueZ A2DP Source
  • 如何设计高并发架构?深入了解高并发架构设计的最佳实践
  • Nature子刊 |HERGAST:揭示超大规模空间转录组数据中的精细空间结构并放大基因表达信号
  • DETRs与协同混合作业训练之CO-DETR论文阅读
  • Pandas 的 Index 与 SQL Index 的对比
  • Flask中的路由尾随斜杠(/)
  • SQL140 未完成率top50%用户近三个月答卷情况
  • react中为啥使用剪头函数
  • (nice!!!)(LeetCode 面试经典 150 题 ) 30. 串联所有单词的子串 (哈希表+字符串+滑动窗口)
  • win10 离线安装wsl
  • 论文翻译:Falcon: A Remote Sensing Vision-Language Foundation Model
  • 26-计组-数据通路
  • 楼宇自动化:Modbus 在暖通空调(HVAC)中的节能控制(一)
  • Linux驱动开发1:设备驱动模块加载与卸载