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

项目:聊天室小项目

该项目是一个基于 Java 开发的网络聊天系统,包含客户端和服务器端,支持用户登录、私聊、群聊以及显示在线用户列表等功能。以下是对项目的详细总结:

主要文件及功能

1. ChatServer.java

  • 功能 :作为聊天系统的服务器端,负责处理客户端的连接、消息转发和用户状态管理。
  • 关键方法 :
    • handleRead(SelectionKey key) :处理客户端的读取事件,根据消息类型(如登录、私聊、群聊等)进行相应处理。
    • broadcast(String message) 和 broadcast(String message, SocketChannel sex) :用于向所有客户端或除指定客户端外的其他客户端广播消息。

2. ChatClientUI.java

  • 功能 :图形用户界面(GUI)版本的聊天客户端,使用 Swing 库实现。用户可以通过界面进行登录、选择聊天对象(私聊或群聊)并发送消息。
  • 关键组件 :
    • JFrame 、 JPanel 、 JTextField 、 JButton 等:用于构建用户界面。
    • JTextArea :显示聊天消息。
    • JList :显示在线用户列表。
  • 关键方法 :
    • connectToServer() :与服务器建立连接,并启动一个线程处理服务器消息。
    • sendMessage(String message) :将消息发送给服务器。
    • ServerMessageHandler 类:处理服务器发送的消息,根据消息类型更新在线用户列表和聊天区域。

消息协议

项目定义了一套消息协议,消息头为 5 个字符,用于区分不同类型的消息:

  • LOGIN :用户登录。
  • OFFUR :用户下线。
  • ONUSR :用户上线。
  • ONALL :在线用户列表。
  • 4USER :发送给指定用户的私聊消息。
  • 4ALLS :发送给所有用户的群聊消息。

总结

该项目实现了一个基本的网络聊天系统,结合了命令行和图形用户界面两种客户端。通过 NIO 实现了非阻塞的网络通信,提高了系统的性能。同时,使用自定义的消息协议确保了消息的正确处理和转发。

缺点

没有处理TCP黏连问题。在快速连续两次发送时,会出现TCP黏连问题,导致不能按指定消息处理。

服务端代码

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;public class ChatServer {private static final int PORT = 8888;private Selector selector;private ServerSocketChannel serverSocketChannel;private Map<String, SocketChannel> clients = new HashMap<>();// 新增一个映射来记录客户端的连接时间private Map<SocketChannel, Long> connectionTimes = new HashMap<>();public ChatServer() {try {selector = Selector.open();serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(PORT));serverSocketChannel.configureBlocking(false);serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("服务器已启动,监听端口:" + PORT);} catch (IOException e) {e.printStackTrace();System.exit(1);}}public void start() {try {while (true) {int count = selector.select();if (count > 0) {Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();if (key.isAcceptable()) {handleAccept(key);} else if (key.isReadable()) {handleRead(key);}iterator.remove();}}}} catch (IOException e) {e.printStackTrace();}}private void handleAccept(SelectionKey key) throws IOException {ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel client = server.accept();client.configureBlocking(false);client.register(selector, SelectionKey.OP_READ);System.out.println("新客户端连接:" + client.getRemoteAddress());// 记录客户端的连接时间connectionTimes.put(client, new Date().getTime()); }/** 服务端消息定义:* 消息头:发送到那个客户端#那个客户端发来的@* 消息头 : 5个字符,例如:LOGIN 登录 ,OFFUR 下线,ONUSR 上线,ONALL 在线列表,4USER 发送给指定客户* 4ALLS 发送给所有客户端* * 客户端消息定义:消息头:那个客户端发来的@* * */private void handleRead(SelectionKey key) {SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);try {int read = client.read(buffer);if (read > 0) {buffer.flip();byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes);String message = new String(bytes).trim();System.out.println(message);if (message.startsWith("LOGIN:")) {String username = message.substring(6);// 检查用户是否已经登录if (clients.containsKey(username)) {// 向客户端发送提示信息client.write(ByteBuffer.wrap(("用户 " + username + " 已登录,请勿重复登录").getBytes()));// 关闭连接client.close();key.cancel();connectionTimes.remove(client);} else {clients.put(username, client);// 登录成功后,移除连接时间记录connectionTimes.remove(client);//发送当前在线用户列表给新登录的用户StringBuilder onlineUsers = new StringBuilder("ONALL:");for (String onlineUser : clients.keySet()) {onlineUsers.append(onlineUser).append(",");}onlineUsers.delete(onlineUsers.length() - 1, onlineUsers.length()); // 删除最后一个逗号和空格client.write(ByteBuffer.wrap(onlineUsers.toString().getBytes()));broadcast("ONUSR:" + username, client );}}else if (connectionTimes.containsKey(client)){// 还在登录阶段,不处理消息return;}else if(message.startsWith("4USER:")) {int userSplite  = message.indexOf("#");String username = message.substring(6,userSplite);String messageContent ="4USER:" + message.substring(userSplite+1);SocketChannel toClient = clients.get(username);if (toClient!= null) {toClient.write(ByteBuffer.wrap(messageContent.getBytes()));}} else  if(message.startsWith("4ALLS:")) {int userSplite  = message.indexOf("#");String messageContent ="4ALLS:" + message.substring(userSplite+1);broadcast( messageContent, client);}} else if (read == -1) {String username = getUsernameByChannel(client);if (username != null) {clients.remove(username);broadcast("OFFUR:" + username ,client);}client.close();connectionTimes.remove(client);}} catch (IOException e) {// 处理客户端异常断开连接的情况String username = getUsernameByChannel(client);if (username != null) {clients.remove(username);broadcast("OFFUR:" + username ,client);}try {client.close();} catch (IOException ex) {ex.printStackTrace();}connectionTimes.remove(client);key.cancel();}}private void broadcast(String message) {for (Map.Entry<String, SocketChannel> entry : clients.entrySet()) {SocketChannel client = entry.getValue();try {client.write(ByteBuffer.wrap(message.getBytes()));} catch (IOException e) {e.printStackTrace();}}}private void broadcast( String message, SocketChannel sex) {for (Map.Entry<String, SocketChannel> entry : clients.entrySet()) {SocketChannel client = entry.getValue();try {if (client.equals(sex)) {continue;}client.write(ByteBuffer.wrap(message.getBytes()));} catch (IOException e) {e.printStackTrace();}}}private String getUsernameByChannel(SocketChannel channel) {for (Map.Entry<String, SocketChannel> entry : clients.entrySet()) {if (entry.getValue().equals(channel)) {return entry.getKey();}}return null;}public static void main(String[] args) {ChatServer server = new ChatServer();ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);// 固定延迟执行(上一次结束后延迟5秒执行下一次)// 但如果用户连接超过5秒,未登录闭连接,防止恶意攻击。executor.scheduleWithFixedDelay(() -> {// System.out.println("任务执行:" + System.currentTimeMillis());try { // 检查是否超时for (Map.Entry<SocketChannel, Long> entry : server.connectionTimes.entrySet()) {SocketChannel client = entry.getKey();Long connectTime = entry.getValue();if (connectTime != null && (new Date().getTime() - connectTime) > 5000) {// 由于在静态上下文中无法直接访问非静态字段,通过 server 实例来访问 connectionTimesserver.connectionTimes.remove(client);try {client.close();} catch (IOException e) {e.printStackTrace();}}}} catch (Exception e) {}}, 0,5, TimeUnit.SECONDS);// 固定频率执行(每3秒执行一次,无论任务耗时)// executor.scheduleAtFixedRate(() -> {//     System.out.println("固定频率任务:" + System.currentTimeMillis());// }, 0, 3, TimeUnit.SECONDS);server.start();}
}

客户端代码

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class ChatClientUI {private static final String SERVER_HOST = "127.0.0.1";private static final int SERVER_PORT = 8888;private Selector selector;private SocketChannel socketChannel;private String username;private JFrame frame;private JTextField usernameField;private JTextField messageField;private JTextArea chatArea;private JButton loginButton;private JButton sendButton;private DefaultListModel<String> userListModel;private JList<String> userList;public ChatClientUI() {// 初始化界面组件frame = new JFrame("聊天客户端");frame.setSize(600, 600);frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setLayout(new BorderLayout());// 登录面板JPanel loginPanel = new JPanel();usernameField = new JTextField(15);loginButton = new JButton("登录");loginPanel.add(new JLabel("用户名:"));loginPanel.add(usernameField);loginPanel.add(loginButton);// 聊天区域chatArea = new JTextArea();chatArea.setEditable(false);JScrollPane scrollPane = new JScrollPane(chatArea);// 消息输入面板JPanel messagePanel = new JPanel(new FlowLayout()); // 显式设置 FlowLayoutmessageField = new JTextField(25);sendButton = new JButton("发送");sendButton.setEnabled(false);// 添加 JLabel 显示当前要发送给谁JLabel recipientLabel = new JLabel("当前发送给: 所有人");messagePanel.add(recipientLabel);messagePanel.add(messageField);messagePanel.add(sendButton);// 初始化在线用户列表userListModel = new DefaultListModel<>();userList = new JList<>(userListModel);JScrollPane userListScrollPane = new JScrollPane(userList);userListScrollPane.setPreferredSize(new Dimension(150, 0));// 使用 JSplitPane 分割聊天区域和在线用户列表JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scrollPane, userListScrollPane);splitPane.setDividerLocation(450);frame.add(loginPanel, BorderLayout.NORTH);frame.add(splitPane, BorderLayout.CENTER);frame.add(messagePanel, BorderLayout.SOUTH);// 在线用户列表添加选择监听器userList.addListSelectionListener(e -> {if (!e.getValueIsAdjusting()) {String selectedUser = userList.getSelectedValue();if (selectedUser != null) {recipientLabel.setText("当前发送给: " + selectedUser);} else {recipientLabel.setText("当前发送给: 所有人");}}});// 登录按钮事件监听器loginButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {username = usernameField.getText().trim();// 验证用户名是否只包含字母和数字if (!username.matches("[a-zA-Z0-9]+") || username.length()>8) {// 使用弹出框警告JOptionPane.showMessageDialog(frame, "用户名只能包含字母和数字,请重新输入。", "警告", JOptionPane.WARNING_MESSAGE);return;}if (!username.isEmpty()) {try {connectToServer();loginButton.setEnabled(false);usernameField.setEditable(false);sendButton.setEnabled(true);} catch (IOException ex) {chatArea.append("连接服务器失败,请重试。\n");}} else {chatArea.append("请输入用户名。\n");}}});// 发送按钮事件监听器sendButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {String message = messageField.getText().trim();if (!message.isEmpty()) {try {// 获取选中的用户String selectedUser = userList.getSelectedValue();if (selectedUser != null && !selectedUser.equals("所有人")) {recipientLabel.setText("当前发送给: " + selectedUser);// 构造私聊消息格式chatArea.append("我@"+ selectedUser + "#:" +message+"\n"); message = "4USER:" + selectedUser + "#" + username + "@"+message;} else {recipientLabel.setText("当前发送给: 所有人");chatArea.append("我@所有人#:" +message+"\n"); message = "4ALLS:" + selectedUser + "#" + username + "@"+message;}sendMessage(message);messageField.setText("");} catch (Exception ex) {chatArea.append("发送消息失败,请重试。\n");}}}});frame.setVisible(true);}private void connectToServer() throws IOException {selector = Selector.open();socketChannel = SocketChannel.open();socketChannel.configureBlocking(false);socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));socketChannel.register(selector, SelectionKey.OP_CONNECT);// 启动一个线程处理服务器消息new Thread(new ServerMessageHandler()).start();}private void sendMessage(String message) throws IOException {if (username != null) {if (message.startsWith("LOGIN:") == false) {socketChannel.write(ByteBuffer.wrap(message.getBytes()));}}}private class ServerMessageHandler implements Runnable {@Overridepublic void run() {try {while (true) {int count = selector.select();if (count > 0) {Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();if (key.isConnectable()) {SocketChannel client = (SocketChannel) key.channel();if (client.isConnectionPending()) {client.finishConnect();client.register(selector, SelectionKey.OP_READ);// 发送登录信息client.write(ByteBuffer.wrap(("LOGIN:" + username).getBytes()));}} else if (key.isReadable()) {/* 服务端消息定义:* 消息头:发送到那个客户端#那个客户端发来的@* 消息头 : 5个字符,例如:LOGIN 登录 ,OFFUR 下线,ONUSR 上线,ONALL 在线列表,4USER 发送给指定客户* 4ALLS 发送给所有客户端* * 客户端消息定义:消息头:那个客户端发来的@* 消息头有:OFFUR 下线,ONUSR 上线,ONALL 在线列表,4USER 发送给指定客户* 4ALLS 发送给所有客户端. 如果不在定义的消息头中,说明是错误信息,打印,并且让发送按钮不可用*/SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int read = client.read(buffer);if (read > 0) {buffer.flip();byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes);String message = new String(bytes).trim();System.out.println(message);// 处理消息if (message.startsWith("ONALL:")) { // 更新在线列表SwingUtilities.invokeLater(new Runnable() {@Overridepublic void run() {// 提取消息内容String privateMessage = message.substring(6);userListModel.clear();userListModel.addElement("所有人");String[] usernames = privateMessage.split(",");for (String username : usernames) {userListModel.addElement(username);}}});} else if (message.startsWith("4USER:")) { // 私聊消息// 提取消息内容int spliteIndex = message.indexOf("@");if (spliteIndex != -1) { // String sendUser = message.substring(6, spliteIndex);String privateMessage = message.substring(spliteIndex+1);chatArea.append("私聊:#" + sendUser + "#: " + privateMessage + "\n");}} else if (message.startsWith("4ALLS")){ // 群聊消息// 提取消息内容int spliteIndex = message.indexOf("@");if (spliteIndex != -1) { // String sendUser = message.substring(6, spliteIndex);String privateMessage = message.substring(spliteIndex+1);chatArea.append("群发:#" + sendUser + "#: " + privateMessage + "\n");}}else if (message.startsWith("ONUSR")) { // 有人登录,更新在线列表                                        chatArea.append("登录成功。\n");String usrName = message.substring(6);if (!userListModel.contains(usrName)) {SwingUtilities.invokeLater(new Runnable() {@Overridepublic void run() {userListModel.addElement(usrName);}});}} else if (message.startsWith("OFFUR")) {  // 有人退出,更新在线列表SwingUtilities.invokeLater(new Runnable() {@Overridepublic void run() {String usrName = message.substring(6);userListModel.removeElement(usrName);}});}else{chatArea.append("错误:#" +message + "\n");sendButton.setEnabled(false);}}}iterator.remove();}}}} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {SwingUtilities.invokeLater(new Runnable() {@Overridepublic void run() {new ChatClientUI();}});}
}

效果图

在这里插入图片描述

http://www.xdnf.cn/news/8945.html

相关文章:

  • day37 python早停策略和模型权重的保存
  • PSNR指标Pytorch实现
  • Spring IoC(1)
  • 论文分享之Prompt优化
  • sass三大循环语法
  • 策略模式与责任链模式学习笔记:从入门到理解
  • ROS架构
  • OpenCV图像认知(二)
  • Docker系列(四):容器操作全栈指南 —— 从入门到生产级管控
  • 三大微调技术对比:Prompt/Prefix/P-Tuning
  • C++ : list
  • TDengine 中集群维护
  • 基于开源链动2+1模式AI智能名片S2B2C商城小程序的产品驱动型增长策略研究
  • 猿大师办公助手网页编辑Office/wps支持服务器文件多线程下载吗?
  • 技术文档写作方法——以MATLAB滤波为例
  • 仓储物流场景下国标GB28181视频平台EasyGBS视频实时监控系统应用解决方案
  • Webtrees 手册/程序概述
  • 组态王KingSCADA3.53连接S7-1200PLC实战教程
  • Nginx 基本概念深度解析:从服务器特性到代理模式应用​
  • 亚当·斯密思想精髓的数学建模与形式化表征
  • 《软件工程》第 15 章 - 软件度量与估算:从概念到实践​
  • 离线安装Microsoft 照片【笔记】
  • Language Model
  • Vue-01(Vue CLI创建项目以及文件配置说明)
  • 爬虫学习-Scrape Center spa2 超简单 JS 逆向
  • 【WEB3】区块链、隐私计算、AI和Web3.0——可信数字身份体系构建(3)
  • Science Robotics 具身智能驱动的空中物理交互新范式:结合形态和传感,与非结构化环境进行稳健交互
  • 2025.05.22-得物春招机考真题解析-第二题
  • 【算法深练】双序列双指针:用“双轨并行”思维,高效破解算法难题
  • RabbitMQ 集群与高可用方案设计(三)