从Spring Websocket Stomp服务器断开客户端会话

36

我已经搜索了很多,但找不到答案:Spring websocket stomp server 有没有办法根据sessionId(或者任何其他的条件)断开客户端的连接?

在我的理解中,一旦客户端连接到服务端后,服务端就无法断开客户端的连接。

7个回答

21

实际上,使用一些变通方法,您可以实现您想要的效果。为此,您应该执行以下操作:

  1. 使用java配置(不确定是否可以使用XML配置)
  2. WebSocketMessageBrokerConfigurationSupport类扩展您的配置类,并实现WebSocketMessageBrokerConfigurer接口
  3. 创建自定义子协议websocket处理程序,并将其扩展自SubProtocolWebSocketHandler
  4. 在自定义子协议websocket处理程序中覆盖afterConnectionEstablished方法,您将可以访问WebSocketSession:)

我创建了一个示例spring-boot项目,以展示如何从服务器端断开客户端会话: https://github.com/isaranchuk/spring-websocket-disconnect


2
这个响应应该被接受,它可以在没有客户端合作的情况下工作。 - Alessandro Polverini
这仍然是关闭WebSocket的唯一方式吗? - leventunver
1
这个可以工作。尽管我不得不添加"spring.main.allow-bean-definition-overriding=true",因为SubProtocolWebSocketHandler的bean已经存在了。 - Vikash

8
您还可以通过实现自定义的WebSocketHandlerDecorator来断开会话:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig<S extends ExpiringSession> extends AbstractSessionWebSocketMessageBrokerConfigurer<S> {

    @Override
    public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
        registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
            @Override
            public WebSocketHandler decorate(final WebSocketHandler handler) {
                return new WebSocketHandlerDecorator(handler) {
                    @Override
                    public void afterConnectionEstablished(final WebSocketSession session) throws Exception {

                        session.close(CloseStatus.NOT_ACCEPTABLE);
                        super.afterConnectionEstablished(session);
                    }
                };
            }
        });
        super.configureWebSocketTransport(registration);
    }


    @Override
    protected void configureStompEndpoints(final StompEndpointRegistry registry) {
    registry.addEndpoint("/home")
            .setHandshakeHandler(new DefaultHandshakeHandler(
                    new UndertowRequestUpgradeStrategy() // If you use undertow
                    // new JettyRequestUpgradeStrategy()
                    // new TomcatRequestUpgradeStrategy()
            ))
            .withSockJS();
    }
}

嗨,感谢您提供的解决方案。我只是想知道,如果当代码执行afterConnectionEstablished时,消息仍未被处理,那么这种情况是否可能发生? - leventunver
我尝试了你的代码,发现会话甚至在消息处理之前就被关闭了。 - leventunver
@leventunver 我猜这只是一个行为示例,以展示您可以获取会话并关闭它。 - Inego

5
我依靠 @Dániel Kis 的想法,并通过将 WebSocket 会话存储在类似单例的对象中来实现了 WebSocket 会话管理的关键点,以便对已认证用户进行管理。
// WebSocketConfig.java

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
            @Override
            public WebSocketHandler decorate(final WebSocketHandler handler) {
                return new WebSocketHandlerDecorator(handler) {

                    @Override
                    public void afterConnectionEstablished(final WebSocketSession session) throws Exception {

                        // We will store current user's session into WebsocketSessionHolder after connection is established
                        String username = session.getPrincipal().getName();
                        WebsocketSessionHolder.addSession(username, session);

                        super.afterConnectionEstablished(session);
                    }
                };
            }
        });
    }
}

存储WebSocket用户会话的类为 WebsocketSessionHolder 。我使用“synchronized”块来确保线程安全。实际上,这些块不是很昂贵的操作,因为每个方法(addSession和closeSessions)并不经常使用(在建立和终止连接时)。这里不需要使用ConcurrentHashMap或SynchronizedMap,因为我们在这些方法中对列表执行了一系列操作。
// WebsocketSessionHolder.java

public class WebsocketSessionHolder {

    static {
        sessions = new HashMap<>();
    }
    
    // key - username, value - List of user's sessions
    private static Map<String, List<WebSocketSession>> sessions;

    public static void addSession(String username, WebSocketSession session)
    {
        synchronized (sessions) {
            var userSessions = sessions.get(username);
            if (userSessions == null)
                userSessions = new ArrayList<WebSocketSession>();

            userSessions.add(session);
            sessions.put(username, userSessions);
        }
    }

    public static void closeSessions(String username) throws IOException 
    {
        synchronized (sessions) {
            var userSessions = sessions.get(username);
            if (userSessions != null)
            {
                for(var session : userSessions) {
                    // I use POLICY_VIOLATION to indicate reason of disconnecting for a client
                    session.close(CloseStatus.POLICY_VIOLATION);
                }
                sessions.remove(username);
            }
        }
    }
}

最后的步骤是在某个控制器中终止(断开)指定用户Websocket会话(例如,“ADMIN”)。
//PageController.java

@Controller
public class PageController {
    @GetMapping("/kill-sessions")
    public void killSessions() throws Exception {

        WebsocketSessionHolder.closeSessions("ADMIN");
    }
}

这种解决方案的缺点是,如果建立了多个会话,则同一用户的所有会话都将关闭。 - Bu Saeed

3
据我所知,API没有提供您要寻找的内容,在服务器端,您只能检测到断开连接事件。如果您想断开特定客户端的连接,则必须采取一些变通方法,例如以下方法:
  1. 编写一个客户端JavaScript函数,能够触发断开连接;
  2. 一旦您的客户端连接到服务器上,请在JavaScript中生成一个客户端ID并将其发送到服务器。记住客户端ID,在步骤(4)中需要用到;
  3. 在您希望服务器断开与特定客户端(由ID标识)之间的连接时,向客户端发送包含ID的消息;
  4. 现在,您的客户端JavaScript会评估从服务器发送的消息,并决定调用您在步骤(1)中编写的断开连接函数;
  5. 您的客户端断开连接。
这个变通方法有些繁琐,但是它是可行的。

9
很不幸,这假设客户端遵守断开连接信息。最终我不想让客户端发表意见。将其标记为答案,因为似乎不可能做到。 - user1751547
2
根据SubProtocolWebSocketHandler.java中的afterConnectionEstablished()方法的源代码,你可以看到以下内容:// WebSocketHandlerDecorator可能会关闭session - Dániel Kis

0
在xml配置中,您可以在中使用来创建自定义的WebSocketHandlerDecorator和WebSocketHandlerDecoratorFactory,这些类都实现了decorate方法。

0

首先,您需要通过继承引入一个类作为您的用户类,然后像这样使用它:

if (userObject instanceof User) {
    User user = (User) userObject;
    if (user.getId().equals(userDTO.getId())) {
       for (SessionInformation information : sessionRegistry.getAllSessions(user, true)) {
          information.expireNow();
       }
    }
}    

0

这可能看起来很简短,但我不确定在您的情况下实现会是什么样子。但是,我认为有一些情况需要这种解决方法/解决方案:

  1. 在后端设置超时时间(比如30秒):
    • 以下是使用Spring Boot Websocket(和Tomcat)进行操作的方式:
    @Bean
    public ServletServerContainerFactoryBean websocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxSessionIdleTimeout(MAX_SESSION_IDLE_TIMEOUT); 
        return container;
    }
  1. 如果您想保持会话开启 - 继续发送消息,否则积极发送 ping/pong。在您希望断开会话的情况下,请在应用程序中适当的位置停止 ping/pong 交互。

当然,如果您想立即断开连接,这似乎不是一个合适的解决方案。但是,如果您只是想减少活动连接的数量,ping/pong 可能是一个很好的选择,因为它仅在积极发送消息时保持会话打开,防止会话过早关闭。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接