Java实战:实时聊天应用开发(附GitHub链接)

一、前置技术

  • 项目介绍

    • 项目为局域网沟通软件,类似内网通,核心功能包括昵称输入、聊天界面展示在线人数(实时更新)、群聊,也可扩展私聊、登录注册、聊天记录存储等功能,结尾附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); // 存入在线集合
  • 更新全部客户端在线人数列表的方法

    • 方法功能:向所有在线客户端推送最新的在线用户列表。
    • 实现步骤:
      1. 获取所有在线用户的昵称(onlineSocketsvalues)。
      2. 遍历所有在线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();
            }
        }
    }
  • 客户端下线的处理

    • 当客户端断开连接(抛出异常),服务端需将其SocketonlineSockets中移除,并触发在线列表更新。
    java 复制代码
    // 客户端下线时移除在线记录
    Server.onlineSockets.remove(socket);
    updateClientOnlineList(); // 重新更新在线列表
  • 整体流程

    1. 服务端接收消息类型编号,判断为登录消息(1)。
    2. 读取昵称并存储到onlineSockets
    3. 调用updateClientOnlineList()方法,向所有客户端推送更新后的在线列表。
    4. 客户端接收消息后,更新本地展示的在线用户列表。

五、服务端(聊天信息转发模块)

  • 解决下线操作的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(包括发送者自身),确保消息在所有客户端面板展示。

  • 读取客户端发送的文本消息

    从数据输入流中读取客户端的文本消息:

    java 复制代码
    DataInputStream dis = new DataInputStream(socket.getInputStream());
    String message = dis.readUTF(); // 读取客户端发送的群聊内容
  • 拼装消息内容

    1. 获取发送者昵称 :通过当前socketmap集合中获取对应的用户名

      java 复制代码
      String senderName = Server.onlineSockets.get(socket); // onlineSockets为<Socket, String>类型的map
    2. 获取并格式化时间 :使用LocalDateTimeDateTimeFormatter处理时间

      java 复制代码
      LocalDateTime now = LocalDateTime.now();
      DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
      String timeStr = dtf.format(now); // 格式化时间为字符串
    3. 拼接消息 :使用StringBuilder组合昵称、时间和消息内容,添加格式符优化展示

      java 复制代码
      StringBuilder sb = new StringBuilder();
      sb.append(senderName) // 发送者昵称
        .append(" ") 
        .append(timeStr) // 发送时间
        .append("\r\n") // 换行
        .append(message) // 消息内容
        .append("\r\n"); // 消息间换行
      String fullMessage = sb.toString(); // 转换为字符串
  • 转发消息给所有在线客户端

    遍历所有在线socket,发送拼装好的消息(消息类型为2号):

    java 复制代码
    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();
            }
        }
    }
  • 添加死循环处理多次消息

    线程需通过死循环持续接收消息,避免只处理一次后终止:

    java 复制代码
    while (true) { // 死循环:持续监听客户端消息
        int msgType = dis.readInt(); // 读取消息类型
        if (msgType == 2) { // 处理2号群聊消息
            String message = dis.readUTF();
            sendMsgToAll(buildFullMessage(socket, message)); // 拼装并转发消息
        }
        // 可扩展处理其他消息类型(如3号私聊消息)
    }

六、客户端(登录开发)

  • 开发客户端的准备与思路:服务端模块已开发完成,接下来需开发客户端,客户端初始仅有界面,需与服务端对接。开发从登录界面开始,遵循用户思维和线性思维,即按照用户操作流程推进。

  • 登录界面的初始操作

    • 给登录界面的"进入"按钮(后改为"登录"按钮)绑定点击事件监听器,代码使用匿名内部类或Lambda表达式简化实现:
    java 复制代码
    entryButton.addActionListener(e -> {
        // 获取昵称
        String nickname = nicknameInput.getText();
        nicknameInput.setText(""); // 清空输入框
        if (nickname != null && !nickname.isEmpty()) {
            try {
                login(nickname); // 调用登录方法
                dispose(); // 关闭登录窗口
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    });
    • 点击按钮后,从输入框获取昵称,判断非空后执行登录逻辑,并关闭登录窗口。
  • 登录方法的创建与完善

    • 将登录相关代码独立为login方法,避免代码臃肿:
    java 复制代码
    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);
    }
    • 服务端的IP和端口在常量类Constant中定义,方便后续修改:
    java 复制代码
    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定义为全局变量:

    java 复制代码
    private Socket socket; // 登录界面的全局Socket变量,用于保存与服务端的连接
  • 聊天界面的初始化

    • 聊天界面通过有参构造器接收昵称和Socket管道,并调用无参构造器初始化界面:
    java 复制代码
    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管道用于后续接收在线人数列表、发送和接收消息等操作。

七、客户端(在线人数展示)

  • 回顾登录界面跳转逻辑:登录成功后,将昵称和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方法,将消息追加到展示区域:
    java 复制代码
    public 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),连接到同一服务端进行多人测试。
    • 可优化的方向包括:消息自动滚动到底部、完善换行显示、支持发送图片、美化界面等。
bash 复制代码
GitHub:https://github.com/Andy123211/chat-system/tree/master
相关推荐
西猫雷婶11 分钟前
python学智能算法(十九)|SVM基础概念-超平面
开发语言·人工智能·python·深度学习·算法·机器学习·支持向量机
汤姆yu25 分钟前
基于springboot的考研互助小程序
java·spring boot·后端·考研互助
MoFe11 小时前
【.net core】支持通过属性名称索引的泛型包装类
java·开发语言·.netcore
没有bug.的程序员1 小时前
JAVA面试宝典 -《缓存架构:穿透 / 雪崩 / 击穿解决方案》
java·缓存·面试
小白的程序空间1 小时前
Anaconda Prompt中删除库虚拟环境【保姆级教程】
linux·开发语言·python
tanyongxi661 小时前
从零手写红黑树(C++实现详解)
开发语言·数据结构·c++·算法
EulerBlind1 小时前
【工具】Pycharm隐藏侧边灯泡提示
ide·python·pycharm
basketball6161 小时前
Linux C 信号操作
linux·c语言·开发语言
Kiri霧1 小时前
Kotlin比较接口
android·java·前端·微信·kotlin
阿华的代码王国2 小时前
【Android】EditText使用和监听
android·xml·java