杰瑞科技汇

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

  • 后端: Spring Boot + Spring WebSocket + SockJS (为了兼容性) + Stomp (用于消息路由)
  • 前端: HTML + JavaScript (使用 sockjs-clientstompjs 库)

项目结构

websocket-chat/
├── src/
│   ├── main/
│   │   ├── java/com/example/demo/
│   │   │   ├── config/
│   │   │   │   └── WebSocketConfig.java      // WebSocket 配置类
│   │   │   ├── controller/
│   │   │   │   └── GreetingController.java   // 处理 HTTP 请求
│   │   │   ├── model/
│   │   │   │   └── ChatMessage.java          // 聊天消息的数据模型
│   │   │   └── WebSocketChatApplication.java // Spring Boot 启动类
│   │   └── resources/
│   │       ├── static/
│   │       │   └── index.html               // 前端聊天页面
│   │       └── templates/
│   └── test/
└── pom.xml                                   // Maven 依赖管理

第 1 步:创建 Spring Boot 项目并添加依赖

使用 Spring Initializr 创建项目,或手动添加以下依赖到你的 pom.xml 文件。

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

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.5</version> <!-- 使用一个较新的稳定版本 -->
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <!-- Spring WebSocket 和 Stomp 支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!-- Thymeleaf 模板引擎,用于渲染 HTML -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- 开发时使用,热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

第 2 步:后端实现

1. 数据模型

创建一个简单的 ChatMessage 类来表示聊天消息。

src/main/java/com/example/demo/model/ChatMessage.java

package com.example.demo.model;
public class ChatMessage {
    private String type; // 'JOIN', 'LEAVE', 'CHAT'
    private sender: String;
    private content: String;
    private timestamp: long;
    // Getters and Setters
    public String getType() { return type; }
    public void setType(String type) { this.type = type; }
    public String getSender() { return sender; }
    public void setSender(String sender) { this.sender = sender; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
    public long getTimestamp() { return timestamp; }
    public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}

2. WebSocket 配置

这是核心配置,它启用 STOMP 并注册一个端点。

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

src/main/java/com/example/demo/config/WebSocketConfig.java

package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker // 启用 WebSocket 消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 启用一个简单的内存消息代理,用于处理以 "/topic" 开头的目的地
        config.enableSimpleBroker("/topic");
        // 设置应用程序目的地前缀,带有 @MessageMapping 注解的方法的映射地址将以它开头
        config.setApplicationDestinationPrefixes("/app");
    }
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册一个 STOMP 端点,客户端将连接到这个端点
        // withSockJS() 允许在不支持 WebSocket 的浏览器上使用回退机制(如 long-polling)
        registry.addEndpoint("/ws").withSockJS();
    }
}

3. 消息控制器

这个控制器处理客户端发送的消息和订阅请求。

src/main/java/com/example/demo/controller/GreetingController.java

package com.example.demo.controller;
import com.example.demo.model.ChatMessage;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;
@Controller
public class GreetingController {
    // @MessageMapping: 映射到 "/app/chat.sendMessage" (因为我们在配置中设置了前缀 "/app")
    // @SendTo: 将方法的返回值发送给所有订阅了 "/topic/public" 的客户端
    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
        return chatMessage;
    }
    // @MessageMapping: 映射到 "/app/chat.addUser"
    // @SendTo: 将方法的返回值发送给所有订阅了 "/topic/public" 的客户端
    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public ChatMessage addUser(@Payload ChatMessage chatMessage,
                               SimpMessageHeaderAccessor headerAccessor) {
        // 添加用户到 WebSocket 会话中
        headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
        return chatMessage;
    }
}

4. HTTP 控制器 (可选,但推荐)

创建一个简单的控制器来提供聊天页面。

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

src/main/java/com/example/demo/controller/ChatController.java

package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ChatController {
    @GetMapping("/chat")
    public String chat() {
        return "index"; // 返回 Thymeleaf 模板 "index.html"
    }
}

第 3 步:前端实现

1. 添加前端库

在你的 index.html 中引入 sockjsstompjs 库,最简单的方式是使用 CDN。

src/main/resources/static/index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">Spring WebSocket Chat</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #chat-container { border: 1px solid #ccc; padding: 10px; height: 400px; overflow-y: scroll; }
        #message-form { margin-top: 10px; }
        #message-input { width: 70%; padding: 5px; }
        button { padding: 5px 10px; }
        .message { margin-bottom: 5px; }
        .system-message { color: #888; font-style: italic; }
    </style>
</head>
<body>
    <h2>Spring WebSocket Chat</h2>
    <div id="chat-container"></div>
    <form id="message-form">
        <input type="text" id="message-input" placeholder="Type a message..." />
        <button type="submit">Send</button>
    </form>
    <!-- 引入 SockJS 和 Stomp 客户端库 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
    <script th:src="@{/js/chat.js}"></script> <!-- 引入我们自己的 JS 文件 -->
</body>
</html>

2. 编写前端 JavaScript 逻辑

创建一个 js 目录和 chat.js 文件。

src/main/resources/static/js/chat.js

document.addEventListener('DOMContentLoaded', function() {
    const chatContainer = document.getElementById('chat-container');
    const messageForm = document.getElementById('message-form');
    const messageInput = document.getElementById('message-input');
    let stompClient = null;
    function connect() {
        // 1. 建立 SockJS 连接
        const socket = new SockJS('/ws');
        // 2. 使用 Stomp 协议 over SockJS
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            console.log('Connected: ' + frame);
            // 3. 订阅公共聊天频道
            stompClient.subscribe('/topic/public', function (message) {
                showMessage(JSON.parse(message.body));
            });
            // 发送加入消息
            const joinMessage = {
                sender: 'User', // 可以在这里动态获取用户名,例如从 prompt 输入
                type: 'JOIN',
                content: 'has joined the chat!',
                timestamp: new Date().getTime()
            };
            stompClient.send("/app/chat.addUser", {}, JSON.stringify(joinMessage));
        });
    }
    function disconnect() {
        if (stompClient !== null) {
            stompClient.disconnect();
        }
        console.log("Disconnected");
    }
    function sendMessage(event) {
        event.preventDefault(); // 阻止表单默认提交行为
        const messageContent = messageInput.value.trim();
        if (messageContent && stompClient) {
            const chatMessage = {
                sender: 'User', // 同上,可以动态设置
                type: 'CHAT',
                content: messageContent,
                timestamp: new Date().getTime()
            };
            // 发送消息到 /app/chat.sendMessage
            stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
            messageInput.value = ''; // 清空输入框
        }
    }
    function showMessage(message) {
        const messageElement = document.createElement('div');
        messageElement.classList.add('message');
        const timestamp = new Date(message.timestamp).toLocaleTimeString();
        if (message.type === 'JOIN' || message.type === 'LEAVE') {
            messageElement.classList.add('system-message');
            messageElement.textContent = `[${timestamp}] ${message.sender} ${message.content}`;
        } else {
            messageElement.textContent = `[${timestamp}] ${message.sender}: ${message.content}`;
        }
        chatContainer.appendChild(messageElement);
        chatContainer.scrollTop = chatContainer.scrollHeight; // 滚动到底部
    }
    // 事件监听
    messageForm.addEventListener('submit', sendMessage);
    // 连接 WebSocket
    connect();
});

第 4 步:运行和测试

  1. 运行后端: 在你的 IDE 中运行 WebSocketChatApplication.java,或者使用 Maven 命令:

    mvn spring-boot:run
  2. 打开前端: 在浏览器中访问 http://localhost:8080/chat

  3. 测试:

    • 打开两个或更多的浏览器窗口,都访问 http://localhost:8080/chat
    • 在任意一个窗口中输入消息并点击 "Send"。
    • 你应该能看到消息实时地出现在所有打开的窗口的聊天记录中。
    • 当页面加载时,会看到 "User has joined the chat!" 的系统消息,当关闭页面时,WebSocket 连接会断开,但为了更好的体验,你可以在 chat.jsdisconnect 函数中发送一个 "LEAVE" 消息。

总结与扩展

  • 用户名: 当前代码中硬编码了用户名为 "User",在实际应用中,你应该在用户登录后,将用户名存储在会话中,并在连接 WebSocket 时传递过去。
  • 房间/频道: 当前实现的是公共聊天室,你可以轻松扩展为私聊或多个房间。
    • 私聊: 在 ChatMessage 中增加 recipient 字段,在 GreetingController 中,使用 @SendToUser("/queue/private") 来发送消息给特定用户。
    • 房间: 在 ChatMessage 中增加 room 字段,在 GreetingController 中,将 @SendTo("/topic/public") 改为 @SendTo("/topic/" + chatMessage.getRoom())
  • 安全性: 考虑使用 Spring Security 来保护你的 WebSocket 端点,确保只有认证用户才能连接和发送消息。

这个例子为你提供了一个功能齐全的、可扩展的 Java WebSocket 聊天应用的基础,你可以基于此进行更多功能的开发。

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