杰瑞科技汇

java websocket 推送

  1. WebSocket 核心概念:为什么需要它,它解决了什么问题。
  2. Java WebSocket 推送的两种主流实现方式
    • Java-WebSocket (纯 Java 实现)
    • Spring Boot + STOMP (更现代、功能更强大的方式)
  3. 一个完整的实战案例:使用 Spring Boot + STOMP 实现一个简单的聊天室应用。
  4. 生产环境中的关键考量:心跳、安全、集群部署等。

WebSocket 核心概念

为什么需要 WebSocket?

传统的 HTTP 协议是“请求-响应”模式的,服务器不能主动向客户端推送数据,如果客户端需要实时获取服务器更新,通常只能采用以下两种方式:

java websocket 推送-图1
(图片来源网络,侵删)
  1. 轮询:客户端定时(比如每秒)向服务器发送 HTTP 请求,询问是否有新数据,这种方式效率低下,会产生大量无用的请求,增加服务器负担。
  2. 长轮询:客户端发送一个请求,服务器保持连接打开,直到有新数据可用或超时,这比轮询好,但连接仍然是半双工的,且服务器管理大量长连接也有性能开销。

WebSocket 如何解决?

WebSocket 在客户端和服务器之间建立一个持久的长连接,并且是全双工的(Full-Duplex),意味着双方可以同时发送和接收数据,一旦连接建立,服务器就可以随时向客户端推送消息,无需客户端先请求。

工作流程简述:

  1. 握手:客户端通过一个 HTTP 请求发起 WebSocket 握手,请求头中包含 Upgrade: websocketConnection: Upgrade
  2. 升级:服务器如果支持 WebSocket,会返回一个 101 Switching Protocols 响应,表示协议从 HTTP 升级到 WebSocket。
  3. 通信:之后,双方就可以在这个连接上自由地发送和接收消息,数据帧通常是轻量级的。

Java WebSocket 推送的两种主流实现方式

Java-WebSocket (纯 Java 实现)

这是一个轻量级的、不依赖任何框架的 Java WebSocket 库,适合学习 WebSocket 底层原理或构建简单的应用。

特点

java websocket 推送-图2
(图片来源网络,侵删)
  • 纯 Java 实现,无外部依赖(除了库本身)。
  • API 较为底层,需要手动处理连接、消息的接收和发送。
  • 代码量相对较多。

基本用法示例

  1. 添加依赖 (Maven):

    <dependency>
        <groupId>org.java-websocket</groupId>
        <artifactId>Java-WebSocket</artifactId>
        <version>1.5.3</version>
    </dependency>
  2. 创建 WebSocket 服务器:

    import org.java_websocket.server.WebSocketServer;
    import org.java_websocket.handshake.ClientHandshake;
    import org.java_websocket.WebSocket;
    import java.net.InetSocketAddress;
    public class MyWebSocketServer extends WebSocketServer {
        public MyWebSocketServer(int port) {
            super(new InetSocketAddress(port));
        }
        // 当有新的客户端连接时调用
        @Override
        public void onOpen(WebSocket conn, ClientHandshake handshake) {
            System.out.println("新连接已建立: " + conn.getRemoteSocketAddress());
            // 向新连接的客户端发送欢迎消息
            conn.send("欢迎连接到 WebSocket 服务器!");
        }
        // 当客户端断开连接时调用
        @Override
        public void onClose(WebSocket conn, int code, String reason, boolean remote) {
            System.out.println("连接已关闭: " + conn.getRemoteSocketAddress());
        }
        // 当收到客户端消息时调用
        @Override
        public void onMessage(WebSocket conn, String message) {
            System.out.println("收到来自 " + conn.getRemoteSocketAddress() + " 的消息: " + message);
            // 将收到的消息广播给所有连接的客户端
            this.broadcast("服务器收到消息: " + message);
        }
        // 当发生错误时调用
        @Override
        public void onError(WebSocket conn, Exception ex) {
            ex.printStackTrace();
        }
        // 启动服务器
        public static void main(String[] args) {
            int port = 8887;
            MyWebSocketServer server = new MyWebSocketServer(port);
            server.start();
            System.out.println("WebSocket 服务器已启动,监听端口: " + port);
        }
    }

Spring Boot + STOMP (推荐)

这是目前最主流、最推荐的方式,Spring 对 WebSocket 提供了非常完善的支持,并在此基础上引入了 STOMP 协议。

为什么推荐 Spring Boot + STOMP?

  1. STOMP 协议:WebSocket 本身只定义了数据帧格式,而 STOMP (Simple Text Oriented Messaging Protocol) 是一个简单的消息协议,它为 WebSocket 提供了消息语义,类似于 JMS 或 AMQP,这使得消息的路由、订阅和广播变得更加结构化和可控。
  2. 与 Spring 生态集成:完美集成 Spring MVC、Spring Security 等,你可以用熟悉的 @Controller@MessageMapping 等注解来处理 WebSocket 消息。
  3. 功能强大:支持点对点消息、广播消息、消息代理(如 RabbitMQ, ActiveMQ)等。
  4. 简化开发:大大减少了底层代码,让开发者可以更专注于业务逻辑。

核心组件

  • @ServerEndpoint (不推荐):Tomcat 自带的 WebSocket API,基于方法级别,与 Spring 整合不深。
  • WebSocketMessageBrokerConfigurer (推荐):Spring 的配置方式,基于 @Configuration,更符合 Spring 的编程模型。
  • @Controller + @MessageMapping:用于处理客户端发送过来的 STOMP 消息。
  • SimpMessagingTemplate:一个模板类,方便在服务端(非 WebSocket 处理方法中)主动向客户端推送消息。
  • @SendTo:注解在方法上,表示方法的返回值将自动发送到指定的目的地。

完整实战案例:Spring Boot + STOMP 聊天室

我们将创建一个简单的聊天应用,用户可以加入一个公共聊天室,并实时看到所有人的消息。

步骤 1:创建 Spring Boot 项目

Spring Initializr 中添加以下依赖:

  • Spring Web
  • Spring WebSocket
  • Spring Stomp (可选,但通常和 WebSocket 一起使用)
  • Lombok (可选,简化代码)

步骤 2:启用 WebSocket 和 STOMP

创建一个配置类来注册 STOMP 端点并启用消息代理。

// package com.example.websocketdemo.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");
        // 设置应用程序的目的地前缀,以 "/app" 开头的消息将被 @Controller 处理
        config.setApplicationDestinationPrefixes("/app");
    }
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册一个名为 "/chat" 的端点,并启用 SockJS
        // SockJS 是为了在不支持 WebSocket 的浏览器上提供类似 WebSocket 的体验
        registry.addEndpoint("/chat").withSockJS();
    }
}

步骤 3:创建消息处理控制器

这个控制器将处理客户端发来的消息,并将广播后的消息发送回 /topic/messages 主题。

// package com.example.websocketdemo.controller;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
    // @MessageMapping 映射到 "/app/chat.sendMessage" (因为前缀是 /app)
    // 这个方法会在收到客户端发送到这个地址的消息时被调用
    @MessageMapping("/sendMessage")
    // @SendTo 将方法的返回值自动发送到这个主题
    // 所有订阅了 "/topic/messages" 的客户端都会收到这条消息
    @SendTo("/topic/messages")
    public Message sendMessage(Message message) {
        // 这里可以添加业务逻辑,比如保存消息到数据库
        System.out.println("收到消息: " + message.getContent());
        return message; // 返回的消息将被广播
    }
}
// 消息实体类
class Message {
    private String from;
    private String content;
    private String type; // e.g., "JOIN", "LEAVE", "CHAT"
    // Getters and Setters (Lombok can be used here)
    public String getFrom() { return from; }
    public void setFrom(String from) { this.from = from; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
    public String getType() { return type; }
    public void setType(String type) { this.type = type; }
}

步骤 4:创建前端页面 (HTML + JavaScript)

src/main/resources/static 目录下创建 index.html

<!DOCTYPE html>
<html>
<head>Spring Boot WebSocket 聊天室</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #chat { border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: scroll; margin-bottom: 10px; }
        #message { width: 80%; padding: 5px; }
        #sendButton { width: 18%; padding: 5px; }
        .message { margin-bottom: 5px; }
    </style>
</head>
<body>
    <h2>WebSocket 聊天室</h2>
    <div id="chat"></div>
    <div>
        <input type="text" id="message" placeholder="输入消息...">
        <button id="sendButton">发送</button>
    </div>
    <script>
        document.addEventListener("DOMContentLoaded", function() {
            const chat = document.getElementById('chat');
            const messageInput = document.getElementById('message');
            const sendButton = document.getElementById('sendButton');
            // 1. 连接 SockJS
            const socket = new SockJS('/chat');
            // 2. 连接 STOMP 客户端
            const stompClient = Stomp.over(socket);
            // 3. 连接 WebSocket 服务器
            stompClient.connect({}, function (frame) {
                console.log('Connected: ' + frame);
                // 4. 订阅 /topic/messages 主题
                stompClient.subscribe('/topic/messages', function (message) {
                    // 收到消息后,将其显示在页面上
                    showMessage(JSON.parse(message.body));
                });
            });
            // 发送消息按钮点击事件
            sendButton.addEventListener('click', sendMessage);
            messageInput.addEventListener('keypress', function(event) {
                if (event.key === 'Enter') {
                    sendMessage();
                }
            });
            function sendMessage() {
                const content = messageInput.value.trim();
                if (content) {
                    // 构造消息对象
                    const message = {
                        from: 'User-' + Math.floor(Math.random() * 1000), // 模拟用户名
                        content: content,
                        type: 'CHAT'
                    };
                    // 5. 发送消息到 /app/sendDestination
                    stompClient.send("/app/sendMessage", {}, JSON.stringify(message));
                    messageInput.value = '';
                }
            }
            function showMessage(message) {
                const messageElement = document.createElement('div');
                messageElement.className = 'message';
                messageElement.textContent = `${message.from}: ${message.content}`;
                chat.appendChild(messageElement);
                chat.scrollTop = chat.scrollHeight;
            }
        });
    </script>
</body>
</html>

步骤 5:运行和测试

  1. 运行 Spring Boot 应用。
  2. 打开两个浏览器窗口,分别访问 http://localhost:8080
  3. 在一个窗口输入消息并发送,另一个窗口会实时看到这条消息。

生产环境中的关键考量

  1. 心跳检测:WebSocket 长连接可能会因为网络问题(如NAT超时)而断开,但客户端和服务器都不知道,STOMP 协议内置了心跳机制,需要在客户端和服务器配置中启用,以保持连接活性。

    • 客户端: stompClient.connect(headers, connectCallback, errorCallback, {'heart.beat': [10000, 10000]});
    • 服务器: 在 WebSocketMessageBrokerConfigurer 中配置 simpleBroker.setHeartbeatValue(new long[]{10000, 10000});
  2. 安全性

    • HTTPS:在生产环境中,WebSocket 连接必须使用 wss:// (WebSocket Secure),这要求你的网站也必须使用 https://
    • 认证授权:Spring Security 可以轻松集成到 WebSocket 中,你可以在握手阶段或连接建立后对用户进行身份验证,并使用 @PreAuthorize 等注解来控制用户是否有权订阅某个主题或发送消息。
  3. 集群部署:当你的应用部署在多台服务器上时,一个用户可能连接到服务器 A,而消息是由服务器 B 产生的,服务器 B 无法直接推送给连接在 A 上的用户,这时需要一个外部消息代理,如 RabbitMQ 或 ActiveMQ。

    • Spring 的 enableSimpleBroker 默认使用内存代理,不适用于集群。
    • 需要配置 MessageBrokerRegistry 使用代理,
      config.enableStompBrokerRelay("/topic", "/queue")
           .setRelayHost(rabbitmqHost)
           .setRelayPort(amqpPort)
           .setClientLogin(username)
           .setClientPass(password);
  4. 性能和监控

    • 监控活跃的 WebSocket 连接数、消息吞吐量等指标。
    • 考虑连接的并发限制,避免单个服务器资源耗尽。
    • 对于大量连接,注意 JVM 内存和 GC 压力。
特性 Java-WebSocket Spring Boot + STOMP
易用性 较低,API 底层 ,注解驱动,与 Spring 生态无缝集成
功能 基础的收发消息 强大,支持 STOMP 协议、消息代理、点对点/广播
依赖 仅 Java-WebSocket 库 需要 Spring Boot 全家桶
适用场景 学习、简单原型、轻量级应用 生产环境、复杂业务应用、企业级项目

对于绝大多数 Java 后端项目,强烈推荐使用 Spring Boot + STOMP 的方式来构建 WebSocket 应用,它能为你提供一个健壮、可扩展且易于维护的实时通信解决方案。

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