使用 Spring Boot 通过 STOMP 实现 WebSocket
已发表: 2022-03-11WebSocket 协议是让您的应用程序处理实时消息的方法之一。 最常见的替代方案是长轮询和服务器发送事件。 这些解决方案中的每一个都有其优点和缺点。 在本文中,我将向您展示如何使用 Spring Boot 框架实现 WebSockets。 我将介绍服务器端和客户端设置,我们将使用 STOMP over WebSocket 协议相互通信。
服务器端将完全用 Java 编码。 但是,在客户端的情况下,我将展示用 Java 和 JavaScript (SockJS) 编写的代码片段,因为通常 WebSockets 客户端嵌入在前端应用程序中。 代码示例将演示如何使用 pub-sub 模型向多个用户广播消息,以及如何仅向单个用户发送消息。 在本文的另一部分,我将简要讨论保护 WebSocket 的安全,以及我们如何确保我们的基于 WebSocket 的解决方案即使在环境不支持 WebSocket 协议的情况下也能保持运行。
请注意,保护 WebSockets 的主题在这里只会简单地涉及,因为它是一个足够复杂的主题,可以单独写一篇文章。 因此,以及我在生产中的 WebSocket 中涉及的其他几个因素? 最后的部分,我建议在生产中使用此设置之前进行修改,请阅读直到最后,以获取具有安全措施的生产就绪设置。
WebSocket 和 STOMP 协议
WebSocket 协议允许您在应用程序之间实现双向通信。 重要的是要知道 HTTP 仅用于初始握手。 发生这种情况后,HTTP 连接将升级为 WebSocket 使用的新打开的 TCP/IP 连接。
WebSocket 协议是一个相当低级的协议。 它定义了如何将字节流转换为帧。 框架可能包含文本或二进制消息。 因为消息本身并没有提供任何关于如何路由或处理它的额外信息,所以如果不编写额外的代码就很难实现更复杂的应用程序。 幸运的是,WebSocket 规范允许使用在更高的应用程序级别上运行的子协议。 Spring Framework 支持的其中之一是 STOMP。
STOMP 是一个简单的基于文本的消息传递协议,最初是为 Ruby、Python 和 Perl 等脚本语言创建的,用于连接到企业消息代理。 多亏了 STOMP,以不同语言开发的客户端和代理可以相互发送和接收消息。 WebSocket 协议有时称为 Web 的 TCP。 类似地,STOMP 被称为 Web 的 HTTP。 它定义了一些映射到 WebSockets 帧的帧类型,例如CONNECT
、 SUBSCRIBE
、 UNSUBSCRIBE
、 ACK
或SEND
。 一方面,这些命令对于管理通信非常方便,另一方面,它们允许我们实现具有更复杂功能的解决方案,例如消息确认。
服务器端:Spring Boot 和 WebSockets
为了构建 WebSocket 服务器端,我们将使用 Spring Boot 框架,它显着加快了 Java 中独立和 Web 应用程序的开发速度。 Spring Boot 包含spring-WebSocket
模块,该模块与 Java WebSocket API 标准(JSR-356)兼容。
使用 Spring Boot 实现 WebSocket 服务器端并不是一项非常复杂的任务,并且只包含几个步骤,我们将一一进行。
步骤 1.首先,我们需要添加 WebSocket 库依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
如果您计划使用 JSON 格式传输消息,您可能还需要包含 GSON 或 Jackson 依赖项。 很可能,您可能还需要一个安全框架,例如 Spring Security。
第 2 步。然后,我们可以配置 Spring 以启用 WebSocket 和 STOMP 消息传递。
Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mywebsockets") .setAllowedOrigins("mydomain.com").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config){ config.enableSimpleBroker("/topic/", "/queue/"); config.setApplicationDestinationPrefixes("/app"); } }
configureMessageBroker
方法做了两件事:
- 创建具有一个或多个目标的内存消息代理,用于发送和接收消息。 在上面的示例中,定义了两个目标前缀:
topic
和queue
。 它们遵循这样的约定,即通过 pub-sub 模型向所有订阅客户端发送消息的目的地应以topic
为前缀。 另一方面,私人消息的目的地通常以queue
为前缀。 - 定义前缀
app
用于过滤由使用@MessageMapping
注释的方法处理的目标,您将在控制器中实现这些目标。 控制器处理完消息后,会将其发送给代理。
回到上面的代码片段——可能你已经注意到了对withSockJS()
方法的调用——它启用了 SockJS 回退选项。 简而言之,即使 Internet 浏览器不支持 WebSocket 协议,它也会让我们的 WebSocket 工作。 我将更详细地讨论这个主题。
还有一件事需要澄清——为什么我们在端点上调用setAllowedOrigins()
方法。 它通常是必需的,因为 WebSocket 和 SockJS 的默认行为是只接受同源请求。 因此,如果您的客户端和服务器端使用不同的域,则需要调用此方法以允许它们之间的通信。
步骤 3 。 实现一个控制器来处理用户请求。 它将向订阅给定主题的所有用户广播收到的消息。
这是一个将消息发送到目标/topic/news
的示例方法。
@MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; }
除了注释@SendTo
,您还可以使用SimpMessagingTemplate
,您可以在控制器内部自动装配。
@MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) }
在后面的步骤中,您可能希望添加一些额外的类来保护您的端点,例如 Spring Security 框架中的ResourceServerConfigurerAdapter
或WebSecurityConfigurerAdapter
。 此外,实现消息模型通常是有益的,以便可以将传输的 JSON 映射到对象。

构建 WebSocket 客户端
实现客户端是一项更简单的任务。
步骤 1. 自动装配 Spring STOMP 客户端。
@Autowired private WebSocketStompClient stompClient;
步骤 2. 打开一个连接。
StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();
一旦完成,就可以向目的地发送消息。 该消息将发送给订阅了某个主题的所有用户。
stompSession.send("topic/greetings", "Hello new user");
也可以订阅消息。
session.subscribe("topic/greetings", this); @Override public void handleFrame(StompHeaders headers, Object payload) { Message msg = (Message) payload; logger.info("Received : " + msg.getText()+ " from : " + msg.getFrom()); }
有时只需要向专用用户发送消息(例如在实现聊天时)。 然后,客户端和服务器端必须使用专用于此私人对话的单独目的地。 可以通过将唯一标识符附加到通用目的地名称(例如, /queue/chat-user123
)来创建目的地的名称。 HTTP 会话或 STOMP 会话标识符可用于此目的。
Spring 使发送私人消息变得更加容易。 我们只需要用@SendToUser
注释控制器的方法。 然后,这个目的地将由UserDestinationMessageHandler
处理,它依赖于会话标识符。 在客户端,当客户端订阅以/user
为前缀的目的地时,该目的地将转换为该用户唯一的目的地。 在服务器端,根据用户的Principal
解析用户目的地。
带有@SendToUser
注释的示例服务器端代码:
@MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; }
或者您可以使用SimpMessagingTemplate
:
String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);
现在让我们看看如何实现一个 JavaScript (SockJS) 客户端,该客户端能够接收可以由上面示例中的 Java 代码发送的私人消息。 值得知道的是,WebSockets 是 HTML5 规范的一部分,并且受到大多数现代浏览器的支持(Internet Explorer 从版本 10 开始支持它们)。
function connect() { var socket = new SockJS('/greetings'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { stompClient.subscribe('/user/queue/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).name); }); }); } function sendName() { stompClient.send("/app/greetings", {}, $("#name").val()); }
您可能已经注意到,要接收私人消息,客户端需要订阅以/user
为前缀的通用目的地/queue/greetings
。 它不必为任何唯一标识符而烦恼。 但是客户端需要先登录到应用程序,所以初始化了服务端的Principal
对象。
保护 WebSockets
许多 Web 应用程序使用基于 cookie 的身份验证。 例如,我们可以使用 Spring Security 来限制对某些页面或控制器的访问仅限于登录用户。 然后通过基于 cookie 的 HTTP 会话维护用户安全上下文,该会话稍后与为该用户创建的 WebSocket 或 SockJS 会话相关联。 WebSockets 端点可以像任何其他请求一样受到保护,例如在 Spring 的WebSecurityConfigurerAdapter
中。
如今,Web 应用程序通常使用 REST API 作为其后端,并使用 OAuth/JWT 令牌进行用户身份验证和授权。 WebSocket 协议没有描述服务器在 HTTP 握手期间如何验证客户端。 在实践中,标准 HTTP 标头(例如,授权)用于此目的。 不幸的是,并非所有 STOMP 客户端都支持它。 Spring Java 的 STOMP 客户端允许为握手设置标头:
WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);
但是 SockJS JavaScript 客户端不支持通过 SockJS 请求发送授权标头。 但是,它允许发送可用于传递令牌的查询参数。 这种方法需要在服务器端编写自定义代码,该代码将从查询参数中读取令牌并对其进行验证。 确保令牌不与请求一起记录(或日志受到良好保护)也很重要,因为这可能会导致严重的安全违规。
SockJS 后备选项
与 WebSocket 的集成可能并不总是顺利进行。 一些浏览器(例如,IE 9)不支持 WebSockets。 更重要的是,限制性代理可能会导致无法执行 HTTP 升级,或者它们会切断打开时间过长的连接。 在这种情况下,SockJS 来救援。
SockJS 传输分为三大类:WebSockets、HTTP 流和 HTTP 长轮询。 通信从 SockJS 发送GET /info
以从服务器获取基本信息开始。 根据响应,SockJS 决定要使用的传输。 第一个选择是 WebSockets。 如果它们不受支持,则尽可能使用流式传输。 如果此选项也不可能,则选择轮询作为传输方法。
生产中的 WebSocket?
尽管此设置有效,但它并不是“最好的”。 Spring Boot 允许您使用任何具有 STOMP 协议的成熟消息系统(例如,ActiveMQ、RabbitMQ),并且外部代理可能支持比我们使用的简单代理更多的 STOMP 操作(例如,确认、收据)。 STOMP Over WebSocket提供了有关 WebSockets 和 STOMP 协议的有趣信息。 它列出了处理 STOMP 协议的消息传递系统,并且可能是在生产中使用的更好的解决方案。 特别是如果由于大量请求,消息代理需要集群。 (Spring 的简单消息代理不适合集群。)然后,不需要在WebSocketConfig
中启用简单代理,而是需要启用 Stomp 代理中继,该中继将消息转发到外部消息代理和从外部消息代理转发。 总而言之,外部消息代理可以帮助您构建更具可扩展性和健壮性的解决方案。
如果您已准备好继续探索 Spring Boot 的 Java 开发人员之旅,请尝试阅读使用 Spring Boot 进行 OAuth2 和 JWT REST 保护接下来。