MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Spring Boot整合WebSocket实现实时通信教程

2021-08-077.2k 阅读

一、WebSocket 简介

  1. 传统 HTTP 协议的局限性 在理解 WebSocket 之前,我们先来回顾一下传统的 HTTP 协议。HTTP 协议是一种无状态的请求 - 响应协议,每次客户端向服务器发起请求,服务器处理请求并返回响应后,连接就会关闭。这意味着如果客户端想要实时获取服务器端的更新,就需要不断地发起新的请求,这种方式被称为轮询(Polling)。轮询存在一些明显的问题,比如会增加服务器的负担,因为每次请求都会消耗服务器资源;同时,由于轮询的时间间隔难以精准把握,如果间隔时间短,会造成过多的无效请求,如果间隔时间长,又会导致实时性较差。

  2. WebSocket 诞生的背景与优势 为了解决传统 HTTP 协议在实时通信方面的不足,WebSocket 应运而生。WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它使得客户端和服务器之间可以进行实时、双向的通信。WebSocket 协议的握手阶段是基于 HTTP 协议的,但是一旦握手成功,就会建立起一个持久的连接,双方可以随时发送和接收数据,大大提高了实时性,并且减少了不必要的请求开销,降低了服务器的负载。

  3. WebSocket 协议原理 WebSocket 的通信过程主要分为握手和数据传输两个阶段。在握手阶段,客户端通过 HTTP 协议向服务器发送一个特殊的请求,请求头中包含了一些关于 WebSocket 的信息,例如 Upgrade: websocket 表示这是一个 WebSocket 升级请求,Connection: Upgrade 用于告知服务器进行协议升级。服务器如果支持 WebSocket 协议,会返回一个状态码为 101 的响应,表示协议切换成功。握手成功后,客户端和服务器之间就建立了一个基于 TCP 的全双工通信通道,双方可以通过这个通道自由地发送和接收数据。

二、Spring Boot 基础

  1. Spring Boot 概述 Spring Boot 是由 Pivotal 团队提供的全新框架,它基于 Spring 框架,旨在简化 Spring 应用的初始搭建以及开发过程。Spring Boot 采用了约定优于配置(Convention over Configuration)的理念,使得开发者可以快速地创建出生产级别的 Spring 应用,而无需进行大量繁琐的配置。它内置了对各种常用技术的支持,包括数据库连接、Web 开发等,大大提高了开发效率。

  2. Spring Boot 项目结构 一个典型的 Spring Boot 项目结构如下:

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           └── myproject/
│   │               ├── MyprojectApplication.java
│   │               ├── controller/
│   │               │   └── ExampleController.java
│   │               ├── service/
│   │               │   └── ExampleService.java
│   │               └── repository/
│   │                   └── ExampleRepository.java
│   └── resources/
│       ├── application.properties
│       ├── static/
│       └── templates/
└── test/
    └── java/
        └── com/
            └── example/
                └── myproject/
                    └── MyprojectApplicationTests.java
  • src/main/java 目录存放项目的 Java 源代码,其中 MyprojectApplication.java 是 Spring Boot 应用的启动类。
  • src/main/resources 目录存放应用的配置文件、静态资源和模板文件。application.properties 是 Spring Boot 的主要配置文件,用于配置应用的各种属性。
  • src/test/java 目录存放项目的测试代码,MyprojectApplicationTests.java 是对 Spring Boot 应用进行测试的类。
  1. Spring Boot 核心注解
  • @SpringBootApplication:这是 Spring Boot 应用的核心注解,它组合了 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan 三个注解。@SpringBootConfiguration 用于标识该类是一个 Spring Boot 配置类;@EnableAutoConfiguration 开启自动配置功能,Spring Boot 会根据项目中引入的依赖自动配置相关的组件;@ComponentScan 用于扫描指定包及其子包下的所有组件,并将它们注册到 Spring 容器中。
  • @Controller:用于标识一个控制器类,该类处理来自客户端的 HTTP 请求。通常与 @RequestMapping 等注解配合使用。
  • @Service:用于标识一个服务类,通常用于封装业务逻辑。服务类通常被注入到控制器或其他服务类中。
  • @Repository:用于标识一个数据访问层类,通常用于与数据库进行交互。Spring Data JPA 等框架会自动识别带有该注解的类,并为其生成实现。

三、Spring Boot 整合 WebSocket 前的准备

  1. 创建 Spring Boot 项目 我们可以通过 Spring Initializr(https://start.spring.io/)来快速创建一个 Spring Boot 项目。在 Spring Initializr 页面,选择项目的构建工具(如 Maven 或 Gradle),项目的语言(Java),Spring Boot 的版本等信息。在依赖项中,添加 Spring WebSpring WebSocket 依赖。

对于 Maven 项目,添加的依赖如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
</dependencies>

对于 Gradle 项目,添加的依赖如下:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

创建好项目后,导入到 IDE(如 IntelliJ IDEA 或 Eclipse)中。

  1. 配置 Spring Boot 项目application.properties 文件中,可以对项目进行一些基本的配置。例如,可以配置服务器的端口号:
server.port=8080

如果项目需要访问静态资源,可以配置静态资源的路径:

spring.resources.static-locations=classpath:/static/

这些配置可以根据项目的实际需求进行调整。

四、Spring Boot 整合 WebSocket 实现实时通信

  1. 配置 WebSocket 在 Spring Boot 中,我们通过创建一个配置类来配置 WebSocket。创建一个名为 WebSocketConfig 的类,并使其继承自 WebSocketConfigurer 接口,重写 registerWebSocketHandlers 方法。
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 registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket-endpoint").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.setUserDestinationPrefix("/user");
        config.enableSimpleBroker("/topic", "/queue");
    }
}
  • @EnableWebSocketMessageBroker 注解启用了使用 STOMP 协议的 WebSocket 消息代理功能。
  • registerStompEndpoints 方法用于注册 STOMP 端点。addEndpoint("/websocket - endpoint") 定义了 WebSocket 的端点路径为 /websocket - endpoint.withSockJS() 表示启用 SockJS 协议,SockJS 是一个 WebSocket 模拟层,它可以在不支持 WebSocket 的浏览器中使用其他传输方式(如 XHR Polling、JSONP Polling 等)来模拟 WebSocket 的功能,从而提供统一的 WebSocket 编程模型。
  • configureMessageBroker 方法用于配置消息代理。setApplicationDestinationPrefixes("/app") 表示以 /app 为前缀的消息将被发送到带有 @MessageMapping 注解的方法;setUserDestinationPrefix("/user") 定义了用户特定目的地的前缀;enableSimpleBroker("/topic", "/queue") 启用了一个简单的消息代理,用于处理以 /topic/queue 为前缀的消息,/topic 通常用于广播消息,多个客户端都可以订阅并接收消息,/queue 通常用于点对点消息,只有特定的客户端可以接收消息。
  1. 创建 WebSocket 消息处理类 创建一个消息处理类,用于处理客户端发送过来的消息,并向客户端发送响应消息。例如,创建一个名为 WebSocketController 的类:
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class WebSocketController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public String handleHelloMessage(String message) {
        return "Hello, " + message + "!";
    }
}
  • @Controller 注解将该类标识为一个 Spring 控制器。
  • @MessageMapping("/hello") 注解类似于 @RequestMapping,用于映射客户端发送到 /app/hello 的消息(因为在配置中设置了 /app 为应用目的地前缀)。当客户端发送消息到 /app/hello 时,handleHelloMessage 方法会被调用。
  • @SendTo("/topic/greetings") 注解表示将方法的返回值发送到 /topic/greetings 这个目的地,所有订阅了 /topic/greetings 的客户端都可以接收到该消息。在这个例子中,当客户端发送一个消息到 /app/hello,服务器处理后会将带有问候语的消息发送到 /topic/greetings,所有订阅了该主题的客户端都能收到。
  1. 前端页面实现 WebSocket 连接与通信 创建一个 HTML 页面来实现与后端 WebSocket 的连接和通信。在 src/main/resources/static 目录下创建一个 index.html 文件:
<!DOCTYPE html>
<html lang="zh - CN">
<head>
    <meta charset="UTF - 8">
    <title>WebSocket 示例</title>
    <script src="https://cdn.jsdelivr.net/npm/sockjs - client@1/dist/sockjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
</head>
<body>
    <h1>WebSocket 实时通信示例</h1>
    <input type="text" id="messageInput" placeholder="输入消息">
    <button onclick="sendMessage()">发送消息</button>
    <div id="messageOutput"></div>

    <script>
        var socket = new SockJS('/websocket - endpoint');
        var stompClient = Stomp.over(socket);

        stompClient.connect({}, function (frame) {
            console.log('Connected: ', frame);
            stompClient.subscribe('/topic/greetings', function (greeting) {
                document.getElementById('messageOutput').innerHTML += '<p>' + greeting.body + '</p>';
            });
        });

        function sendMessage() {
            var message = document.getElementById('messageInput').value;
            stompClient.send('/app/hello', {}, message);
            document.getElementById('messageInput').value = '';
        }
    </script>
</body>
</html>
  • 引入了 SockJS 和 Stomp.js 的 JavaScript 库,用于在浏览器中实现与后端 WebSocket 的通信。
  • var socket = new SockJS('/websocket - endpoint'); 创建了一个 SockJS 连接,连接到后端的 /websocket - endpoint 端点。
  • var stompClient = Stomp.over(socket); 使用 Stomp.js 对 SockJS 连接进行封装,以支持 STOMP 协议。
  • stompClient.connect({}, function(frame) {... }) 建立连接,连接成功后,通过 stompClient.subscribe('/topic/greetings', function(greeting) {... }) 订阅 /topic/greetings 主题,当有消息发送到该主题时,会将消息显示在页面上。
  • sendMessage 函数获取输入框中的消息,并通过 stompClient.send('/app/hello', {}, message); 将消息发送到 /app/hello 目的地。
  1. 运行与测试 启动 Spring Boot 应用,然后在浏览器中打开 index.html 页面。在输入框中输入消息并点击发送按钮,服务器会处理该消息,并将带有问候语的消息广播到所有订阅了 /topic/greetings 主题的客户端,包括当前发送消息的客户端,页面上会显示接收到的消息,从而实现了实时通信。

五、深入理解 Spring Boot 与 WebSocket 的通信机制

  1. STOMP 协议详解 STOMP(Simple Text - Oriented Messaging Protocol)是一种简单的、文本导向的消息协议,它被广泛应用于 WebSocket 通信中。STOMP 协议定义了一系列的命令和帧结构,用于在客户端和服务器之间进行消息的发送和接收。

STOMP 帧由三个部分组成:命令(Command)、头部(Headers)和体(Body)。常见的 STOMP 命令有 CONNECTSENDSUBSCRIBEUNSUBSCRIBE 等。例如,CONNECT 命令用于客户端连接到服务器,SEND 命令用于发送消息,SUBSCRIBE 命令用于订阅主题。

头部部分包含了一些元数据,例如目的地(destination)、消息的类型等信息。体部分则是消息的具体内容。

在 Spring Boot 整合 WebSocket 中,STOMP 协议起到了关键的作用。客户端通过发送符合 STOMP 协议的帧来与服务器进行交互,服务器也按照 STOMP 协议的规则来处理和响应这些帧。

  1. 消息代理的工作原理 消息代理在 Spring Boot 的 WebSocket 通信中扮演着重要的角色。消息代理负责接收客户端发送的消息,并将其路由到合适的目的地。

当客户端发送一个消息到服务器时,服务器首先根据消息的目的地前缀(如 /app)来确定是否需要将消息发送到带有 @MessageMapping 注解的方法进行处理。如果需要,服务器会调用相应的方法,并将处理结果根据 @SendTo 等注解指定的目的地发送给消息代理。

消息代理会根据目的地的类型(如 /topic/queue)来决定如何处理消息。对于 /topic 类型的目的地,消息代理会将消息广播给所有订阅了该主题的客户端;对于 /queue 类型的目的地,消息代理会将消息发送给特定的客户端。

在 Spring Boot 中,我们可以通过配置 configureMessageBroker 方法来定制消息代理的行为,例如设置应用目的地前缀、用户目的地前缀以及启用的消息代理类型等。

  1. 会话管理与安全机制 在 WebSocket 通信中,会话管理是非常重要的。Spring Boot 提供了对 WebSocket 会话的管理功能,通过 WebSocketSession 类可以获取会话的相关信息,如会话 ID、客户端的远程地址等。

在安全方面,Spring Boot 可以与 Spring Security 集成来保护 WebSocket 端点。例如,可以通过配置 Spring Security 来限制只有认证用户才能连接到 WebSocket 端点。可以在 Spring Security 的配置类中添加如下配置:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
           .authorizeRequests()
               .antMatchers("/websocket - endpoint/**").authenticated()
               .anyRequest().permitAll()
               .and()
           .formLogin();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        UserDetails user =
            User.withDefaultPasswordEncoder()
               .username("user")
               .password("password")
               .roles("USER")
               .build();

        return new InMemoryUserDetailsManager(user);
    }
}

上述配置表示只有认证用户才能访问 /websocket - endpoint 及其子路径,其他请求则允许所有人访问。同时,配置了一个基于内存的用户认证,用户名是 user,密码是 password

六、优化与扩展 Spring Boot WebSocket 应用

  1. 性能优化
  • 减少消息序列化开销:在 WebSocket 通信中,消息的序列化和反序列化会消耗一定的性能。可以选择高效的序列化方式,如 JSON 序列化。Spring Boot 默认支持 JSON 序列化,并且可以通过配置来优化 JSON 序列化的性能。例如,可以使用 Jackson 库的一些高级特性,如自定义序列化器和反序列化器,来减少不必要的序列化字段,提高序列化和反序列化的速度。
  • 合理设置线程池:Spring Boot 的 WebSocket 处理是基于线程池的。可以根据服务器的硬件资源和预计的并发量来合理设置线程池的参数,如核心线程数、最大线程数、队列容量等。如果线程池设置过小,可能会导致请求处理不及时;如果设置过大,又会浪费系统资源。可以通过在配置类中自定义 TaskExecutor 来设置线程池参数:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.concurrent.Executor;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(100);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("WebSocket - Thread - ");
        executor.initialize();
        return executor;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new TextWebSocketHandler(), "/websocket - endpoint").setAllowedOrigins("*");
    }
}
  • 缓存常用数据:如果在 WebSocket 处理过程中需要频繁访问一些不变的数据,可以将这些数据缓存起来。例如,可以使用 Spring Cache 来缓存数据库查询结果,减少数据库的访问次数,提高响应速度。
  1. 功能扩展
  • 集群支持:当应用需要处理大量并发连接时,可能需要将 WebSocket 服务扩展到多个服务器节点,形成集群。Spring Boot 可以与一些分布式消息队列(如 RabbitMQ、Kafka 等)集成,通过消息队列来实现不同节点之间的消息同步。例如,使用 RabbitMQ 作为消息代理,不同节点的 WebSocket 服务可以通过 RabbitMQ 来交换消息,从而实现集群环境下的实时通信。
  • 与其他系统集成:可以将 Spring Boot WebSocket 应用与其他系统进行集成,如与监控系统集成,实时获取系统的运行状态信息并展示给用户;与日志系统集成,实时推送日志信息等。可以通过调用其他系统提供的 API 或者使用消息队列来实现与其他系统的交互。
  • 支持多种消息类型:除了简单的文本消息,还可以扩展支持多种消息类型,如 JSON 格式的复杂对象消息、二进制消息等。可以通过自定义消息编码器和解码器来实现对不同消息类型的处理。例如,创建一个自定义的 TextWebSocketHandler 子类,并重写 handleTextMessage 方法来处理不同类型的消息:
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;

public class CustomWebSocketHandler extends TextWebSocketHandler {

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
        // 解析消息类型,根据不同类型进行处理
        String payload = message.getPayload();
        if (payload.startsWith("{") && payload.endsWith("}")) {
            // 处理 JSON 格式消息
        } else {
            // 处理普通文本消息
        }
        session.sendMessage(new TextMessage("Message received: " + payload));
    }
}

七、常见问题与解决方法

  1. 连接问题
  • 问题描述:客户端无法连接到 WebSocket 端点,浏览器控制台显示连接失败的错误信息。
  • 解决方法:首先检查服务器是否正常启动,端口号是否正确。如果服务器启动正常,检查网络连接,确保客户端和服务器之间的网络畅通。另外,检查 WebSocket 配置是否正确,特别是端点路径和协议配置。如果使用了 SockJS,检查 SockJS 的相关配置,确保客户端和服务器的 SockJS 版本兼容。还需要注意跨域问题,如果客户端和服务器不在同一个域下,需要在服务器端配置允许跨域访问。可以在 WebSocket 配置类中添加如下配置:
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 registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket - endpoint").withSockJS().setAllowedOrigins("*");
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.setUserDestinationPrefix("/user");
        config.enableSimpleBroker("/topic", "/queue");
    }
}
  1. 消息发送与接收问题
  • 问题描述:客户端发送消息后,服务器没有收到;或者服务器发送消息后,客户端没有接收到。
  • 解决方法:检查消息的目的地是否正确,确保客户端发送的消息目的地与服务器端 @MessageMapping 注解映射的目的地一致,服务器端 @SendTo 注解指定的目的地与客户端订阅的目的地一致。检查消息的格式是否正确,如果使用了自定义的消息编码器和解码器,确保编码和解码过程没有错误。同时,检查日志信息,Spring Boot 会记录 WebSocket 相关的日志,可以通过查看日志来定位问题。例如,可以在 application.properties 文件中配置日志级别:
logging.level.org.springframework.web.socket=DEBUG
  1. 性能问题
  • 问题描述:在高并发情况下,WebSocket 应用出现响应缓慢、连接断开等性能问题。
  • 解决方法:按照前面提到的性能优化方法进行调整,如合理设置线程池参数、优化消息序列化方式、缓存常用数据等。同时,监控服务器的资源使用情况,如 CPU、内存、网络带宽等,找出性能瓶颈。如果是因为网络带宽不足导致的问题,可以考虑升级网络带宽或者采用负载均衡等技术来分摊网络流量。

通过以上步骤和方法,我们可以在 Spring Boot 项目中成功整合 WebSocket 实现实时通信,并对其进行优化和扩展,同时解决常见的问题。希望这篇教程能帮助你在后端开发中更好地运用 Spring Boot 和 WebSocket 技术。