通信网络编程4.0——JAVA
一,服务端
1. 类和变量定义
public class ChatServer {// 聊天服务器类,用于创建服务器对象并处理客户端连接和消息int port = 6666;// 服务器监听的端口号ServerSocket ss;// 服务器套接字List<Socket> clientSockets = new CopyOnWriteArrayList<>();// 客户端套接字列表,使用 CopyOnWriteArrayList 保证线程安全Map<Socket, String> clientNames = new HashMap<>();// 客户端套接字与用户名的映射Map<String, User> userMap = new HashMap<>();// 用户名与用户对象的映射private int nextUserId = 1;// 下一个用户的 IDprivate ChatUI ui;// 服务器端的聊天界面
}
port
:服务器监听的端口号,设为6666。ss
:服务器套接字,用于接受客户端连接。clientSockets
:保存所有客户端套接字的列表,使用CopyOnWriteArrayList
保证线程安全。clientNames
:将客户端套接字与用户名进行映射。userMap
:将用户名与用户对象进行映射。nextUserId
:用于生成新用户的ID。ui
:服务器端的聊天界面
2. 初始化服务器
// 初始化服务器的方法public void initServer() {try {ss = new ServerSocket(port);// 创建服务器套接字并绑定到指定端口System.out.println("服务器启动,等待客户端连接...");} catch (IOException e) {throw new RuntimeException("服务器启动失败: " + e.getMessage());}}
- 创建一个
ServerSocket
对象,绑定到指定端口。 - 如果创建失败,抛出运行时异常。
3. 监听客户端连接
// 监听客户端连接的方法public void listenerConnection() {new Thread(() -> {// 创建一个新线程来监听客户端连接while (true) {// 持续监听try {Socket socket = ss.accept();// 接受客户端连接clientSockets.add(socket);// 将客户端套接字添加到列表中System.out.println("客户端已连接:" + socket.getInetAddress().getHostAddress());// 处理客户端登录handleClientLogin(socket);// 为每个客户端启动一个消息处理线程new Thread(() -> handleClientMessages(socket)).start();} catch (IOException e) {System.err.println("接受客户端连接失败: " + e.getMessage());}}}).start();}
- 使用一个线程持续调用
accept()
方法,接受客户端的连接。 - 每当有客户端连接时,将其套接字加入到
clientSockets
中。 - 调用
handleClientLogin()
处理客户端登录。 - 为每个客户端创建一个线程,调用
handleClientMessages()
处理客户端消息
4. 处理客户端登录
// 处理客户端登录的方法private void handleClientLogin(Socket socket) {try {InputStream is = socket.getInputStream();// 获取客户端套接字的输入流int nameLen = is.read();// 读取用户名的长度if (nameLen > 0) {byte[] nameBytes = new byte[nameLen];// 创建字节数组,用于存储用户名is.read(nameBytes);// 读取用户名String userName = new String(nameBytes);// 将字节数组转换为字符串// 注册新用户synchronized (userMap) {// 同步访问 userMap,避免并发问题User user = new User(nextUserId++, userName, "");// 创建新用户对象user.online = true;// 设置用户为在线状态userMap.put(userName, user);// 将用户对象添加到 userMap 中}clientNames.put(socket, userName);// 将客户端套接字与用户名的映射添加到 clientNames 中broadcastSystemMessage(userName + " 加入了聊天室");// 广播系统消息,通知其他用户有新用户加入ui.msgShow.append("系统消息: " + userName + " 加入了聊天室\n");// 在服务器端聊天界面显示系统消息updateClientList(); // 更新客户端列表显示broadcastClientList();}} catch (IOException e) {System.err.println("处理用户登录失败: " + e.getMessage());closeClientSocket(socket);// 关闭客户端套接字}}
- 读取客户端发送的用户名长度和用户名。
- 创建新用户对象并将其添加到
userMap
中。 - 将客户端套接字与用户名映射到
clientNames
中。 - 广播系统消息通知其他用户新用户加入。
- 更新客户端列表并广播更新后的列表
5. 处理客户端消息
// 处理客户端消息的方法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);// 将字节数组转换为字符串Message msg = new Message((byte)4,-1,-1,System.currentTimeMillis(),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);// 关闭客户端套接字}}
- 读取客户端发送的用户名和消息内容。
- 判断消息是否为私聊消息,如果是私聊消息,则调用
sendPrivateMessage()
方法发送私聊消息。 - 如果是群聊消息,则调用
broadcastMessage()
方法广播消息。 - 如果客户端断开连接,则关闭其套接字。
6. 发送私聊消息
// 发送私聊消息的方法private void sendPrivateMessage(String senderName, String recipientName, String message) {User sender = userMap.get(senderName);// 获取发送者用户对象User recipient = userMap.get(recipientName);// 获取接收者用户对象if (sender == null || recipient == null) {return;// 如果发送者或接收者不存在,则返回}// 创建私聊消息Message privateMsg = new Message((byte) 4, sender.id, recipient.id, System.currentTimeMillis(), 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());}}}}}
- 获取用户对象:通过
userMap
获取发送者和接收者的用户对象。 - 检查用户是否存在:如果发送者或接收者不存在,则直接返回。
- 创建私聊消息对象:创建一个
Message
对象,表示私聊消息。 - 判断接收者是否在线:
- 如果接收者在线,遍历
clientNames
找到接收者的客户端套接字,通过其输出流发送私聊消息。 - 如果接收者不在线,将私聊消息存储到接收者的离线消息列表中,并通知发送者接收者不在线,消息将在对方上线后送达。
- 发送私聊消息:
- 发送者用户名(带私聊标识)的长度和内容。
- 消息内容的长度和内容。
- 通知发送者:
- 如果接收者不在线,发送系统消息给发送者,告知其消息已存储,将在接收者上线后送达。
7. 广播群聊消息
// 广播群聊消息的方法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);// 关闭客户端套接字}}}
- 遍历所有客户端套接字:通过
clientSockets
列表遍历所有客户端。 - 跳过发送者:不将消息发送给消息的发送者。
- 发送消息:
- 发送发送者用户名的长度和内容。
- 发送消息内容的长度和内容。
- 处理异常:如果发送失败,捕获异常并关闭该客户端的套接字。
8. 广播系统消息
// 广播系统消息的方法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);// 关闭客户端套接字}}}
- 遍历所有客户端套接字:通过
clientSockets
列表遍历所有客户端。 - 发送系统消息:
- 发送系统消息标识的长度和内容。
- 发送系统消息内容的长度和内容。
- 处理异常:如果发送失败,捕获异常并关闭该客户端的套接字。
9. 关闭客户端套接字
// 关闭客户端套接字的方法private void closeClientSocket(Socket socket) {try {String userName = clientNames.get(socket);// 获取客户端的用户名if (userName != null) {clientNames.remove(socket);// 从 clientNames 中移除该客户端套接字与用户名的映射User user = userMap.get(userName);// 获取该用户对象if (user != null) {user.online = false;// 设置用户为离线状态}broadcastSystemMessage(userName + " 离开了聊天室");// 广播系统消息,通知其他用户该用户离开ui.msgShow.append("系统消息: " + userName + " 离开了聊天室\n");// 在服务器端聊天界面显示系统消息updateClientList(); // 更新客户端列表显示broadcastClientList();}socket.close();// 关闭客户端套接字clientSockets.remove(socket);// 从 clientSockets 中移除该客户端套接字} catch (IOException e) {System.err.println("关闭客户端连接失败: " + e.getMessage());}}
- 获取用户名:通过
clientNames
获取客户端的用户名。 - 更新用户状态:将用户设置为离线状态,并从
clientNames
和clientSockets
中移除该客户端。 - 广播系统消息:通知其他用户该用户已离开聊天室。
- 更新客户端列表:更新服务器端的客户端列表显示,并广播更新后的列表。
- 关闭套接字:关闭客户端的套接字。
10. 更新客户端列表
// 更新客户端列表显示的方法private void updateClientList() {StringBuilder clientList = new StringBuilder();// 创建一个 StringBuilder 对象,用于构建客户端列表字符串for (String name : clientNames.values()) {// 遍历所有客户端的用户名clientList.append(name).append("\n");// 将用户名添加到 StringBuilder 中,并添加换行符}ui.clientListShow.setText(clientList.toString());// 在服务器端聊天界面的客户端列表显示区域显示客户端列表}
- 构建客户端列表字符串:通过
clientNames
获取所有在线用户的用户名,构建一个字符串。 - 更新显示:将构建好的客户端列表字符串显示在服务器端的聊天界面中。
11. 广播客户端列表
// 广播客户端列表的方法private void broadcastClientList() {StringBuilder clientList = new StringBuilder();for (String name : clientNames.values()) {clientList.append(name).append("\n");}String listStr = clientList.toString();for (Socket clientSocket : clientSockets) {try {OutputStream os = clientSocket.getOutputStream();os.write("CLIENT_LIST".getBytes().length);os.write("CLIENT_LIST".getBytes());os.write(listStr.getBytes().length);os.write(listStr.getBytes());os.flush();} catch (IOException e) {System.err.println("广播客户端列表失败: " + e.getMessage());closeClientSocket(clientSocket);}}}
- 构建客户端列表字符串:通过
clientNames
获取所有在线用户的用户名,构建一个字符串。 - 广播客户端列表:通过每个客户端的输出流发送客户端列表。
- 处理异常:如果发送失败,捕获异常并关闭该客户端的套接字。
12. 启动服务器
// 启动服务器的方法public void start() {initServer();// 初始化服务器ui = new ChatUI("服务端", clientSockets);// 创建服务器端的聊天界面ui.setVisible(true);// 设置聊天界面可见listenerConnection();// 开始监听客户端连接}// 主方法,程序入口public static void main(String[] args) {ChatServer server = new ChatServer();// 创建服务器对象server.start();}
- 初始化服务器:调用
initServer()
方法初始化服务器。 - 创建聊天界面:创建服务器端的聊天界面,并使其可见。
- 监听客户端连接:调用
listenerConnection()
方法开始监听客户端连接。 - 程序入口:在
main
方法中创建ChatServer
对象并调用start()
方法启动服务器。
二,客户端
1. 类和变量定义
public class Client {// 客户端类,用于创建客户端对象并与服务器进行通信Socket socket;// 客户端套接字String ip;// 服务器的 IP 地址int port;// 服务器的端口号InputStream in;// 输入流,用于接收服务器的消息OutputStream out;// 输出流,用于向服务器发送消息private User user;// 用户对象private ChatUI ui;
}
socket
:客户端套接字,用于与服务器建立连接。ip
:服务器的IP地址。port
:服务器的端口号。in
:输入流,用于接收服务器发送的消息。out
:输出流,用于向服务器发送消息。user
:用户对象,暂未在代码中使用。ui
:聊天界面对象,用于显示消息和客户端列表。
2. 初始化客户端对象
public Client(String ip, int port) {//初始化客户端对象this.ip = ip;// 初始化服务器的 IP 地址this.port = port;// 初始化服务器的端口号}
- 构造方法,初始化客户端的IP地址和端口号。
3. 连接服务器
public void connectServer(String userName) {// 连接服务器的方法try {socket = new Socket(ip, port);// 创建套接字并连接到服务器in = socket.getInputStream();// 获取输入流out = socket.getOutputStream();// 获取输出流// 发送用户名out.write(userName.getBytes().length);// 发送用户名的长度out.write(userName.getBytes());// 发送用户名out.flush();// 刷新输出流System.out.println("连接服务器成功");} catch (IOException e) {throw new RuntimeException("连接服务器失败: " + e.getMessage());}}
- 创建一个
Socket
对象,连接到指定的服务器IP和端口。 - 获取输入流和输出流。
- 将用户名的长度和内容发送给服务器。
- 如果连接失败,抛出运行时异常。
4. 读取服务器消息
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 clientList = new String(listBytes);ui.clientListShow.setText(clientList);continue;}int msgLength = in.read();// 读取消息内容的长度byte[] msgBytes = new byte[msgLength];// 创建字节数组,用于存储消息内容in.read(msgBytes);// 读取消息内容String message = new String(msgBytes);// 将字节数组转换为字符串Message msg = new Message((byte)4,-1,-1,System.currentTimeMillis(),message);msgShow.append(senderName + ": " + msg.getContentAsString() + "\n");// 在消息显示文本区域显示消息}} catch (IOException e) {msgShow.append("与服务器断开连接\n");}}).start();}
- 创建一个新线程,持续从服务器读取消息。
- 读取发送者用户名的长度和内容。
- 如果发送者用户名为
"CLIENT_LIST"
,则读取客户端列表并更新显示。 - 否则,读取消息内容并显示在消息显示区域。
- 如果读取失败,捕获异常并显示断开连接的提示。
5. 启动客户端
public void startClient() {// 启动客户端的方法String userName = JOptionPane.showInputDialog("请输入用户名:");// 弹出对话框,让用户输入用户名while (userName == null || userName.trim().isEmpty()) {userName = JOptionPane.showInputDialog("用户名不能为空,请重新输入:");// 如果为空,提示用户重新输入}userName = userName.trim();// 去除用户名的首尾空格connectServer(userName);// 连接服务器ui = new ChatUI(userName, out);// 创建聊天界面readMsg(ui.msgShow);// 开始读取服务器消息// 心跳包,保持连接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();}
- 弹出对话框,提示用户输入用户名。
- 如果用户名为空,提示用户重新输入。
- 去除用户名的首尾空格。
- 调用
connectServer()
方法连接服务器。 - 创建聊天界面并开始读取服务器消息。
- 创建一个心跳包线程,每隔3秒发送一次心跳包,以保持与服务器的连接。
6. 主方法
public static void main(String[] args) {// 主方法,程序入口Client client = new Client("127.0.0.1", 6666);// 创建客户端对象client.startClient();// 启动客户端}
- 创建一个
Client
对象,指定服务器的IP地址和端口号。 - 调用
startClient()
方法启动客户端。
三,消息
1. 类和变量定义
public class Message {byte type; // 消息的类型,1-注册 2-登录 3-添加好友 4-文本消息 5-文件 6-图片int fromId;// 消息发送者的用户 IDint toId;// 消息接收者的用户 IDlong time;// 消息发送的时间戳int length;// 消息内容的长度List<Byte> content = new ArrayList<>();// 消息的内容
}
type
:消息的类型,使用字节表示。不同的值代表不同类型的消息,如注册、登录、添加好友、文本消息、文件和图片等。fromId
:消息发送者的用户ID。toId
:消息接收者的用户ID。time
:消息发送的时间戳,用于记录消息发送的时间。length
:消息内容的长度,即消息内容的字节数。content
:消息的内容,使用List<Byte>
存储,方便动态添加和访问消息内容的字节。
2. 构造方法
public Message(byte type, int fromId, int toId, long time, String content) {//初始化消息对象this.type = type;// 初始化消息类型this.fromId = fromId;// 初始化消息发送者的用户 IDthis.toId = toId;// 初始化消息接收者的用户 IDthis.time = time;// 初始化消息发送的时间戳this.length = content.getBytes().length;// 初始化消息内容的长度for (byte b : content.getBytes()) {// 将消息内容转换为字节数组,并添加到 content 列表中this.content.add(b);}}
- 构造方法用于初始化
Message
对象。 - 接收消息类型、发送者ID、接收者ID、时间戳和消息内容作为参数。
- 将消息内容转换为字节数组,并将每个字节添加到
content
列表中。 - 初始化消息内容的长度为字节数组的长度。
3. 获取消息内容为字符串
public String getContentAsString() {// 将消息内容的字节列表转换为字符串byte[] bytes = new byte[content.size()];// 创建一个字节数组,用于存储消息内容for (int i = 0; i < content.size(); i++) {// 将 content 列表中的字节复制到字节数组中bytes[i] = content.get(i);}SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String timeStr = sdf.format(new Date(time));return new String(bytes)+ " - " + timeStr;// 将字节数组转换为字符串并返回,添加时间显示}
- 创建字节数组:根据
content
列表的大小创建一个字节数组。 - 复制字节:将
content
列表中的每个字节复制到字节数组中。 - 格式化时间:使用
SimpleDateFormat
将时间戳格式化为可读的日期时间字符串。 - 返回消息内容:将字节数组转换为字符串,并在末尾添加时间戳,返回完整的消息内容。
四,用户
1. 类和变量定义
public class User {//用户类,表示一个用户对象int id;//用户唯一表示符String name;// 用户的名称String password;// 用户的密码boolean online;// 用户是否在线的标志List<User> friends;// 用户的好友列表List<Message> offlineMessages;// 用户的离线消息列表
}
id
:用户的唯一标识符,用于区分不同的用户。name
:用户的名称,用于显示和识别用户。password
:用户的密码,用于用户登录验证。online
:布尔值,表示用户是否在线。true
表示在线,false
表示离线。friends
:用户的好友列表,存储了用户的好友对象。offlineMessages
:用户的离线消息列表,存储了用户离线时收到的消息。
2. 构造方法
public User(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<>();// 初始化离线消息列表}
- 构造方法用于初始化
User
对象。 - 接收用户ID、名称和密码作为参数。
- 初始化用户状态为离线(
online = false
)。 - 初始化好友列表和离线消息列表为空的
ArrayList
。
3. 添加好友
public void addFriend(User friend) {// 向用户的好友列表中添加一个好友if (!friends.contains(friend)) {// 检查好友列表中是否已经包含该好友friends.add(friend);// 如果不包含,则添加该好友}}
- 检查好友列表:使用
contains
方法检查好友列表中是否已经包含该好友。 - 添加好友:如果好友列表中不包含该好友,则将其添加到好友列表中。
4. 接收消息
public void receiveMessage(Message message) {// 处理用户接收消息的逻辑if (online) {// 检查用户是否在线// 在线直接处理消息} else {offlineMessages.add(message);// 离线则将消息添加到离线消息列表}}
- 检查用户状态:使用
online
标志检查用户是否在线。 - 在线处理:如果用户在线,可以直接处理消息(这部分代码未实现)。
- 离线处理:如果用户离线,将消息添加到离线消息列表中,以便用户上线后处理。
五,UI界面
1. ChatUI
类
ChatUI
类继承自JFrame
,用于创建聊天窗口。
构造函数
public class ChatUI extends JFrame {// 聊天界面类,继承自 JFrame,用于创建聊天窗口public JTextArea msgShow = new JTextArea();// 用于显示聊天消息的文本区域public JTextArea clientListShow = new JTextArea(); // 新增:用于显示已连接客户端public ChatUI(String title, List<Socket> clientSockets) {// 构造函数,用于服务器端的聊天界面super(title);// 调用父类构造函数,设置窗口标题setupUI();// 调用 setupUI 方法,初始化界面ChatListener cl = new ChatListener();// 创建 ChatListener 对象cl.clientSockets = clientSockets;// 设置 ChatListener 的客户端套接字列表setupListener(cl);// 调用 setupListener 方法,设置事件监听器}public ChatUI(String title, OutputStream out) {// 构造函数,用于客户端的聊天界面super(title);// 调用父类构造函数,设置窗口标题setupUI();// 调用 setupUI 方法,初始化界面clientListener cl = new clientListener();// 创建 clientListener 对象cl.out = out;// 设置 clientListener 的输出流setupListener(cl);// 调用 setupListener 方法,设置事件监听器}
}
- 服务器端构造函数:接收窗口标题和客户端套接字列表,初始化界面并设置
ChatListener
。 - 客户端构造函数:接收窗口标题和输出流,初始化界面并设置
clientListener
。
初始化界面
private void setupUI() {// 初始化聊天界面的方法setSize(800, 500); // 调整窗口大小以适应新区域setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置窗口关闭时的操作// 创建一个分割面板,将窗口分为左右两部分JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);// 左侧:消息显示区域JScrollPane scrollPane = new JScrollPane(msgShow);// 创建滚动面板,用于显示消息scrollPane.setPreferredSize(new Dimension(500, 350));// 设置滚动面板的首选大小splitPane.setLeftComponent(scrollPane);// 将滚动面板添加到分割面板的左侧// 右侧:客户端列表显示区域JScrollPane clientListScrollPane = new JScrollPane(clientListShow);// 创建滚动面板,用于显示客户端列表clientListScrollPane.setPreferredSize(new Dimension(250, 350));// 设置滚动面板的首选大小clientListShow.setEditable(false);// 设置客户端列表文本区域为不可编辑splitPane.setRightComponent(clientListScrollPane);// 将滚动面板添加到分割面板的右侧add(splitPane, BorderLayout.CENTER);// 将分割面板添加到窗口的中央JPanel msgInput = new JPanel();// 创建一个面板,用于输入消息JTextArea msg = new JTextArea();// 创建一个文本区域,用于输入消息JScrollPane scrollPane1 = new JScrollPane(msg);// 创建滚动面板,用于输入消息scrollPane1.setPreferredSize(new Dimension(780, 80));// 设置滚动面板的首选大小msgInput.add(scrollPane1);// 将滚动面板添加到输入面板JButton send = new JButton("发送");// 创建一个发送按钮msgInput.add(send);// 将发送按钮添加到输入面板msgInput.setPreferredSize(new Dimension(0, 120));// 设置输入面板的首选大小add(msgInput, BorderLayout.SOUTH);// 将输入面板添加到窗口的底部msgShow.setEditable(false);// 设置消息显示文本区域为不可编辑setVisible(true);// 设置窗口可见}
- 设置窗口大小和关闭操作:设置窗口大小为800x500,并在关闭窗口时退出程序。
- 分割面板:使用
JSplitPane
将窗口分为左右两部分。 - 左侧:消息显示区域,使用
JScrollPane
包裹msgShow
。 - 右侧:客户端列表显示区域,使用
JScrollPane
包裹clientListShow
,并设置为不可编辑。 - 消息输入面板:创建一个面板,包含一个文本区域用于输入消息和一个发送按钮。
- 设置组件属性:设置滚动面板和输入面板的首选大小,设置消息显示区域为不可编辑。
- 显示窗口:调用
setVisible(true)
使窗口可见。
设置事件监听器
private void setupListener(ActionListener listener) {// 设置事件监听器的方法JButton sendButton = null;// 初始化发送按钮JTextArea msgInputArea = null;// 初始化消息输入文本区域for (Component comp : getContentPane().getComponents()) {// 遍历窗口的所有组件if (comp instanceof JPanel) {// 如果组件是面板for (Component innerComp : ((JPanel) comp).getComponents()) {// 遍历面板的所有组件if (innerComp instanceof JButton && ((JButton) innerComp).getText().equals("发送")) {sendButton = (JButton) innerComp;// 如果是发送按钮,则记录该按钮} else if (innerComp instanceof JScrollPane) {// 如果是滚动面板JScrollPane scrollPane = (JScrollPane) innerComp;if (scrollPane.getViewport().getView() instanceof JTextArea) {msgInputArea = (JTextArea) scrollPane.getViewport().getView();// 如果滚动面板中包含文本区域,则记录该文本区域}}}}if (sendButton != null && msgInputArea != null) break;// 如果找到发送按钮和消息输入文本区域,则退出循环}if (sendButton != null && msgInputArea != null) {sendButton.addActionListener(listener);// 为发送按钮添加事件监听器if (listener instanceof ChatListener) {// 如果监听器是 ChatListener 类型ChatListener cl = (ChatListener) listener;cl.showMsg = msgShow;// 设置 ChatListener 的消息显示文本区域cl.msgInput = msgInputArea;// 设置 ChatListener 的消息输入文本区域cl.userName = getTitle();// 设置 ChatListener 的用户名} else if (listener instanceof clientListener) {// 如果监听器是 clientListener 类型clientListener cl = (clientListener) listener;cl.showMsg = msgShow;// 设置 clientListener 的消息显示文本区域cl.msgInput = msgInputArea;// 设置 clientListener 的消息输入文本区域cl.userName = getTitle();// 设置 clientListener 的用户名}}}
}
- 查找组件:遍历窗口的所有组件,找到发送按钮和消息输入文本区域。
- 设置事件监听器:为发送按钮添加事件监听器。
- 设置监听器属性:根据监听器的类型,设置消息显示文本区域、消息输入文本区域和用户名。
2. ChatListener
类
ChatListener
类实现了ActionListener
接口,用于服务器端的消息发送逻辑。
class ChatListener implements ActionListener {// 服务器端聊天事件监听器类,实现 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;// 如果消息为空,则返回Message message = new Message((byte)4,-1,-1,System.currentTimeMillis(),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();}}}
}
- 获取输入的消息并去除首尾空格。
- 如果消息为空,直接返回。
- 创建一个
Message
对象,表示要发送的消息。 - 将消息显示在消息显示文本区域。
- 清空消息输入文本区域。
- 遍历所有客户端套接字,将消息发送给每个客户端。
3. clientListener
类
clientListener
类实现了ActionListener
接口,用于客户端的消息发送逻辑。
class clientListener implements ActionListener {// 客户端聊天事件监听器类,实现 ActionListener 接口JTextArea showMsg;// 消息显示文本区域JTextArea msgInput;// 消息输入文本区域String userName;// 用户名OutputStream out;// 输出流public void actionPerformed(ActionEvent e) {// 处理按钮点击事件的方法String text = msgInput.getText().trim();// 获取输入的消息并去除首尾空格if (text.isEmpty()) return;// 如果消息为空,则返回Message message = new Message((byte)4,-1,-1,System.currentTimeMillis(),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();}}
}
- 获取输入的消息并去除首尾空格。
- 如果消息为空,直接返回。
- 创建一个
Message
对象,表示要发送的消息。 - 将消息显示在消息显示文本区域。
- 清空消息输入文本区域。
- 通过输出流将消息发送给服务器。
六,运行效果
七,总结
这个基于 Java Socket 的多人聊天系统实现了一个基本的在线聊天功能,包括用户登录、群聊、私聊、客户端列表显示等功能。通过服务器和客户端的配合,实现了多个用户之间的实时消息交流。
系统的核心在于服务器端的多线程处理和客户端的实时数据接收。服务器通过为每个客户端连接创建一个新的线程,实现了并发处理多个客户端请求的能力。客户端则通过不断地读取服务器发送的数据,实时更新聊天界面。