在 SpringWebsocket 上向特定用户发送消息

如何发送网络套接字消息从服务器到特定的用户只?

我的 webapp 有 Spring 安全设置并且使用 websocket。我遇到棘手的问题,试图发送消息从服务器到 仅限特定用户

我从阅读 手册的理解是从我们可以做的服务器

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

而在客户方面:

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

但是我从来没有调用过订阅回调,我尝试了很多不同的方法,但是都没有成功。

如果我发送到 /主题/答复它的工作,但所有其他连接的用户将收到它太。

为了说明这个问题,我在 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 }”显示在旁边的“接收消息给我只”为此用户,实际: 没有收到

77982 次浏览

Ah I found what my problem was. First I didn't register the /user prefix on the simple broker

<websocket:simple-broker prefix="/topic,/user" />

Then I don't need the extra /user prefix when sending:

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

Spring will automatically prepend "/user/" + principal.getName() to the destination, hence it resolves into "/user/bob/reply".

This also means in javascript I had to subscribe to different address per user

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

Oh, client side no need to known about current user, server will do that for you.

On server side, using following way to send message to an user:

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

Note: Using queue, not topic, Spring always using queue with sendToUser

On client side

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

Explain

When any websocket connection is open, Spring will assign it a session id (not HttpSession, assign per connection). And when your client subscribe to an channel start with /user/, eg: /user/queue/reply, your server instance will subscribe to a queue named queue/reply-user[session id]

When use send message to user, eg: username is admin You will write simpMessagingTemplate.convertAndSendToUser("admin", "/queue/reply", message);

Spring will determine which session id mapped to user admin. Eg: It found two session wsxedc123 and thnujm456, Spring will translate it to 2 destination queue/reply-userwsxedc123 and queue/reply-userthnujm456, and it send your message with 2 destinations to your message broker.

The message broker receive the messages and provide it back to your server instance that holding session corresponding to each session (WebSocket sessions can be hold by one or more server). Spring will translate the message to destination (eg: user/queue/reply) and session id (eg: wsxedc123). Then, it send the message to corresponding Websocket session

My solution of that based on Thanh Nguyen Van's best explanation, but in addition I have configured MessageBrokerRegistry:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {


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

I created a sample websocket project using STOMP as well. What I am noticing is that

@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();
}

it works whether or not "/user" is included in config.enableSimpleBroker(...

Exactly i did the same and it is working without using user

@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");
}
}

In the following solution, I wrote code snippets for both the client and backend sides. We need to put /user at the start of the socket's topic for client code. Otherwise, the client can not listen to the socket. Dependency

<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;
}
}

You can send message to socket from anywhere. In my case my principal.getName() is equal to userId.

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

Client (ReactJs with 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;

The url attribute of the SockJsClient element is http://localhost:9090/notification-service/socket. http://localhost:9090 is Api Gateway ip address, notification-service is name of the microservice and /socket is defined in the WebSocketConfig.java.