WebSocket Integration

Spring 会话提供与 Spring 的 WebSocket 支持的透明集成。

Spring Session provides transparent integration with Spring’s WebSocket support.

@19

Spring Session - WebSocket

Rob Winch

本指南描述如何使用 Spring 会话来确保 WebSocket 消息使您的 HttpSession 保持活动状态。

This guide describes how to use Spring Session to ensure that WebSocket messages keep your HttpSession alive.

Spring Session 的 WebSocket 支持仅适用于 Spring 的 WebSocket 支持。具体来说,它不适用于直接使用 JSR-356,因为 JSR-356 没有用于拦截传入 WebSocket 消息的机制。

Spring Session’s WebSocket support works only with Spring’s WebSocket support. Specifically,it does not work with using JSR-356 directly, because JSR-356 does not have a mechanism for intercepting incoming WebSocket messages.

HttpSession Setup

第一步是将 Spring Session 与 HttpSession 集成。这些步骤已在 HttpSession with Redis Guide 中概述。

The first step is to integrate Spring Session with the HttpSession. These steps are already outlined in the HttpSession with Redis Guide.

确保在继续之前,您已将 Spring 会话与 HttpSession 集成。

Please make sure you have already integrated Spring Session with HttpSession before proceeding.

Spring Configuration

在典型的 Spring WebSocket 应用程序中,您会实现 WebSocketMessageBrokerConfigurer。例如,配置可能类似于以下内容:

In a typical Spring WebSocket application, you would implement WebSocketMessageBrokerConfigurer. For example, the configuration might look something like the following:

/*
 * Copyright 2014-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs.websocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @author Rob Winch
 */
// tag::class[]
@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/messages").withSockJS();
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.enableSimpleBroker("/queue/", "/topic/");
		registry.setApplicationDestinationPrefixes("/app");
	}

}
// end::class[]

我们可以更新配置以使用 Spring 会话的 WebSocket 支持。以下示例演示如何操作:

We can update our configuration to use Spring Session’s WebSocket support. The following example shows how to do so:

src/main/java/samples/config/WebSocketConfig.java
Unresolved include directive in modules/ROOT/pages/guides/boot-websocket.adoc - include::example$spring-session-samples/spring-session-sample-boot-websocket/src/main/java/sample/config/WebSocketConfig.java[]

连接到 Spring 会话支持时,我们只需做两件事:

To hook in the Spring Session support we only need to change two things:

1 Instead of implementing WebSocketMessageBrokerConfigurer, we extend AbstractSessionWebSocketMessageBrokerConfigurer
2 We rename the registerStompEndpoints method to configureStompEndpoints

`AbstractSessionWebSocketMessageBrokerConfigurer`在幕后执行什么操作?

What does AbstractSessionWebSocketMessageBrokerConfigurer do behind the scenes?

  • WebSocketConnectHandlerDecoratorFactory is added as a WebSocketHandlerDecoratorFactory to WebSocketTransportRegistration. This ensures a custom SessionConnectEvent is fired that contains the WebSocketSession. The WebSocketSession is necessary to end any WebSocket connections that are still open when a Spring Session is ended.

  • SessionRepositoryMessageInterceptor is added as a HandshakeInterceptor to every StompWebSocketEndpointRegistration. This ensures that the Session is added to the WebSocket properties to enable updating the last accessed time.

  • SessionRepositoryMessageInterceptor is added as a ChannelInterceptor to our inbound ChannelRegistration. This ensures that every time an inbound message is received, that the last accessed time of our Spring Session is updated.

  • WebSocketRegistryListener is created as a Spring bean. This ensures that we have a mapping of all of the Session IDs to the corresponding WebSocket connections. By maintaining this mapping, we can close all the WebSocket connections when a Spring Session (HttpSession) is ended.

websocket Sample Application

`websocket`示例应用程序演示了如何在 WebSocket 中使用 Spring Session。

The websocket sample application demonstrates how to use Spring Session with WebSockets.

Running the websocket Sample Application

您可以获取 源代码 并调用以下命令运行示例:

You can run the sample by obtaining the source code and invoking the following command:

$ ./gradlew :spring-session-sample-boot-websocket:bootRun

出于测试会话到期时间的目的,你可能希望在启动应用程序之前添加以下配置属性,将会话到期时间更改为 1 分钟(默认值为 30 分钟):

For the purposes of testing session expiration, you may want to change the session expiration to be 1 minute (the default is 30 minutes) by adding the following configuration property before starting the application:

src/main/resources/application.properties
server.servlet.session.timeout=1m # Session timeout. If a duration suffix is not specified, seconds will be used.

要让示例发挥作用,你必须在 localhost 上 install Redis 2.8+ 并使用默认端口 (6379) 运行它。或者,你可以更新 RedisConnectionFactory 以指向 Redis 服务器。另一个选项是使用 Docker 在 localhost 上运行 Redis。有关详细说明,请参见 Docker Redis repository

For the sample to work, you must install Redis 2.8+ on localhost and run it with the default port (6379). Alternatively, you can update the RedisConnectionFactory to point to a Redis server. Another option is to use Docker to run Redis on localhost. See Docker Redis repository for detailed instructions.

您现在应该能够访问 [role="bare"][role="bare"]http://localhost:8080/ 中的应用程序。

You should now be able to access the application at [role="bare"]http://localhost:8080/

Exploring the websocket Sample Application

现在你可以尝试使用该应用程序。使用以下信息进行身份验证:

Now you can try using the application. Authenticate with the following information:

  • Username rob

  • Password password

现在,单击*Login*按钮。你现在应该以用户*rob*的身份进行身份验证。

Now click the Login button. You should now be authenticated as the user rob.

打开一个隐身窗口并访问 [role="bare"][role="bare"]http://localhost:8080/

Open an incognito window and access [role="bare"]http://localhost:8080/

系统会提示你提供一个登录表单。使用以下信息进行身份验证:

You are prompted with a login form. Authenticate with the following information:

  • Username luke

  • Password password

现在从 Rob 发送一条消息给 Luke。消息应该会显示出来。

Now send a message from rob to luke. The message should appear.

等待两分钟,然后再次尝试从 Rob 给 Luke 发送一条消息。你可以看到,消息不再发送。

Wait for two minutes and try sending a message from rob to luke again. You can see that the message is no longer sent.

Why two minutes?

Spring Session 在 60 秒后到期,但不能保证在 60 秒内收到来自 Redis 的通知。为了确保在合理的时间范围内关闭套接字,Spring Session 会在每分钟的第 00 秒运行一个后台任务,以强制清除所有过期的会话。这意味着你需要最多等待两分钟,WebSocket 连接才会关闭。

Spring Session expires in 60 seconds, but the notification from Redis is not guaranteed to happen within 60 seconds. To ensure the socket is closed in a reasonable amount of time, Spring Session runs a background task every minute at 00 seconds that forcibly cleans up any expired sessions. This means you need to wait at most two minutes before the WebSocket connection is closed.

You can now try accessing [role="bare"]http://localhost:8080/ You are prompted to authenticate again. This demonstrates that the session properly expires.

现在重复相同的练习,但是不要等待两分钟,而是在每 30 秒内从每个用户发送一条消息。您可以看到仍会继续发送消息。尝试访问 [role="bare"][role="bare"]http://localhost:8080/,您不会收到再次进行身份验证的提示。这表明会话保持活跃。

Now repeat the same exercise, but instead of waiting two minutes, send a message from each of the users every 30 seconds. You can see that the messages continue to be sent. Try accessing [role="bare"]http://localhost:8080/ You are not prompted to authenticate again. This demonstrates the session is kept alive.

只有来自用户的消息才能使会话保持活动状态。这是因为只有来自用户的消息才表示用户活动。收到的消息并不表示活动,因此不会续订会话过期。

Only messages sent from a user keep the session alive. This is because only messages coming from a user imply user activity. Received messages do not imply activity and, thus, do not renew the session expiration.

Why Spring Session and WebSockets?

那么当我们使用 WebSocket 时,为什么需要 Spring 会话?

So why do we need Spring Session when we use WebSockets?

请考虑一个通过 HTTP 请求执行大部分工作电子邮件应用程序。然而,它内部还有一个通过 WebSocket API 工作的聊天应用程序。如果用户正在积极地与某人聊天,我们不应该使 HttpSession 超时,因为这会是相当糟糕的用户体验。然而,这正是 JSR-356 所做的。

Consider an email application that does much of its work through HTTP requests. However, there is also a chat application embedded within it that works over WebSocket APIs. If a user is actively chatting with someone, we should not timeout the HttpSession, since this would be a pretty poor user experience. However, this is exactly what JSR-356 does.

另一个问题是,根据 JSR-356,如果 HttpSession 超时,使用该 HttpSession 创建的任何 WebSocket 都应该被强行关闭。这意味着,如果我们正在应用程序中积极聊天,并且未使用 HttpSession,我们也会断开与对话的连接。

Another issue is that, according to JSR-356, if the HttpSession times out, any WebSocket that was created with that HttpSession and an authenticated user should be forcibly closed. This means that, if we are actively chatting in our application and are not using the HttpSession, we also do disconnect from our conversation.

WebSocket Usage

WebSocket Sample 提供了一个 Spring Session 与 WebSockets 集成的工作示例。您可以按照接下来几小节描述的集成基本步骤,但是我们鼓励您在与自己的应用程序集成时按照详细的 WebSocket 指南进行操作。

The WebSocket Sample provides a working sample of how to integrate Spring Session with WebSockets. You can follow the basic steps for integration described in the next few headings, but we encourage you to follow along with the detailed WebSocket Guide when integrating with your own application.

HttpSession Integration

在使用 WebSocket 集成之前,您应该确保首先让 HttpSession Integration 正常工作。

Before using WebSocket integration, you should be sure that you have HttpSession Integration working first.

@19

Spring Session - WebSocket

Rob Winch

本指南描述如何使用 Spring 会话来确保 WebSocket 消息使您的 HttpSession 保持活动状态。

This guide describes how to use Spring Session to ensure that WebSocket messages keep your HttpSession alive.

Spring Session 的 WebSocket 支持仅适用于 Spring 的 WebSocket 支持。具体来说,它不适用于直接使用 JSR-356,因为 JSR-356 没有用于拦截传入 WebSocket 消息的机制。

Spring Session’s WebSocket support works only with Spring’s WebSocket support. Specifically,it does not work with using JSR-356 directly, because JSR-356 does not have a mechanism for intercepting incoming WebSocket messages.

HttpSession Setup

第一步是将 Spring Session 与 HttpSession 集成。这些步骤已在 HttpSession with Redis Guide 中概述。

The first step is to integrate Spring Session with the HttpSession. These steps are already outlined in the HttpSession with Redis Guide.

确保在继续之前,您已将 Spring 会话与 HttpSession 集成。

Please make sure you have already integrated Spring Session with HttpSession before proceeding.

Spring Configuration

在典型的 Spring WebSocket 应用程序中,您会实现 WebSocketMessageBrokerConfigurer。例如,配置可能类似于以下内容:

In a typical Spring WebSocket application, you would implement WebSocketMessageBrokerConfigurer. For example, the configuration might look something like the following:

/*
 * Copyright 2014-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package docs.websocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @author Rob Winch
 */
// tag::class[]
@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/messages").withSockJS();
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.enableSimpleBroker("/queue/", "/topic/");
		registry.setApplicationDestinationPrefixes("/app");
	}

}
// end::class[]

我们可以更新配置以使用 Spring 会话的 WebSocket 支持。以下示例演示如何操作:

We can update our configuration to use Spring Session’s WebSocket support. The following example shows how to do so:

src/main/java/samples/config/WebSocketConfig.java
Unresolved include directive in modules/ROOT/pages/guides/boot-websocket.adoc - include::example$spring-session-samples/spring-session-sample-boot-websocket/src/main/java/sample/config/WebSocketConfig.java[]

连接到 Spring 会话支持时,我们只需做两件事:

To hook in the Spring Session support we only need to change two things:

1 Instead of implementing WebSocketMessageBrokerConfigurer, we extend AbstractSessionWebSocketMessageBrokerConfigurer
2 We rename the registerStompEndpoints method to configureStompEndpoints

`AbstractSessionWebSocketMessageBrokerConfigurer`在幕后执行什么操作?

What does AbstractSessionWebSocketMessageBrokerConfigurer do behind the scenes?

  • WebSocketConnectHandlerDecoratorFactory is added as a WebSocketHandlerDecoratorFactory to WebSocketTransportRegistration. This ensures a custom SessionConnectEvent is fired that contains the WebSocketSession. The WebSocketSession is necessary to end any WebSocket connections that are still open when a Spring Session is ended.

  • SessionRepositoryMessageInterceptor is added as a HandshakeInterceptor to every StompWebSocketEndpointRegistration. This ensures that the Session is added to the WebSocket properties to enable updating the last accessed time.

  • SessionRepositoryMessageInterceptor is added as a ChannelInterceptor to our inbound ChannelRegistration. This ensures that every time an inbound message is received, that the last accessed time of our Spring Session is updated.

  • WebSocketRegistryListener is created as a Spring bean. This ensures that we have a mapping of all of the Session IDs to the corresponding WebSocket connections. By maintaining this mapping, we can close all the WebSocket connections when a Spring Session (HttpSession) is ended.

websocket Sample Application

`websocket`示例应用程序演示了如何在 WebSocket 中使用 Spring Session。

The websocket sample application demonstrates how to use Spring Session with WebSockets.

Running the websocket Sample Application

您可以获取 源代码 并调用以下命令运行示例:

You can run the sample by obtaining the source code and invoking the following command:

$ ./gradlew :spring-session-sample-boot-websocket:bootRun

出于测试会话到期时间的目的,你可能希望在启动应用程序之前添加以下配置属性,将会话到期时间更改为 1 分钟(默认值为 30 分钟):

For the purposes of testing session expiration, you may want to change the session expiration to be 1 minute (the default is 30 minutes) by adding the following configuration property before starting the application:

src/main/resources/application.properties
server.servlet.session.timeout=1m # Session timeout. If a duration suffix is not specified, seconds will be used.

要让示例发挥作用,你必须在 localhost 上 install Redis 2.8+ 并使用默认端口 (6379) 运行它。或者,你可以更新 RedisConnectionFactory 以指向 Redis 服务器。另一个选项是使用 Docker 在 localhost 上运行 Redis。有关详细说明,请参见 Docker Redis repository

For the sample to work, you must install Redis 2.8+ on localhost and run it with the default port (6379). Alternatively, you can update the RedisConnectionFactory to point to a Redis server. Another option is to use Docker to run Redis on localhost. See Docker Redis repository for detailed instructions.

您现在应该能够访问 [role="bare"][role="bare"]http://localhost:8080/ 中的应用程序。

You should now be able to access the application at [role="bare"]http://localhost:8080/

Exploring the websocket Sample Application

现在你可以尝试使用该应用程序。使用以下信息进行身份验证:

Now you can try using the application. Authenticate with the following information:

  • Username rob

  • Password password

现在,单击*Login*按钮。你现在应该以用户*rob*的身份进行身份验证。

Now click the Login button. You should now be authenticated as the user rob.

打开一个隐身窗口并访问 [role="bare"][role="bare"]http://localhost:8080/

Open an incognito window and access [role="bare"]http://localhost:8080/

系统会提示你提供一个登录表单。使用以下信息进行身份验证:

You are prompted with a login form. Authenticate with the following information:

  • Username luke

  • Password password

现在从 Rob 发送一条消息给 Luke。消息应该会显示出来。

Now send a message from rob to luke. The message should appear.

等待两分钟,然后再次尝试从 Rob 给 Luke 发送一条消息。你可以看到,消息不再发送。

Wait for two minutes and try sending a message from rob to luke again. You can see that the message is no longer sent.

Why two minutes?

Spring Session 在 60 秒后到期,但不能保证在 60 秒内收到来自 Redis 的通知。为了确保在合理的时间范围内关闭套接字,Spring Session 会在每分钟的第 00 秒运行一个后台任务,以强制清除所有过期的会话。这意味着你需要最多等待两分钟,WebSocket 连接才会关闭。

Spring Session expires in 60 seconds, but the notification from Redis is not guaranteed to happen within 60 seconds. To ensure the socket is closed in a reasonable amount of time, Spring Session runs a background task every minute at 00 seconds that forcibly cleans up any expired sessions. This means you need to wait at most two minutes before the WebSocket connection is closed.

You can now try accessing [role="bare"]http://localhost:8080/ You are prompted to authenticate again. This demonstrates that the session properly expires.

现在重复相同的练习,但是不要等待两分钟,而是在每 30 秒内从每个用户发送一条消息。您可以看到仍会继续发送消息。尝试访问 [role="bare"][role="bare"]http://localhost:8080/,您不会收到再次进行身份验证的提示。这表明会话保持活跃。

Now repeat the same exercise, but instead of waiting two minutes, send a message from each of the users every 30 seconds. You can see that the messages continue to be sent. Try accessing [role="bare"]http://localhost:8080/ You are not prompted to authenticate again. This demonstrates the session is kept alive.

只有来自用户的消息才能使会话保持活动状态。这是因为只有来自用户的消息才表示用户活动。收到的消息并不表示活动,因此不会续订会话过期。

Only messages sent from a user keep the session alive. This is because only messages coming from a user imply user activity. Received messages do not imply activity and, thus, do not renew the session expiration.