杰瑞科技汇

Java WebSocket聊天如何实现实时消息推送?

  1. 核心概念简介:什么是 WebSocket,为什么用它?
  2. 项目环境搭建:使用 Maven 和一个简单的 Servlet 容器(如 Tomcat)。
  3. 后端实现:创建 WebSocket 端点、处理消息和广播。
  4. 前端实现:创建一个简单的 HTML 页面来连接和收发消息。
  5. 运行与测试:启动应用并体验聊天功能。
  6. 进阶与扩展:讨论如何让应用更完善。

核心概念简介

HTTP vs. WebSocket

Java WebSocket聊天如何实现实时消息推送?-图1
(图片来源网络,侵删)
  • HTTP (请求/响应模式):客户端主动发起请求,服务器响应,服务器无法主动向客户端推送信息,想象一下你每隔几秒就问一次服务器“有新消息吗?”,这很浪费资源。
  • WebSocket (全双工通信):客户端和服务器之间建立一个长连接,一旦连接建立,双方都可以主动向对方发送消息,这就像一个电话,连接后双方可以随时说话,非常适合实时聊天、在线游戏、股票行情等场景。

WebSocket 端点 在 Java 中,我们使用一个特殊的类来处理 WebSocket 连接,这个类被称为“端点”(Endpoint),它使用注解来声明其功能,

  • @ServerEndpoint:将一个类标记为 WebSocket 端点,并指定连接的 URL。
  • @OnOpen:当一个新的 WebSocket 连接建立时,此方法被调用。
  • @OnClose:当 WebSocket 连接关闭时,此方法被调用。
  • @OnMessage:当从客户端接收到消息时,此方法被调用。
  • @OnError:当连接过程中发生错误时,此方法被调用。

项目环境搭建

我们将使用 Maven 来管理项目依赖。

创建 Maven 项目

在你的 IDE(如 IntelliJ IDEA 或 Eclipse)中,创建一个新的 Maven 项目。

pom.xml 配置

我们需要添加 Jakarta WebSocket API 和一个 Servlet 容器(如 Tomcat)的依赖,对于开发,我们可以使用 maven-war-plugin 来打包项目。

Java WebSocket聊天如何实现实时消息推送?-图2
(图片来源网络,侵删)
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>java-websocket-chat</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!-- Jakarta WebSocket API -->
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>9.1.0</version>
            <scope>provided</scope> <!-- 由 Servlet 容器(如 Tomcat)提供 -->
        </dependency>
    </dependencies>
    <build>
        <finalName>java-websocket-chat</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.2</version>
            </plugin>
            <!-- 用于在 IDE 中方便运行 Tomcat 的插件 -->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <path>/</path>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

注意:如果你使用的是 Java 8 和旧版的 Java EE (javax),依赖会是 javax.websocket:javax.websocket-api,这里我们使用较新的 Jakarta EE 9 规范。


后端实现

后端的核心是 WebSocket 端点,它负责管理所有连接和消息的广播。

创建 ChatEndpoint.java

src/main/java 下创建你的包,com.example.websocket,然后在该包下创建 ChatEndpoint.java

package com.example.websocket;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
 * WebSocket 聊天端点
 * @ServerEndpoint 注解将此类声明为 WebSocket 端点。
 * value 属性定义了 WebSocket 连接的 URL。
 *  ws://your-domain.com:8080/java-websocket-chat/chat
 */
@ServerEndpoint("/chat")
public class ChatEndpoint {
    // 使用一个静态的 Set 来存储所有活跃的会话。
    // 必须是线程安全的,因为多个线程(来自不同连接)可能会同时访问它。
    private static final Set<Session> chatroomUsers = Collections.synchronizedSet(new HashSet<>());
    /**
     * 当一个新的 WebSocket 连接打开时调用。
     * @param session 新建立的会话
     */
    @OnOpen
    public void onOpen(Session session) {
        // 将新会话添加到用户集合中
        chatroomUsers.add(session);
        System.out.println("新连接已建立: " + session.getId());
        // 广播一条新用户加入的消息
        broadcast("用户 [" + session.getId() + "] 加入了聊天室。");
    }
    /**
     * 当客户端发送消息时调用。
     * @param message 客户端发送的消息
     * @param session 发送消息的会话
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("来自 " + session.getId() + " 的消息: " + message);
        // 广播收到的消息给所有用户
        broadcast("用户 [" + session.getId() + "]: " + message);
    }
    /**
     * 当 WebSocket 连接关闭时调用。
     * @param session 关闭的会话
     * @param closeReason 关闭原因
     */
    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        // 从用户集合中移除该会话
        chatroomUsers.remove(session);
        System.out.println("连接已关闭: " + session.getId() + ", 原因: " + closeReason.getReasonPhrase());
        // 广播一条用户离开的消息
        broadcast("用户 [" + session.getId() + "] 离开了聊天室。");
    }
    /**
     * 当通信过程中发生错误时调用。
     * @param session 出错的会话
     * @param throwable 异常对象
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.err.println("连接出错: " + session.getId());
        throwable.printStackTrace();
    }
    /**
     * 广播消息给所有连接的客户端。
     * @param message 要广播的消息
     */
    private void broadcast(String message) {
        // 遍历所有会话并发送消息
        chatroomUsers.forEach(session -> {
            try {
                // getBasicRemote() 用于同步发送消息
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                // 如果发送失败,很可能是因为客户端已经断开连接,我们将其从集合中移除
                System.err.println("向会话 " + session.getId() + " 发送消息失败: " + e.getMessage());
                chatroomUsers.remove(session);
            }
        });
    }
}

前端实现

前端只需要一个 HTML 文件和一些 JavaScript 代码来连接 WebSocket 并处理用户交互。

Java WebSocket聊天如何实现实时消息推送?-图3
(图片来源网络,侵删)

src/main/webapp 目录下创建 index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">Java WebSocket 聊天室</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #chat-box {
            border: 1px solid #ccc;
            padding: 10px;
            height: 400px;
            overflow-y: scroll;
            margin-bottom: 10px;
        }
        #chat-log {
            white-space: pre-wrap; /* 保留换行符 */
            word-wrap: break-word; /* 长单词自动换行 */
        }
        #input-box {
            display: flex;
        }
        #message-input {
            flex-grow: 1;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        #send-button {
            padding: 8px 15px;
            margin-left: 10px;
            border: none;
            background-color: #007bff;
            color: white;
            border-radius: 4px;
            cursor: pointer;
        }
        #send-button:hover {
            background-color: #0056b3;
        }
    </style>
</head>
<body>
    <h1>Java WebSocket 聊天室</h1>
    <div id="chat-box">
        <div id="chat-log"></div>
    </div>
    <div id="input-box">
        <input type="text" id="message-input" placeholder="输入消息..." onkeypress="if(event.key==='Enter') sendMessage()">
        <button id="send-button" onclick="sendMessage()">发送</button>
    </div>
    <script>
        // 获取 DOM 元素
        const chatLog = document.getElementById('chat-log');
        const messageInput = document.getElementById('message-input');
        // 获取当前页面的协议和主机名,构建 WebSocket URL
        // 注意:这里的路径需要与后端 @ServerEndpoint 的 value 属性匹配
        const wsUrl = `ws://${window.location.host}/java-websocket-chat/chat`;
        // 创建 WebSocket 对象
        const socket = new WebSocket(wsUrl);
        // 连接建立时的回调
        socket.onopen = function(event) {
            console.log("WebSocket 连接已建立!");
            appendLog("系统: 你已成功连接到聊天室。");
        };
        // 接收到服务器消息时的回调
        socket.onmessage = function(event) {
            console.log("收到服务器消息: " + event.data);
            appendLog(event.data);
        };
        // 连接关闭时的回调
        socket.onclose = function(event) {
            if (event.wasClean) {
                console.log(`连接已正常关闭,代码=${event.code},原因=${event.reason}`);
            } else {
                console.error('连接被意外中断');
            }
            appendLog("系统: 与聊天室的连接已断开。");
        };
        // 发生错误时的回调
        socket.onerror = function(error) {
            console.error("WebSocket 错误: " + error);
            appendLog("系统: 发生错误,请检查连接。");
        };
        // 发送消息的函数
        function sendMessage() {
            const message = messageInput.value.trim();
            if (message) {
                socket.send(message); // 通过 WebSocket 发送消息
                messageInput.value = ''; // 清空输入框
            }
        }
        // 将消息追加到聊天日志的函数
        function appendLog(message) {
            chatLog.textContent += message + '\n';
            // 滚动到底部
            chatLog.scrollTop = chatLog.scrollHeight;
        }
    </script>
</body>
</html>

运行与测试

  1. 打包项目:在项目根目录下运行 Maven 命令 mvn clean package,这会在 target 目录下生成一个 java-websocket-chat.war 文件。
  2. 部署到 Tomcat
    • 方式一(推荐开发):如果你配置了 tomcat7-maven-plugin,可以直接运行 mvn tomcat7:run,它会自动启动一个嵌入式的 Tomcat 服务器并部署你的应用。
    • 方式二:将生成的 java-websocket-chat.war 文件复制到你安装的 Tomcat 的 webapps 目录下,然后启动 Tomcat。
  3. 打开浏览器
    • 打开两个或多个浏览器窗口(或标签页)。
    • 在地址栏输入 http://localhost:8080/ (如果你使用了 tomcat7:runpath 配置为 ) 或者 http://localhost:8080/java-websocket-chat/ (如果你部署了 .war 文件)。
  4. 体验聊天
    • 在第一个窗口中输入消息并点击“发送”,你会看到消息出现在该窗口的聊天记录中。
    • 立刻在第二个窗口中输入消息并发送,你会看到它出现在所有窗口的聊天记录中。
    • 当你关闭其中一个窗口时,其他窗口会立即收到“用户 [xxx] 离开了聊天室”的通知。

进阶与扩展

这个简单的聊天室已经具备了基本功能,但一个生产级的应用还需要更多:

  1. 用户身份认证

    • 问题:目前用户只是一个匿名的 session.getId()
    • 方案:在 @OnOpen 时,可以从 HTTP Session 或 Token 中获取用户信息(如用户名),并将其与 WebSocket Session 关联起来,你需要一个 ConcurrentHashMap<Session, User> 来存储这种映射关系,广播消息时,使用用户名而不是 session.getId()
  2. 聊天室/频道

    • 问题:所有用户都在一个公共聊天室。
    • 方案:修改 WebSocket 端点的 URL,使其包含房间 ID,@ServerEndpoint("/chat/{roomId}"),在 onOpen 方法中,可以从路径参数中获取 roomId,并将用户添加到对应房间的 Set 中,广播时,只向特定房间的用户发送消息。
  3. 消息持久化

    • 问题:用户刷新页面后,之前的聊天记录就丢失了。
    • 方案:将聊天消息保存到数据库(如 MySQL, PostgreSQL)中,当用户连接时,可以从数据库中加载历史消息并显示。
  4. 安全性

    • 问题:WebSocket 连接容易被未授权的用户滥用。
    • 方案
      • HTTPS/WSS:在生产环境中,必须使用 wss:// (WebSocket Secure) 协议,它建立在 TLS/SSL 之上。
      • 鉴权:如第1点所述,在建立连接前验证用户身份。
  5. 心跳机制

    • 问题:如果客户端或服务器突然崩溃,对方可能无法及时感知连接已断开。
    • 方案:实现一个心跳机制,客户端定期向服务器发送一个“ping”消息,服务器收到后立即回复一个“pong”消息,如果客户端在一定时间内没有收到“pong”,就认为连接已断开。@OnMessage 方法需要能够区分心跳消息和普通聊天消息。

这个教程为你提供了一个坚实的基础,你可以基于此进行扩展,构建出功能更强大、更健壮的实时应用。

分享:
扫描分享到社交APP
上一篇
下一篇