向特定用户发送消息的Spring Websocket

104

如何从服务器向特定用户发送WebSocket消息?

我的web应用程序已设置spring安全性并使用websocket。我遇到了一个棘手的问题,试图从服务器仅向特定用户发送消息。

我从阅读手册中了解到,我们可以从服务器端执行以下操作:

simpMessagingTemplate.convertAndSend("/user/{username}/reply", reply);

在客户端:

stompClient.subscribe('/user/reply', handler);

但我无法成功调用订阅回调。我尝试了很多不同的路径,但都没有成功。

如果我将它发送到/topic/reply,它可以工作,但是所有其他连接的用户也会收到它。

为了说明这个问题,我在github上创建了一个小项目:https://github.com/gerrytan/wsproblem

复现步骤:

1)克隆并构建该项目(确保您使用的是jdk 1.7和maven 3.1)

$ git clone https://github.com/gerrytan/wsproblem.git
$ cd wsproblem
$ mvn jetty:run

2) 导航至 http://localhost:8080,使用 bob/test 或 jim/test 登录

3) 点击“请求用户特定消息”。预期结果:仅对该用户显示一条消息“hello {username}”并出现在“只发给我收到的消息”旁边,实际结果:未收到任何消息。


你是否一直在关注convertAndSendToUser(String user, String destination, T message)?http://docs.spring.io/spring/docs/4.0.0.M3/javadoc-api/org/springframework/messaging/simp/SimpMessagingTemplate.html#convertAndSendToUser%28java.lang.String,%20java.lang.String,%20T%29 - Viktor K.
1
尝试过那个方法,但没有成功。 - gerrytan
我一直在从事私人项目,这是我们使用的方法,并且对我们有效。我认为一个潜在的问题可能是您订阅了“/user/reply”,但您正在向“/user/{username}/reply”发送消息。我认为您应该删除{username}部分并使用convertAndSendToUser(String user, String destination, T message)。 - Viktor K.
谢谢,但我尝试了 simpMessagingTemplate.convertAndSendToUser(principal.getName(), "/user/reply", reply);,当消息从服务器发送时,它会抛出以下异常 java.lang.IllegalArgumentException: Expected destination pattern "/principal/{userId}/**" - gerrytan
@ViktorK. 是正确的,而你离正确的解决方案也很接近了。你在客户端订阅方面是正确的,你只需要尝试:convertAndSendToUser(principal.getName(), "/reply", reply); - Tip-Sy
6个回答

106

哦,客户端不需要知道当前用户,这是服务器为您做的。

在服务器端,可以使用以下方式向用户发送消息:

simpMessagingTemplate.convertAndSendToUser(username, "/queue/reply", message);

注意:在使用Spring的sendToUser时,始终使用queue而不是topic

在客户端方面:

stompClient.subscribe("/user/queue/reply", handler);

解释

当任何websocket连接打开时,Spring将为其分配一个session id(不是HttpSession,每个连接分配一个)。当客户端订阅以/user/开头的频道时,例如:/user/queue/reply,您的服务器实例将订阅名为queue/reply-user[session id]的队列。

当向用户发送消息时,例如:用户名为admin,您将编写simpMessagingTemplate.convertAndSendToUser("admin", "/queue/reply", message);

Spring将确定映射到用户adminsession id。例如:它发现了两个会话wsxedc123thnujm456,Spring将其翻译成2个目标queue/reply-userwsxedc123queue/reply-userthnujm456,并将您的消息与这2个目标一起发送到您的消息代理。

消息代理接收消息并将其提供回持有每个会话对应的服务器实例(WebSocket会话可以由一个或多个服务器保持)。 Spring将消息翻译为destination(例如:user/queue/reply)和session id(例如:wsxedc123)。然后,它将消息发送到相应的WebSocket会话


8
请问您能解释一下您是如何知道用户名的吗?在您的例子中,您说用户名是“admin”,您是从用户那里接收到的用户名吗? - Jorj
1
当初始化 WebSocket 连接时,用户名是从 HttpSession 中获取的。 - Thanh Nguyen Van
2
请问您能否提供有关如何设置用户名的更多信息?我是否可以在订阅消息中发送用户名? - Andres
1
@Andres:你可以继承DefaultHandshakeHandler并重写determineUser方法。 - Thanh Nguyen Van
2
你的解释非常好。但是在官方文档中,queue/reply-user[session id] 部分在哪里呢? - hbrls
显示剩余5条评论

37
啊,我找到了我的问题所在。首先,我没有在简单代理上注册/user前缀。
<websocket:simple-broker prefix="/topic,/user" />

在发送请求时,我就不需要额外的 /user 前缀了:

convertAndSendToUser(principal.getName(), "/reply", reply);

Spring会自动将"/user/" + principal.getName()添加到目标地址之前,因此它会变成 "/user/bob/reply"。

这也意味着在JavaScript中,我必须为每个用户订阅不同的地址。

stompClient.subscribe('/user/' + userName + '/reply,...) 

8
有没有避免在客户端设置用户名的方法?如果更改该值(例如使用另一个用户名),则可以查看其他人的消息。 - vdenotaris
2
请查看我的解决方案:https://dev59.com/fIPba4cB1Zd3GeqPnBC4#25647822 - vdenotaris
如何在使用 @RequestMapping@MessageMapping 注释的方法中订阅以回复用户,请参见此处:如何在Spring Websocket集成中订阅特定用户名(userId)并从使用 @RequestMapping 注释的方法获取通知? - Shantaram Tupe
嗨,朋友,你能告诉我这是什么吗?“用户”到底是什么?我正在使用Spring Security,我的用户(用户名)是电子邮件地址。当每个用户登录时,他会在Spring Security上获得他的JWT令牌。 - Francisco Souza
我遇到了同样的问题,在找了几个小时之后才看到了你的答案,只有你的解释对我有所帮助。非常感谢您!! - Navaneeth

3

我使用STOMP创建了一个示例websocket项目。

我注意到的是:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic", "/queue");// including /user also works
    config.setApplicationDestinationPrefixes("/app");
}

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

无论config.enableSimpleBroker(...)中是否包含"/user",它都能正常工作。

2

我也是这样做的,而且不需要使用用户,它可以正常运行。

@Configuration
@EnableWebSocketMessageBroker  
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
       registry.addEndpoint("/gs-guide-websocket").withSockJS();
    }

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

嘿,你在哪里使用这个/app?在什么情况下使用? - Francisco Souza

2

我的解决方案基于Thanh Nguyen Van的最佳解释,但我又额外配置了MessageBrokerRegistry:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

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

1
在下面的解决方案中,我编写了客户端和后端代码片段。我们需要在客户端代码中以/user开头放置socket主题。否则,客户端将无法监听socket。 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocketConfig.java

package com.oktaykcr.notificationservice.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 registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/socket").setAllowedOriginPatterns("*");
        registry.addEndpoint("/socket").setAllowedOriginPatterns("*").withSockJS();
    }

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

WebSocketContoller.java

package com.oktaykcr.notificationservice.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class WebSocketController {

    Logger logger = LoggerFactory.getLogger(WebSocketController.class);

    @MessageMapping("/socket")
    @SendTo("/file/status")
    public String fileStatus(@Payload String message) {
        logger.info(message);
        return message;
    }
}

您可以从任何地方向套接字发送消息。在我的情况下,我的principal.getName()等于userId。

simpMessagingTemplate.convertAndSendToUser(userId, "/file/status", socketMessage);

客户端 (使用ReactJs和react-stomp) App.js

import './App.css';
import SockJsClient from 'react-stomp'
import { useRef } from 'react';

function App() {

  const clientRef = useRef();

  const sendMessage = (msg) => {
    clientRef.current.sendMessage('/app/socket', msg);
  }

  return (
    <div className="App">
      <div>
        <button onClick={() => sendMessage("Hola")}>Send</button>
      </div>
      <SockJsClient url='http://localhost:9090/notification-service/socket' topics={['/user/file/status']}
        onMessage={(msg) => { console.log(msg); }}
        ref={(client) => { clientRef.current = client }} />
    </div>
  );
}

export default App;

SockJsClient元素的url属性为http://localhost:9090/notification-service/sockethttp://localhost:9090是Api Gateway的IP地址,notification-service是微服务的名称,/socketWebSocketConfig.java中定义。


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