簡體   English   中英

Spring WebSocket @SendToSession:向特定會話發送消息

[英]Spring WebSocket @SendToSession: send message to specific session

是否可以向特定會話發送消息?

我在客戶端和 Spring servlet 之間有一個未經身份驗證的 websocket。 當異步作業結束時,我需要向特定連接發送未經請求的消息。

@Controller
public class WebsocketTest {


     @Autowired
    public SimpMessageSendingOperations messagingTemplate;

    ExecutorService executor = Executors.newSingleThreadExecutor();

    @MessageMapping("/start")
    public void start(SimpMessageHeaderAccessor accessor) throws Exception {
        String applicantId=accessor.getSessionId();        
        executor.submit(() -> {
            //... slow job
            jobEnd(applicantId);
        });
    }

    public void jobEnd(String sessionId){
        messagingTemplate.convertAndSend("/queue/jobend"); //how to send only to that session?
    }
}

正如您在這段代碼中看到的,客戶端可以啟動一個異步作業,當它完成時,它需要結束消息。 顯然,我只需要向申請人發送消息,而不是向所有人廣播。 這將是巨大的,有一個@SendToSession注釋或messagingTemplate.convertAndSendToSession方法。

更新

我試過這個:

messagingTemplate.convertAndSend("/queue/jobend", true, Collections.singletonMap(SimpMessageHeaderAccessor.SESSION_ID_HEADER, sessionId));

但這會廣播到所有會話,而不僅僅是指定的會話。

更新 2

使用 convertAndSendToUser() 方法進行測試。 這個測試是官方 Spring 教程的 hack: https : //spring.io/guides/gs/messaging-stomp-websocket/

這是服務器代碼:

@Controller
public class WebsocketTest {

    @PostConstruct
    public void init(){
        ScheduledExecutorService statusTimerExecutor=Executors.newSingleThreadScheduledExecutor();
        statusTimerExecutor.scheduleAtFixedRate(new Runnable() {                
            @Override
            public void run() {
                messagingTemplate.convertAndSendToUser("1","/queue/test", new Return("test"));
            }
        }, 5000,5000, TimeUnit.MILLISECONDS);
    } 

     @Autowired
        public SimpMessageSendingOperations messagingTemplate;
}

這是客戶端代碼:

function connect() {
            var socket = new WebSocket('ws://localhost:8080/hello');
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function(frame) {
                setConnected(true);
                console.log('Connected: ' + frame);
                stompClient.subscribe('/user/queue/test', function(greeting){
                    console.log(JSON.parse(greeting.body));
                });
            });
        }

不幸的是,客戶端沒有按預期每 5000 毫秒收到一次會話回復。 我確定“1”是連接的第二個客戶端的有效 sessionId,因為我在調試模式下看到它SimpMessageHeaderAccessor.getSessionId()

背景場景

我想為遠程作業創建一個進度條,客戶端向服務器詢問異步作業,並通過從服務器發送的 websocket 消息檢查其進度。 這不是文件上傳而是遠程計算,因此只有服務器知道每個作業的進度。 我需要向特定會話發送消息,因為每個作業都是由會話啟動的。 客戶端請求遠程計算服務器啟動此作業,並針對每個作業步驟回復申請人客戶端及其作業進度狀態。 客戶端獲取有關其工作的消息並建立進度/狀態欄。 這就是為什么我需要每個會話的消息。 我也可以使用每用戶消息,但 Spring不提供每用戶未經請求的消息。 無法使用 Spring Websocket 發送用戶消息

工作解決方案

 __      __ ___   ___  _  __ ___  _  _   ___      ___   ___   _    _   _  _____  ___  ___   _  _ 
 \ \    / // _ \ | _ \| |/ /|_ _|| \| | / __|    / __| / _ \ | |  | | | ||_   _||_ _|/ _ \ | \| |
  \ \/\/ /| (_) ||   /| ' <  | | | .` || (_ |    \__ \| (_) || |__| |_| |  | |   | || (_) || .` |
   \_/\_/  \___/ |_|_\|_|\_\|___||_|\_| \___|    |___/ \___/ |____|\___/   |_|  |___|\___/ |_|\_|

從 UPDATE2 解決方案開始,我必須使用最后一個參數(MessageHeaders)完成 convertAndSendToUser 方法:

messagingTemplate.convertAndSendToUser("1","/queue/test", new Return("test"), createHeaders("1"));

其中createHeaders()是這個方法:

private MessageHeaders createHeaders(String sessionId) {
        SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
        headerAccessor.setSessionId(sessionId);
        headerAccessor.setLeaveMutable(true);
        return headerAccessor.getMessageHeaders();
    }

無需創建特定的目的地,從 Spring 4.1 開始,它已經開箱即用(參見SPR-11309 )。

鑒於用戶訂閱了/user/queue/something隊列,您可以使用以下命令向單個會話發送消息:

SimpMessageSendingOperations Javadoc 中所述,由於您的用戶名實際上是 sessionId,因此您必須將其設置為標頭,否則DefaultUserDestinationResolver將無法路由消息並將其丟棄。

SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor
    .create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);

messagingTemplate.convertAndSendToUser(sessionId,"/queue/something", payload, 
    headerAccessor.getMessageHeaders());

您不需要為此對用戶進行身份驗證。

這非常復雜,在我看來,不值得。 您需要通過會話 ID 為每個用戶(甚至未經身份驗證的用戶)創建訂閱。

假設每個用戶都為他訂閱了一個唯一的隊列:

stompClient.subscribe('/session/specific' + uuid, handler);

在服務器上,在用戶訂閱之前,您需要通知並發送特定會話的消息並保存到地圖:

    @MessageMapping("/putAnonymousSession/{sessionId}")
    public void start(@DestinationVariable sessionId) throws Exception {
        anonymousUserSession.put(key, sessionId);
    }

之后,當您想向用戶發送消息時,您需要:

messagingTemplate.convertAndSend("/session/specific" + key); 

但我真的不知道您要做什么以及您將如何找到特定會話(誰是匿名的)。

您只需將會話 ID 添加到

  • 服務器端

    convertAndSendToUser(sessionId,apiName,responseObject);

  • 客戶端

    $stomp.subscribe('/user/+sessionId+'/apiName',handler);

筆記:
不要忘記在服務器端的端點添加'/user'

我正在努力解決同樣的問題並且提出的解決方案對我不起作用,因此我不得不采取不同的方法:

  1. 修改 Web 套接字配置,以便通過會話 ID 識別用戶:
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws-endpoint")
                .setHandshakeHandler(new DefaultHandshakeHandler() {
                    
                    @Override
                    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
                        if (request instanceof ServletServerHttpRequest) {
                            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
                            HttpSession session = servletRequest.getServletRequest().getSession();
                            return new Principal() {
                                @Override
                                public String getName() {
                                    return session.getId();
                                }
                            };
                        } else {
                            return null;
                        }
                    }
                }).withSockJS();
    }
  1. 向該會話 ID 發送消息(不帶標題):
    simpMessagingTemplate.convertAndSendToUser(sessionId, "/queue", payload);

最簡單的方法是利用@SendToUser 中的廣播參數。 文檔:

消息是應該發送到與用戶關聯的所有會話還是只發送到正在處理的輸入消息的會話。 默認情況下,這設置為 true,在這種情況下,消息將廣播到所有會話。

對於您的確切情況,它看起來像這樣

    @MessageMapping("/start")
    @SendToUser(value = "/queue/jobend", broadcast = false)
    //...

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM