在 Spring Boot 中实现 WebSockets

什么是 WebSockets?

WebSockets 是一种基于 TCP 的全双工通信协议,允许客户端和服务器之间建立持久的双向连接,用于实时数据交换。相较于传统的 HTTP 请求-响应模型,WebSockets 提供了低延迟、高效率的通信方式,特别适合需要实时更新的应用场景,如聊天应用、实时通知、在线游戏和股票价格更新。

核心特点
  1. 全双工通信:客户端和服务器可同时发送和接收数据。
  2. 持久连接:一次握手后,连接保持开放,减少重复建立连接的开销。
  3. 低延迟:适合实时性要求高的场景。
  4. 轻量协议 :基于 HTTP 协议升级(通过 Upgrade 头),数据帧开销小。
  5. 跨平台支持:现代浏览器和服务器均支持 WebSockets。
工作原理
  1. 握手 :客户端通过 HTTP 发送 WebSocket 握手请求(GET /ws HTTP/1.1Upgrade: websocket 头),服务器响应 101 Switching Protocols
  2. 连接建立 :双方建立 WebSocket 连接,使用 ws://wss://(加密)协议。
  3. 数据交换:通过文本或二进制帧传输数据,连接保持开放直到一方关闭。
  4. 关闭:发送关闭帧,断开连接。
与 HTTP 和 AJAX 的对比
  • HTTP:单向、请求-响应模式,适合静态内容。
  • AJAX:通过轮询模拟实时性,增加服务器负载。
  • WebSockets:持久连接,低延迟,适合动态交互。
应用场景
  • 实时聊天(如 WhatsApp)。
  • 实时通知(如新消息提醒)。
  • 在线协作工具(如 Google Docs)。
  • 金融数据流(如股票价格)。
  • 多人游戏。
挑战
  • 资源消耗:持久连接占用服务器资源。
  • 复杂性:需要处理连接断开、重连等。
  • 安全性:需防止未授权访问(参考你的 Spring Security 查询)。
  • 集成:需与分页、Swagger、ActiveMQ、Spring Profiles、Spring Batch、FreeMarker、热加载、ThreadLocal、Actuator 安全性、CSRF、异常处理等协调。

Spring Boot 通过 Spring WebSocket 和 STOMP(Simple Text Oriented Messaging Protocol)简化 WebSocket 实现。以下是在 Spring Boot 中实现 WebSockets 的步骤,结合你的先前查询(分页、Swagger、ActiveMQ、Spring Profiles、Spring Security、Spring Batch、FreeMarker、热加载、ThreadLocal、Actuator 安全性、CSRF、异常处理)。

1. 环境搭建
  1. 添加依赖pom.xml):

    xml 复制代码
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-activemq</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.2.0</version>
    </dependency>
  2. 配置 application.yml

    yaml 复制代码
    spring:
      profiles:
        active: dev
      freemarker:
        template-loader-path: classpath:/templates/
        suffix: .ftl
        cache: false
      activemq:
        broker-url: tcp://localhost:61616
        user: admin
        password: admin
    server:
      port: 8081
    springdoc:
      api-docs:
        path: /api-docs
      swagger-ui:
        path: /swagger-ui.html
2. 实现 WebSocket 聊天应用

以下是一个简单的实时聊天应用示例,使用 STOMP over WebSocket。

  1. WebSocket 配置

    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
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
            config.enableSimpleBroker("/topic"); // 广播消息
            config.setApplicationDestinationPrefixes("/app"); // 客户端发送消息前缀
        }
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/chat").withSockJS(); // WebSocket 端点,兼容 SockJS
        }
    }
  2. 消息控制器

    java 复制代码
    package com.example.demo.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("/sendMessage")
        @SendTo("/topic/messages")
        public ChatMessage sendMessage(ChatMessage message) {
            return message; // 广播消息
        }
    }
  3. 消息实体

    java 复制代码
    package com.example.demo.controller;
    
    public class ChatMessage {
        private String content;
        private String sender;
    
        // Getters and Setters
        public String getContent() { return content; }
        public void setContent(String content) { this.content = content; }
        public String getSender() { return sender; }
        public void setSender(String sender) { this.sender = sender; }
    }
  4. FreeMarker 聊天页面src/main/resources/templates/chat.ftl):

    ftl 复制代码
    <!DOCTYPE html>
    <html>
    <head>
        <title>实时聊天</title>
        <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>
            var stompClient = null;
            function connect() {
                var socket = new SockJS('/chat');
                stompClient = Stomp.over(socket);
                stompClient.connect({}, function(frame) {
                    stompClient.subscribe('/topic/messages', function(message) {
                        var msg = JSON.parse(message.body);
                        document.getElementById('messages').innerHTML += 
                            '<p>' + msg.sender + ': ' + msg.content + '</p>';
                    });
                });
            }
            function sendMessage() {
                var content = document.getElementById('content').value;
                var sender = document.getElementById('sender').value;
                stompClient.send('/app/sendMessage', {}, JSON.stringify({
                    'content': content,
                    'sender': sender
                }));
                document.getElementById('content').value = '';
            }
            window.onload = connect;
        </script>
    </head>
    <body>
        <h1>实时聊天</h1>
        <div>
            <label>用户名: <input id="sender" type="text" value="User"/></label>
            <label>消息: <input id="content" type="text"/></label>
            <button onclick="sendMessage()">发送</button>
        </div>
        <div id="messages"></div>
    </body>
    </html>
  5. 控制器

    java 复制代码
    package com.example.demo.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @Controller
    public class WebSocketController {
        @GetMapping("/chat")
        public String chat() {
            return "chat";
        }
    }
  6. 运行验证

    • 启动应用:mvn spring-boot:run
    • 访问 http://localhost:8081/chat,打开多个浏览器窗口。
    • 输入用户名和消息,发送后所有窗口实时显示消息。
3. 与先前查询集成

结合你的查询(分页、Swagger、ActiveMQ、Spring Profiles、Spring Security、Spring Batch、FreeMarker、热加载、ThreadLocal、Actuator 安全性、CSRF、异常处理):

  1. 分页与排序

    • 实时显示分页用户数据:

      java 复制代码
      @MessageMapping("/fetchUsers")
      @SendTo("/topic/users")
      public Page<User> fetchUsers(@Payload UserFilter filter) {
          return userService.searchUsers(filter.getName(), filter.getPage(), filter.getSize(), "id", "asc");
      }
      java 复制代码
      public class UserFilter {
          private String name;
          private int page;
          private int size;
          // Getters and Setters
      }
  2. Swagger

    • 文档化 REST API,非 WebSocket:

      java 复制代码
      @Operation(summary = "获取用户列表")
      @GetMapping("/api/users")
      public Page<User> getUsers(@RequestParam String name, @RequestParam int page, @RequestParam int size) {
          return userService.searchUsers(name, page, size, "id", "asc");
      }
  3. ActiveMQ

    • 记录聊天消息:

      java 复制代码
      @Controller
      public class ChatController {
          @Autowired
          private JmsTemplate jmsTemplate;
      
          @MessageMapping("/sendMessage")
          @SendTo("/topic/messages")
          public ChatMessage sendMessage(ChatMessage message) {
              jmsTemplate.convertAndSend("chat-log", message.getSender() + ": " + message.getContent());
              return message;
          }
      }
  4. Spring Profiles

    • 配置开发/生产环境:

      yaml 复制代码
      # application-dev.yml
      spring:
        freemarker:
          cache: false
      logging:
        level:
          root: DEBUG
      yaml 复制代码
      # application-prod.yml
      spring:
        freemarker:
          cache: true
  5. Spring Security

    • 保护 WebSocket 连接:

      java 复制代码
      @Configuration
      public class SecurityConfig {
          @Bean
          public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
              http
                  .authorizeHttpRequests(auth -> auth
                      .requestMatchers("/chat").authenticated()
                      .anyRequest().permitAll()
                  )
                  .formLogin()
                  .and()
                  .csrf().ignoringRequestMatchers("/chat"); // WebSocket 端点禁用 CSRF
              return http.build();
          }
      }
  6. Spring Batch

    • 批量推送用户数据:

      java 复制代码
      @Component
      public class BatchConfig {
          @Bean
          public Step pushUsers(@Autowired SimpMessagingTemplate messagingTemplate) {
              return stepBuilderFactory.get("pushUsers")
                      .<User, User>chunk(10)
                      .reader(reader())
                      .processor(user -> {
                          messagingTemplate.convertAndSend("/topic/users", user);
                          return user;
                      })
                      .writer(writer())
                      .build();
          }
      }
  7. FreeMarker

    • 已使用 FreeMarker 渲染聊天页面。
  8. 热加载

    • 启用 DevTools:

      yaml 复制代码
      spring:
        devtools:
          restart:
            enabled: true
  9. ThreadLocal

    • 清理 WebSocket 上下文:

      java 复制代码
      @Service
      public class UserService {
          private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
          public Page<User> searchUsers(...) {
              try {
                  CONTEXT.set("WS-" + Thread.currentThread().getName());
                  // 逻辑
              } finally {
                  CONTEXT.remove();
              }
          }
      }
  10. Actuator 安全性

    • 保护 /actuator/**,允许 /actuator/health
  11. CSRF

    • WebSocket 端点禁用 CSRF(见 SecurityConfig)。
  12. 异常处理

    • 处理 WebSocket 异常:

      java 复制代码
      @ControllerAdvice
      public class WebSocketExceptionHandler {
          @ExceptionHandler(MessagingException.class)
          public void handleMessagingException(MessagingException ex, SimpMessageHeaderAccessor headerAccessor) {
              headerAccessor.getSessionAttributes().put("error", ex.getMessage());
          }
      }
4. 运行验证
  • 开发环境

    bash 复制代码
    java -jar demo.jar --spring.profiles.active=dev
    • 访问 http://localhost:8081/chat,登录后发送消息,验证实时性。
    • 检查 ActiveMQ chat-log 队列。
  • 生产环境

    bash 复制代码
    java -jar demo.jar --spring.profiles.active=prod
    • 确认安全性和模板缓存。

原理与性能

原理
  • WebSocket 协议:基于 TCP,使用 HTTP 握手升级。
  • STOMP:在 WebSocket 上添加消息路由,支持订阅/发布。
  • Spring WebSocket:管理连接、消息路由和广播。
性能
  • 连接建立:50ms(单用户)。
  • 消息传输:1-2ms/消息。
  • 并发:1000 用户,延迟 <10ms(8 核 CPU,16GB 内存)。
测试
java 复制代码
@Test
public void testWebSocketPerformance() {
    WebSocketClient client = new StandardWebSocketClient();
    client.doHandshake(new TextWebSocketHandler(), "ws://localhost:8081/chat");
    // 测试消息发送
}

常见问题

  1. 连接失败

    • 问题:WebSocket 握手失败。
    • 解决:检查端点路径,禁用防火墙。
  2. 消息丢失

    • 问题:客户端未收到消息。
    • 解决:确认订阅 /topic/messages
  3. ThreadLocal 泄漏

    • 问题:/actuator/threaddump 显示泄漏。
    • 解决:清理 ThreadLocal。

总结

WebSockets 提供实时双向通信,Spring Boot 通过 STOMP 简化实现。示例展示了聊天应用及与分页、Swagger、ActiveMQ 等集成。针对你的查询(ThreadLocal、Actuator、热加载、CSRF),通过清理、Security 和 DevTools 解决。

相关推荐
A阳俊yi10 分钟前
Spring Boot日志配置
java·spring boot·后端
苹果酱056711 分钟前
2020-06-23 暑期学习日更计划(机器学习入门之路(资源汇总)+概率论)
java·vue.js·spring boot·mysql·课程设计
起风了布布16 分钟前
配置版本化是怎么实现的
后端
资深前端外卖员16 分钟前
【nodejs高可用】前端APM应用监控方案 + 落地
前端·后端
健康的猪20 分钟前
golang的cgo的一点小心得
开发语言·后端·golang
斜月22 分钟前
一个服务预约系统该如何设计?
spring boot·后端
M1A131 分钟前
云原生第一步:Windows Go环境极速配置
后端·go
异常君1 小时前
Java 高并发编程:等值判断的隐患与如何精确控制线程状态
java·后端·代码规范
异常君1 小时前
Java 日期处理:SimpleDateFormat 线程安全问题及解决方案
java·后端·代码规范
Java水解1 小时前
Mysql之存储过程
后端·mysql