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

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

一、前置技术

  • 项目介绍

    • 项目为局域网沟通软件,类似内网通,核心功能包括昵称输入、聊天界面展示在线人数(实时更新)、群聊,也可扩展私聊、登录注册、聊天记录存储等功能,结尾附GitHub链接。
  • 项目涉及技术

    • 包括GUI界面编程、网络通信、面向对象编程,以及字符串处理、时间获取等相关API。
  • 时间获取方案之JDK8之前的Date API

    • 通过创建Date对象获取此刻时间,但其格式为美式且不直观,需用SimpleDateFormat进行格式化。
    // 创建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。
    // 获取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类型。
    // 创建StringBuilder对象
    StringBuilder sb = new StringBuilder();
    // 拼接字符串
    sb.append("张三").append("李四").append("王五");
    // 转为String类型
    String result = sb.toString();
    
  • 解决浮点型运算失真的BigDecimal

    • 用于解决小数运算结果失真问题,需通过字符串构造器或valueOf()方法(内部使用字符串构造器)创建对象,提供加减乘除等方法,除法时若结果除不尽需指定保留位数和舍入模式(如四舍五入)。
    // 创建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”。
    • 创建服务端启动类,负责启动服务端并等待客户端连接
    // 服务端启动类
    public class Server {public static void main(String[] args) {try {// 注册端口,端口从常量类获取ServerSocket serverSocket = new ServerSocket(Constant.PORT);System.out.println("服务端启动成功,等待客户端连接...");while (true) {// 等待客户端连接,获取管道Socket socket = serverSocket.accept();System.out.println("一个客户端连接成功!");// 将管道交给独立线程处理new ServerReaderThread(socket).start();// 将管道暂存(后续需结合登录消息存储昵称)// 此处仅为示意,实际需在接收登录消息后完善onlineSockets.put(socket, "未知用户");}} catch (IOException e) {e.printStackTrace();}}// 定义Map集合存储在线客户端管道及对应昵称public static Map<Socket, String> onlineSockets = new HashMap<>();
    }
    
    • 定义常量类存储端口信息
    // 常量类
    public class Constant {public static final int PORT = 6666; // 服务端端口
    }
    
    • 创建线程类处理客户端管道通信
    // 线程类处理客户端消息
    public class ServerReaderThread extends Thread {private Socket socket;public ServerReaderThread(Socket socket) {this.socket = socket;}@Overridepublic void run() {// 后续将实现接收登录消息、群聊消息等逻辑System.out.println("线程开始处理客户端:" + socket);}
    }
    

四、服务端(在线人数模块)

  • 内容承接:已完成服务端基础开发,包括创建项目、接收客户端Socket管道并交由独立线程处理,同时准备了Map集合(onlineSockets)用于存储在线客户端的Socket及对应昵称(Socket为键,昵称为值)。

  • 服务端接收消息的类型及处理思路

    • 消息类型:登录消息、群聊消息、私聊消息、图片消息等。
    • 协议设计:客户端需先发送消息类型编号(如1代表登录、2代表群聊、3代表私聊),服务端通过编号区分处理。
    • 核心逻辑:服务端从Socket输入流读取类型编号,通过switch分支判断并执行对应逻辑。
    // 服务端接收消息类型的核心逻辑
    DataInputStream dis = new DataInputStream(socket.getInputStream());
    int type = dis.readInt(); // 读取消息类型编号
    switch (type) {case 1: // 处理登录消息break;case 2: // 处理群聊消息break;// 其他消息类型...
    }
    
  • 服务端接收登录消息的处理

    • 读取昵称:当消息类型为1时,通过输入流读取客户端发送的昵称。
    • 存储在线信息:将当前Socket和昵称存入onlineSockets集合,标记客户端上线。
    // 处理登录消息
    String nickname = dis.readUTF(); // 读取昵称
    Server.onlineSockets.put(socket, nickname); // 存入在线集合
    
  • 更新全部客户端在线人数列表的方法

    • 方法功能:向所有在线客户端推送最新的在线用户列表。
    • 实现步骤:
      1. 获取所有在线用户的昵称(onlineSocketsvalues)。
      2. 遍历所有在线Socket,通过输出流向每个客户端发送更新消息:
        • 先发送消息类型(1代表在线列表更新)。
        • 发送用户数量,再逐个发送用户名。
    // 更新在线人数列表的方法
    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中移除,并触发在线列表更新。
    // 客户端下线时移除在线记录
    Server.onlineSockets.remove(socket);
    updateClientOnlineList(); // 重新更新在线列表
    
  • 整体流程

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

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

  • 解决下线操作的bug
    下线时需更新所有客户端的在线人数列表,需重新调用更新在线人数的方法。此时map集合中已移除下线客户端信息,遍历剩余socket推送更新后的列表(消息类型为1号)。

    // 下线时调用更新在线人数列表的方法
    updateOnlineUserList();// 更新在线人数列表的方法逻辑
    private void updateOnlineUserList() {// 获取当前在线用户列表(已移除下线用户)Collection<String> usernames = onlineSockets.values();// 遍历所有在线socket,推送更新后的列表for (Socket socket : onlineSockets.keySet()) {DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(1); // 1号消息:更新在线人数dos.writeUTF(String.join(",", usernames));dos.flush();}
    }
    
  • 接收群聊消息并转发的整体逻辑
    服务端线程接收2号类型的群聊消息后,需转发给所有在线socket(包括发送者自身),确保消息在所有客户端面板展示。

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

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

    1. 获取发送者昵称:通过当前socketmap集合中获取对应的用户名
      String senderName = Server.onlineSockets.get(socket); // onlineSockets为<Socket, String>类型的map
      
    2. 获取并格式化时间:使用LocalDateTimeDateTimeFormatter处理时间
      LocalDateTime now = LocalDateTime.now();
      DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
      String timeStr = dtf.format(now); // 格式化时间为字符串
      
    3. 拼接消息:使用StringBuilder组合昵称、时间和消息内容,添加格式符优化展示
      StringBuilder sb = new StringBuilder();
      sb.append(senderName) // 发送者昵称.append(" ") .append(timeStr) // 发送时间.append("\r\n") // 换行.append(message) // 消息内容.append("\r\n"); // 消息间换行
      String fullMessage = sb.toString(); // 转换为字符串
      
  • 转发消息给所有在线客户端
    遍历所有在线socket,发送拼装好的消息(消息类型为2号):

    private void sendMsgToAll(String fullMessage) {for (Socket socket : Server.onlineSockets.keySet()) {try {DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(2); // 2号消息:群聊消息dos.writeUTF(fullMessage);dos.flush(); // 刷新输出流} catch (IOException e) {e.printStackTrace();}}
    }
    
  • 添加死循环处理多次消息
    线程需通过死循环持续接收消息,避免只处理一次后终止:

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

六、客户端(登录开发)

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

  • 登录界面的初始操作

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

    • 将登录相关代码独立为login方法,避免代码臃肿:
    private void login(String nickname) throws IOException {// 连接服务端socket = new Socket(Constant.SERVER_IP, Constant.SERVER_PORT);// 发送登录信息DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(1); // 消息类型为登录(1代表登录)dos.writeUTF(nickname); // 发送昵称dos.flush(); // 刷新缓冲区// 登录成功后进入聊天界面new ClientChatFrame(nickname, socket);
    }
    
    • 服务端的IP和端口在常量类Constant中定义,方便后续修改:
    public class Constant {public static final String SERVER_IP = "127.0.0.1"; // 服务器IPpublic static final int SERVER_PORT = 666; // 服务器端口,需与服务端保持一致
    }
    
  • 发送登录消息给服务端:连接成功后,通过DataOutputStream向服务端发送消息类型(1代表登录)和昵称,且不能关闭流和管道,否则会中断后续通信。

  • 进入聊天界面的准备:登录成功后,启动聊天界面(ClientChatFrame类)。需将昵称和Socket管道传给聊天界面,因此在登录界面将Socket定义为全局变量:

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

    • 聊天界面通过有参构造器接收昵称和Socket管道,并调用无参构造器初始化界面:
    public class ClientChatFrame extends JFrame {private String nickname;private Socket socket;public ClientChatFrame(String nickname, Socket socket) {this(); // 调用无参构造器初始化界面this.nickname = nickname;this.socket = socket;setTitle(nickname + "的聊天窗口"); // 在窗口标题展示昵称}public ClientChatFrame() {// 初始化界面组件的代码initComponents();}
    }
    
    • 聊天界面将昵称展示在窗口标题上,方便用户识别当前登录账号,同时保存Socket管道用于后续接收在线人数列表、发送和接收消息等操作。

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

  • 回顾登录界面跳转逻辑:登录成功后,将昵称和Socket管道传递给聊天界面,并销毁登录窗口,避免资源占用。关键代码逻辑如下(示意):
// 登录成功后跳转至聊天界面
ChatFrame chatFrame = new ChatFrame(nickname, socket);
chatFrame.setVisible(true);
this.dispose(); // 销毁当前登录窗口
  • 明确客户端核心任务:登录后需实时读取服务端发送的两类消息:

    • 在线人数更新消息(类型1)
    • 群聊消息(类型2)
  • 采用多线程处理消息收发

    • 独立线程(ClientReaderThread)负责持续接收服务端消息,避免阻塞主线程
    • 主线程负责处理用户交互(如发送消息)
  • 创建客户端消息读取线程类

public class ClientReaderThread extends Thread {private Socket socket;private ChatFrame chatFrame; // 持有聊天界面对象public ClientReaderThread(Socket socket, ChatFrame chatFrame) {this.socket = socket;this.chatFrame = chatFrame;}@Overridepublic void run() {try (DataInputStream dis = new DataInputStream(socket.getInputStream())) {while (true) {int type = dis.readInt(); // 读取消息类型if (type == 1) {// 处理在线人数更新updateClientOnlineUserList(dis);} else if (type == 2) {// 处理群聊消息(下节课实现)}}} catch (IOException e) {e.printStackTrace();}}
}
  • 在线人数更新方法实现
private void updateClientOnlineUserList(DataInputStream dis) throws IOException {int count = dis.readInt(); // 读取在线用户数量String[] onlineUsers = new String[count];// 循环读取所有在线用户名for (int i = 0; i < count; i++) {onlineUsers[i] = dis.readUTF();}// 调用聊天界面方法更新UIchatFrame.updateOnlineUsers(onlineUsers);
}
  • 聊天界面更新UI组件
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和界面对象:
// 聊天界面构造方法中启动读取线程
public ChatFrame(String nickname, Socket socket) {this.nickname = nickname;this.socket = socket;// 启动消息读取线程new ClientReaderThread(socket, this).start();
}
  • 功能测试验证
    • 启动服务端后,多客户端登录测试在线人数同步展示
    • 关闭任一客户端,验证其他客户端在线列表实时移除该用户

八、客户端(群聊功能)

  • 接收群聊消息逻辑

    • 接收消息类型为2的群聊消息,通过输入流读取服务端发送的UTF格式消息(包含发送者、时间等信息)。
    • 将消息展示到界面面板,核心代码如下:
    // 读取群聊消息
    String message = dis.readUTF();
    // 将消息更新到窗口
    win.setMessageToWindow(message);
    
    • 在窗口类中实现setMessageToWindow方法,将消息追加到展示区域:
    public void setMessageToWindow(String message) {msgArea.append(message);
    }
    
  • 发送群聊消息功能

    • 为发送按钮绑定点击事件,获取输入框内容并清空,通过输出流向服务端发送消息。
    • 先发送消息类型2,再发送具体的群聊内容,核心代码如下:
    // 为发送按钮绑定点击事件
    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),连接到同一服务端进行多人测试。
    • 可优化的方向包括:消息自动滚动到底部、完善换行显示、支持发送图片、美化界面等。
GitHub:https://github.com/Andy123211/chat-system/tree/master
http://www.dtcms.com/a/286189.html

相关文章:

  • http性能测试命令ab
  • IntelliJ IDEA大括号格式设置:换行改行尾
  • Java 核心工具类 API 详解(一):从 Math 到 Runtime 的实用指南
  • 【AI News | 20250717】每日AI进展
  • 【解码文本世界的“隐形分界线”:Windows与Linux回车换行之谜】
  • 基于单片机智能充电器系统设计
  • 如何检查GitHub上可能潜在的信息泄漏
  • 深入理解 Redis 集群化看门狗机制:原理、实践与风险
  • synchronized锁升级过程【AI笔记,仅供自己参考】
  • Pythonday17
  • 中国1km逐月潜在蒸散发数据集 - matlab按shp批量裁剪
  • lesson17:Python函数之递归、匿名函数与变量作用域
  • 电脑装机软件一键安装管理器
  • sky-take-out项目Mybatis的使用
  • MyBatis 动态 SQL:让 SQL 语句随条件灵活变化
  • Java面试宝典:Maven
  • UE5多人MOBA+GAS 番外篇:使用ECC(UGameplayEffectExecutionCalculation)制作伤害计算的流程
  • 【Java新特性】Java 17 新特性全解析
  • 嵌入式Linux:什么是线程?
  • Docker搭建Elasticsearch和Kibana
  • 图机器学习(12)——社区检测
  • 飞牛上使用Docker方式部署LibreTV,再配合内网穿透,实现免费无广告刷剧的服务教程
  • Oracle ADG 一键自动化搭建脚本
  • 【宇树科技:未来1-3年,机器人可流水线打螺丝】
  • Go语言实战案例-模拟登录验证(用户名密码)
  • 什么是高光谱相机,它与数码相机有什么区别?
  • C#引用转换核心原理:类型视角切换
  • 弧焊机器人智能节气装置
  • Android 开机流程中的图片与动画解析
  • leetcode:冗余连接 II[并查集检查环][节点入度]