如何使用弹簧websocket向自定义用户发送自定义消息?

我是春季网络袜子的新手。我想向客户发送产品更改。为此,我想按如下方式进行:客户端创建套接字连接并订阅目标:

var socket = new SockJS('/websocket');
var stompClient = Stomp.over(socket);

stompClient.connect({}, function (frame) {
    stompClient.subscribe('/product/changes', function (scoredata) {
        // We received product changes
    });
});
//Send Ajax request and say server I want to know product with id=5 changes.
sendAjaxRequest(5);

我已按如下方式配置了弹簧应用程序:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

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

现在我需要以下方法:

@RestController
public class ProductController {

    @GetMapping("product-{id}")
    public void startSubscribe(@PathVariable("id") Long id) {

        // register current websocket session with product id and 
        // then with convertAndSendToUser send changes to current user.
    }

}

如何实现它?


答案 1

我首先的问题是,当你成功地将websockets与踩踏集成时,为什么你试图向休息控制器发送http请求?如果我正确理解了您的用例,那么我可以想到atm应该有三种解决方案。

解决方案 1(套接字会话↔产品 ID)

您可以通过打开的 websocket 连接将请求直接从客户端发送到服务器。然后,Spring 可以确定哪个 Websocket 会话进行了调用,并且您可以实现业务逻辑。您需要激活另一个名为“/queue”的代理,并为订阅不用于广播时所需的用户目标指定前缀。在客户端,还必须更改订阅路径。最后,必须创建一个使用@Controller注释的类,该类包含消息映射,以便从连接的客户端接收消息。

服务器配置

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket").withSockJS();
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue", "/product");  // <- added "/queue"
        registry.setApplicationDestinationPrefixes("/app");
        registry.setUserDestinationPrefix("/user");
    }
}

服务器控制器

@Controller
public class WebSocketContoller{
    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @MessageMapping("/product/register")
    public void register(@Payload Long productId, @Header("simpSessionId") String sessionId) {
        // register current websocket session with product id and 
        // then with convertAndSendToUser send changes to current user.

        // Example of how to send a message to the user using the sessionId
        String response = "This could also be one of your product objects of type Product";
        SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
        headerAccessor.setSessionId(sessionId);
        headerAccessor.setLeaveMutable(true);

        messagingTemplate.convertAndSendToUser(sessionId,"/queue/product/changes", response, headerAccessor.getMessageHeaders());
    }
}

客户端订阅更改

stompClient.subscribe('/user/queue/product/changes', function (scoredata) {
    // We received product changes
});

有关详细信息,您还可以查看此答案:https://stackoverflow.com/a/26288475/11133168


解决方案 2(主↔产品 ID)

但是,如果您真的想考虑使用 rest 控制器开始注册您的进程,或者如果它只是不符合您的要求,则应查看下面的链接。Spring还能够通过公开的SimpUserRegistry bean跟踪活动的websocket会话及其用户。但是,您需要为客户端输入通道配置自定义通道接收器适配器,具体取决于应用程序的安全性,以确定用户。有关详细信息和代码示例,请查看此答案:https://stackoverflow.com/a/45359294/11133168


解决方案 3(产品 ID 主题)

您还可以订阅特定的产品 ID 主题,这样您甚至不需要知道哪个用户希望收到有关特定产品更改的通知。

客户端订阅更改

//e.g if you want to be notified about changes for products with id 5 
stompClient.subscribe('/product/changes/5', function (scoredata) {
    // We received product changes
});

服务器服务示例

@Service
public class WebSocketProductService{

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    // This would be the method which should inform your clients about specific product     
    // changes, instead of the String parameters a Product object should be used instead, 
    // you have to call this method yourself on product changes or schedule it or sth.
    public void sendProductChange(String product, String productId) {
        this.simpMessagingTemplate.convertAndSend("/product/changes/"+productId, product);
    }
}

服务器控制器

如果要管理产品 ID 订阅的列表,则需要此项。如解决方案 1 中所述,您需要一个用 @Controller 注释的类,其中包含一个用 @SubscribeMapping 注释的方法。如果客户端尝试订阅指定的路径,则调用此方法。

@Controller
public class WebSocketContoller{
    @SubscribeMapping("/product/changes/{productId}")
    public void productIdSubscription(@DestinationVariable Long productId) {
        //Manage your product id subscription list e.g.
    }
}

答案 2

如果要仅在用户请求时才向用户发送产品更新,则可以使用正常的 HTTP 请求。但我理解您希望基于特定于用户的业务逻辑推送通知。您还必须实现以对用户进行身份验证。Spring Security


溶液

我建议使用表在后端添加此业务逻辑 - 每行对应于用户想要订阅更新的 a:user_product_updates( user_id, product_id)product_iduser_id

@GetMapping("product-{id}")
public void startSubscribe(@PathVariable("id") Long id) {
    // Save this custom setting into your models
}

现在,您可以运行计划的后端作业(可以是基于推送通知的业务逻辑的 cron 作业)以向用户发送更新:

@Autowired 
org.springframework.messaging.simp.SimpMessagingTemplate simpMessagingTemplate;   

@Scheduled(cron = "0 0 1 * * ?") // based on your business logic (say daily at 12:01 am)
public void scheduleTaskUsingCronExpression() {
   // loop through user_product_updates table and construct "data"
   // username is from your spring security username (principal.getName())
   simpMessagingTemplate.convertAndSendToUser(username, "/queue/products", data);
}

展望未来,您可能希望添加一些缓存来优化它们(特别是从 中获取产品信息),以便事情顺利进行。product_id


总结

您的网络套接字配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

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

前端应用程序中的侦听器可能如下所示:

that.stompClient.subscribe("/user/queue/products", (message) => {
    if (message.body) {
      // We received product changes
    }
});

用户将注册产品更新:

@GetMapping("product-{id}")
public void startSubscribe(@PathVariable("id") Long id) {
    // Save to your persistence module
    // (that the particular user wants updates from such-and-such products)
}

后端计划程序作业将在可用时发送更新:

@Scheduled(cron = "0 0 1 * * ?") // based on your business logic
public void scheduleTaskUsingCronExpression() {
   // loop through user_product_updates table and construct "data"
   // username is from your spring security username (principal.getName())
   template.convertAndSendToUser(username, "/queue/products", data);
}

推荐