1. 程式人生 > >Spring Chapter4 WebSocket 胡亂翻譯 (二)

Spring Chapter4 WebSocket 胡亂翻譯 (二)

標準 .html 丟失 perror 自動刪除 nco 整合 system 訂閱

書接上文,Spring Chapter4 WebSocket 胡亂翻譯 (一)

4.4.4. 消息流

一旦暴露了STOMP端點,Spring應用程序就成為連接客戶端的STOMP代理。 本節介紹服務器端的消息流。

Spring-messaging模塊包含對源自Spring Integration的消息傳遞應用程序的基礎支持,後來被提取並整合到Spring Framework中,以便在許多Spring項目和應用程序場景中得到更廣泛的使用。 下面列出了一些可用的消息傳遞抽象:

  • Message - 包含標頭和內容的消息的簡單表示。
  • MessageHandler - 處理消息的合同。
  • MessageChannel - 發送消息的合同,該消息允許生成者和使用者之間的松散耦合。
  • SubscribableChannel - MessageChannel和MessageHandler訂閱者。
  • ExecutorSubscribableChannel - 使用Executor傳遞消息的SubscribableChannel。

@EnableWebSocketMessageBroker使用上面的組件來實現消息的工作流。

下圖示意了spring自帶的簡單消息代理:

技術分享圖片

“clientInboundChannel” - 用於傳遞從WebSocket客戶端收到的消息。
“clientOutboundChannel” - 用於將服務器消息發送到WebSocket客戶端。
“brokerChannel” - 用於從服務器端的應用程序代碼向消息代理發送消息。

下圖顯示了Spring使用外部的代理,比如ActiveMQ:

技術分享圖片

當從WebSocket連接接收消息時,它們被解碼為STOMP幀,然後變為Spring消息表示,並發送到“clientInboundChannel”以進行進一步處理。 例如,目標頭以“/ app”開頭的STOMP消息可以被路由到帶註釋的控制器中的@MessageMapping方法,而“/ topic”和“/ queue”消息可以直接路由到消息代理。

處理來自客戶端的STOMP消息的帶註釋的@Controller可以通過“brokerChannel”向消息代理發送消息,並且代理將通過“clientOutboundChannel”將消息廣播給匹配的訂戶。 相同的控制器也可以響應HTTP請求執行相同的操作,因此客戶端可以執行HTTP POST,然後@PostMapping方法可以向消息代理發送消息以向訂閱的客戶端廣播。

讓我們通過一個簡單的例子來追蹤消息流。 鑒於以下服務器設置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio");
    }

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

@Controller
public class GreetingController {
    @MessageMapping("/greeting"){
    public String handle(String greeting) {
        return "[" + getTimestamp() + ": " + greeting;
    }
}

1.客戶端連接到"http://localhost:8080/portfolio",一旦建立了WebSocket連接,STOMP幀就會開始流動。
2.客戶端發送帶有目標頭"/topic/greeting"的SUBSCRIBE幀。一旦接收並解碼,該消息就被發送到“clientInboundChannel”,然後被路由到存儲客戶端訂閱的消息代理。
3.客戶端將SEND幀發送到"/app/greeting"。 "/app"前綴有助於將其路由到帶註釋的控制器。刪除“/ app”前綴後,目標的剩余“/ greeting”部分將映射到GreetingController中的@MessageMapping方法。
4.從GreetingController返回的值變為Spring消息,其中消息內容基於返回值和默認目標頭"/topic/greeting"(從輸入目標派生,"/app"替換為"/topic" )。生成的消息將發送到"brokerChannel" 並由消息代理處理。
5.消息代理找到所有匹配的訂戶,並通過“clientOutboundChannel”向每個訂戶發送MESSAGE幀,消息被編碼為STOMP幀並在WebSocket連接上發送。

4.4.5. Annotated Controllers

應用程序可以使用帶@Controller註釋的類來處理來自客戶端的消息。 這些類可以聲明@MessageMapping,@ SubscribeMapping和@ExceptionHandler方法,如下所述。

@MessageMapping

@MessageMapping註釋可用於根據目標路由消息的方法。 它在方法級別和類型級別受支持。 在類型級別,@ MessessMapping用於表示控制器中所有方法的共享映射。

默認情況下,目標映射應為Ant樣式,路徑模式,例如, “/foo *”,“/foo/**”。 模式包括對模板變量的支持,例如 “/foo /{id}”,可以使用@DestinationVariable方法參數引用。

當@MessageMapping方法返回一個值時,默認情況下,該值通過配置的MessageConverter序列化為有效負載,然後作為消息發送到“brokerChannel”,從那裏向用戶廣播。 出站消息的目的地與入站消息的目的地相同,但前綴為“/ topic”。

您可以使用@SendTo方法批註來自定義將消息內容發送到的目標。 @SendTo也可以在類級別使用,以共享發送消息的默認目標。 @SendToUser是僅向與消息關聯的用戶發送消息的變體。 有關詳細信息,請參閱用戶目標。

@MessageMapping方法的返回值可以用ListenableFuture,CompletableFuture或CompletionStage包裝,以便異步生成消息內容。

作為從@MessageMapping方法返回消息內容的替代方法,您還可以使用SimpMessagingTemplate發送消息,這也是在封面下處理返回值的方式。 請參閱發送消息。

@SubscribeMapping

@SubscribeMapping類似於@MessageMapping,但僅將映射縮小為訂閱消息。 它支持與@MessageMapping相同的方法參數。 但是對於返回值,默認情況下,消息通過“clientOutboundChannel”直接發送到客戶端以響應訂閱,而不是通過“brokerChannel”作為對匹配訂閱的廣播發送給代理。 添加@SendTo或@SendToUser會覆蓋此行為並發送給代理。

什麽時候有用? 假設應用程序控制器映射到“/app”時代理映射到“/topic”和“/queue”。 在此設置中,代理將所有訂閱存儲到旨在用於重復廣播的“/topic”和“/queue”,並且不需要應用程序參與。 客戶端還可以訂閱一些“/app”目的地,並且控制器可以返回響應該訂閱的值而不涉及代理,實際上是一次性的請求 - 回復交換,而無需再次存儲或使用訂閱。 一種情況是在啟動時使用初始數據填充UI。

什麽時候這沒用? 不要嘗試將代理和控制器映射到相同的目標前綴,除非您希望由於某種原因單獨處理消息(包括訂閱)。 入站消息是並行處理的。 無法保證代理或控制器是否將首先處理給定的消息。 如果在存儲訂閱並準備好廣播時通知目標,則客戶端應該在服務器支持時詢問收據(簡單代理不支持)。 例如,使用Java STOMP Client:

@Autowired
private TaskScheduler messageBrokerTaskScheduler;

// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);

// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
    // Subscription ready...
});

一個服務器端的選擇是在brokerChannel上註冊ExecutorChannelInterceptor,並實現在處理完消息(包括訂閱)後調用的afterMessageHandled方法。

@MessageExceptionHandler

應用程序可以使用@MessageExceptionHandler方法來處理來自@MessageMapping方法的異常。 感興趣的異常可以在註釋本身中聲明,或者如果要獲取對異常實例的訪問權限,則可以通過方法參數聲明:

@Controller
public class MyController {
    // ...
    @MessageExceptionHandler
    public ApplicationError handleException(MyException exception) {
        // ...
        return appError;
    }
}

@MessageExceptionHandler方法支持靈活的方法簽名,並支持與@MessageMapping方法相同的方法參數類型和返回值。

通常,@ MessessExceptionHandler方法在聲明它們的@Controller類(或類層次結構)中應用。如果您希望這些方法在控制器之間全局應用更多,則可以在標有@ControllerAdvice的類中聲明它們。 這與Spring MVC中的類似支持相當。

4.4.6. 發送消息

如果要從應用程序的任何部分向連接的客戶端發送消息,該怎麽辦? 任何應用程序組件都可以向“brokerChannel”發送消息。 最簡單的方法是註入一個SimpMessagingTemplate,並使用它來發送消息。 通常,應該很容易按類型註入,例如:

@Controller
public class GreetingController {
    private SimpMessagingTemplate template;

    @Autowired
    public GreetingController(SimpMessagingTemplate template) {
        this.template = template;
    }

    @RequestMapping(path = "/greetings", method = POST)
    public void greet(String greeting) {
        String text = "[" + getTimestamp() + "]:" + greeting;
        this.template.convertAndSend("/topic/greetings", text);
    }
}

但如果存在相同類型的另一個bean,它也可以通過其名稱“brokerMessagingTemplate”進行限定。

4.4.7. Simple Broker

內置的簡單消息代理處理來自客戶端的訂閱請求,將它們存儲在內存中,並將消息廣播到具有匹配目標的連接客戶端。 代理支持類似路徑的目標,包括對Ant樣式目標模式的訂閱。

4.4.8. External Broker

簡單的代理非常適合入門但僅支持STOMP命令的子集(例如,沒有ack,收據等),依賴於簡單的消息發送循環,並且不適合於群集。 作為替代方案,應用程序可以升級到使用功能齊全的消息代理。

檢查STOMP文檔以查找您選擇的消息代理(例如RabbitMQ,ActiveMQ等),安裝代理,並在啟用STOMP支持的情況下運行它。 然後在Spring配置中啟用STOMP代理中繼而不是簡單代理。

以下是啟用功能齊全的代理的示例配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }

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

上述配置中的“STOMP代理中繼”是Spring MessageHandler,它通過將消息轉發到外部消息代理來處理消息。 為此,它建立到代理的TCP連接,將所有消息轉發給它,然後通過其WebSocket會話將從代理接收的所有消息轉發給客戶端。 從本質上講,它充當“轉發”,可以在兩個方向上轉發消息。

註意:請將io.projectreactor.ipc:reactor-netty和io.netty:netty-all dependencies添加到項目中以進行TCP連接管理。

此外,應用程序組件(例如,HTTP請求處理方法,業務服務等)也可以向代理中繼發送消息,如發送消息中所述,以便向訂閱的WebSocket客戶端廣播消息。

實際上,代理中繼實現了健壯且可擴展的消息廣播。

4.4.9. Connect to Broker

STOMP代理中繼維護與代理的單個“系統”TCP連接。 此連接僅用於源自服務器端應用程序的消息,而不用於接收消息。 您可以為此連接配置STOMP憑據,即STOMP幀登錄和密碼標頭。 這在XML命名空間和Java配置中都顯示為systemLogin / systemPasscode屬性,默認值為guest / guest。

STOMP代理中繼還為每個連接的WebSocket客戶端創建單獨的TCP連接。 您可以配置STOMP憑據以用於代表客戶端創建的所有TCP連接。 它在XML命名空間和Java配置中作為clientLogin / clientPasscode屬性公開,默認值為guest / guest。

提示:STOMP代理中繼始終在每個CONNECT幀上設置登錄和密碼頭,它代表客戶端轉發給代理。 因此,WebSocket客戶端無需設置這些標頭; 他們會被忽略。 正如身份驗證部分所述,WebSocket客戶端應該依賴HTTP身份驗證來保護WebSocket端點並建立客戶端身份。

STOMP代理中繼還通過“system”TCP連接向消息代理發送和接收心跳。 您可以配置發送和接收心跳的間隔(默認情況下每個10秒)。 如果與代理的連接丟失,代理中繼將繼續嘗試每5秒重新連接一次,直到成功為止。

任何Spring bean都可以實現ApplicationListener <BrokerAvailabilityEvent>,以便在與代理的“系統”連接丟失並重新建立時接收通知。 例如,股票報價服務廣播股票報價可以在沒有活動的“system”連接時停止嘗試發送消息。

默認情況下,STOMP代理中繼始終連接,並在連接丟失時根據需要重新連接到同一主機和端口。 如果您希望提供多個地址,則在每次嘗試連接時,您都可以配置地址供應商,而不是固定的主機和端口。 例如:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    // ...
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
        registry.setApplicationDestinationPrefixes("/app");
    }

    private ReactorNettyTcpClient<byte[]> createTcpClient() {
        Consumer<ClientOptions.Builder<?>> builderConsumer = builder -> {
            builder.connectAddress(() -> {
                // Select address to connect to ...
            });
        };
        return new ReactorNettyTcpClient<>(builderConsumer, new StompReactorNettyCodec());
    }
}

還可以使用virtualHost屬性配置STOMP代理中繼。 此屬性的值將被設置為每個CONNECT幀的主機頭,並且可能在例如雲環境中有用,其中建立TCP連接的實際主機與提供基於雲的STOMP服務的主機不同。

4.4.10 點作為分隔符

當消息路由到@MessageMapping方法時,它們與AntPathMatcher匹配,並且默認模式應使用斜杠“/”作為分隔符。 這是Web應用程序中的一個很好的約定,類似於HTTP URL。 但是,如果您更習慣於消息傳遞約定,則可以切換到使用點“.” 作為分隔符。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    // ...

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setPathMatcher(new AntPathMatcher("."));
        registry.enableStompBrokerRelay("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

之後,控制器可以使用點“.” 作為@MessageMapping方法中的分隔符:

@Controller
@MessageMapping("foo")
public class FooController {
    @MessageMapping("bar.{baz}")
    public void handleBaz(@DestinationVariable String baz) {
        // ...
    }
}

客戶端現在可以向“/app/foo.bar.baz123”發送消息。

在上面的示例中,我們沒有更改“代理中繼”上的前綴,因為它們完全依賴於外部消息代理。 檢查您正在使用的代理的STOMP文檔頁面,以查看它為目標標頭支持的約定。

另一方面,“簡單代理”確實依賴於配置的PathMatcher,因此如果您切換也將應用於代理的分隔符,並且將消息中的目標與訂閱中的模式匹配。

4.4.11認證 (Authentication)

WebSocket消息傳遞會話中的每個STOMP都以HTTP請求開始 - 可以是升級到WebSockets的請求(即WebSocket握手),或者在SockJS回退一系列SockJS HTTP傳輸請求的情況下。

Web應用程序已經具有用於保護HTTP請求的身份驗證和授權。 通常,用戶通過Spring Security使用某種機制(例如登錄頁面,HTTP基本身份驗證或其他)進行身份驗證。 經過身份驗證的用戶的安全上下文保存在HTTP會話中,並與同一個基於cookie的會話中的後續請求相關聯。

因此,對於WebSocket握手或SockJS HTTP傳輸請求,通常已經存在可通過HttpServletRequest#getUserPrincipal()訪問的經過身份驗證的用戶。 Spring自動將該用戶與為其創建的WebSocket或SockJS會話相關聯,隨後通過用戶頭與該會話上傳輸的所有STOMP消息相關聯。

簡而言之,為了安全性,典型的Web應用程序不需要做任何其他特殊的事情。 用戶在HTTP請求級別進行身份驗證,並通過基於cookie的HTTP會話維護安全上下文,然後將該會話與為該用戶創建的WebSocket或SockJS會話相關聯,並在流經應用程序的每個Message上標記用戶標頭。

請註意,STOMP協議在CONNECT幀上確實有“登錄”和“密碼”標頭。 這些最初設計用於並且仍然需要例如用於TCP上的STOMP。 但是,對於STOMP over WebSocket,Spring默認忽略STOMP協議級別的授權標頭,並假定用戶已在HTTP傳輸級別進行了身份驗證,並期望WebSocket或SockJS會話包含經過身份驗證的用戶。

提示:Spring Security提供WebSocket子協議授權,該授權使用ChannelInterceptor根據其中的用戶頭來授權消息。 此外,Spring Session還提供WebSocket集成,以確保在WebSocket會話仍處於活動狀態時,用戶HTTP會話不會過期。

4.4.12令牌認證

Spring Security OAuth支持基於令牌的安全性,包括JSON Web Token(JWT)。 這可以用作Web應用程序中的身份驗證機制,包括STOMP over WebSocket交互,正如上一節所述,即通過基於cookie的會話維護身份。

同時,基於cookie的會話並不總是最適合,例如在壓根不希望維護服務器端會話的應用程序中,或者在通常使用標頭進行身份驗證的手機應用程序中。

WebSocket協議RFC 6455“沒有規定服務器在WebSocket握手期間可以對客戶端進行身份驗證的任何特定方式。” 實際上,瀏覽器客戶端只能使用標準身份驗證標頭(即基本HTTP身份驗證)或cookie,並且不能提供自定義標頭。 同樣,SockJS JavaScript客戶端沒有提供使用SockJS傳輸請求發送HTTP標頭的方法,請參閱sockjs-client問題196.相反,它確實允許發送可用於發送令牌但具有其自身缺點的查詢參數,例如 因為令牌可能無意中使用服務器日誌中的URL進行了記錄。

提示:以上限制適用於基於瀏覽器的客戶端,不適用於基於Spring Java的STOMP客戶端,該客戶端支持使用WebSocket和SockJS請求發送頭文件

因此,希望避免使用cookie的應用程序可能無法在HTTP協議級別進行身份驗證。 他們可能更喜歡在STOMP消息傳遞協議級別使用標頭進行身份驗證,而不是使用Cookie。有兩個簡單的步驟可以做到這一點:

  1. 使用STOMP客戶端在連接時傳遞身份驗證標頭。
  2. 使用ChannelInterceptor處理身份驗證標頭。

下面是註冊自定義身份驗證攔截器的示例服務器端配置。 請註意,攔截器只需要在CONNECT消息上進行身份驗證並設置用戶頭。 Spring將記錄並保存經過身份驗證的用戶,並將其與同一會話中的後續STOMP消息相關聯:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.setInterceptors(new ChannelInterceptorAdapter() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    Authentication user = ...; // access authentication header(s)
                    accessor.setUser(user);
                }
                return message;
            }
        });
    }
}

還要註意,當使用Spring Security的消息授權時,目前您需要確保在Spring Security之前申請認證ChannelInterceptor配置。 最好通過在自己的標記為@Order(Ordered.HIGHEST_PRECEDENCE + 99)的WebSocketMessageBrokerConfigurer實現中聲明自定義攔截器來完成。

4.4.13 用戶目的地

應用程序可以發送針對特定用戶的消息,Spring的STOMP支持可識別以“/user/”為前綴的目標。 例如,客戶端可能訂閱目標“/user/queue/position-updates”。 該目的地將由UserDestinationMessageHandler處理並轉換為用戶會話唯一的目的地,例如,“/queue/position-updates-user123”。 這提供了訂閱一般命名的目的地的便利性,同時確保不與訂閱相同目的地的其他用戶發生沖突,使得每個用戶可以接收唯一的庫存位置更新。

在發送方,消息可以發送到目的地,例如“/user/{username}/queue/position-updates”,然後由UserDestinationMessageHandler將其轉換為一個或多個目的地,每個目的地對應用戶會話。 這允許應用程序中的任何組件發送針對特定用戶的消息,而不必知道除其名稱和通用目標之外的任何內容。 通過註釋和消息傳遞模板也支持這一點。

例如,消息處理方法可以向與通過@SendToUser註釋處理的消息相關聯的用戶發送消息(在類級別上也支持共享公共目的地):

@Controller
public class PortfolioController {
    @MessageMapping("/trade")
    @SendToUser("/queue/position-updates")
    public TradeResult executeTrade(Trade trade, Principal principal) {
        // ...
        return tradeResult;
    }
}

如果用戶有多個會話,則默認情況下,所有訂閱給定目標的會話都是目標。 但是,有時可能需要僅定位發送正在處理的消息的會話。 這可以通過將broadcast屬性設置為false來完成,例如:

@Controller
public class MyController {
    @MessageMapping("/action")
    public void handleAction() throws Exception {
        // raise MyBusinessException here
    }

    @MessageExceptionHandler
    @SendToUser(destinations = "/queue/errors", broadcast = false)
    public ApplicationError handleException(MyBusinessException exception) {
        // ...
        return appError;
    }
}

雖然用戶目的地通常意味著經過身份驗證的用戶,但並不嚴格要求。 與經過身份驗證的用戶無關的WebSocket會話可以訂閱用戶目標。 在這種情況下,@ SendToUser註釋的行為與broadcast = false完全相同,即僅針對發送正在處理的消息的會話。

例如,通過註入由Java配置或XML命名空間創建的SimpMessagingTemplate,也可以從任何應用程序組件向用戶目標發送消息(如果需要使用@Qualifier進行限定,則bean名稱為“brokerMessagingTemplate”):

@Service
public class TradeServiceImpl implements TradeService {
    private final SimpMessagingTemplate messagingTemplate;

    @Autowired
    public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    // ...
    public void afterTradeExecuted(Trade trade) {
        this.messagingTemplate.convertAndSendToUser(trade.getUserName(), "/queue/position-updates", trade.getResult());
    }
}

提示:將用戶目標與外部消息代理一起使用時,請檢查代理文檔,了解如何管理非活動隊列,以便在用戶會話結束時刪除所有唯一用戶隊列。 例如,當使用/exchange/amq.direct/position-updates等目標時,RabbitMQ會創建自動刪除隊列。 因此,在這種情況下,客戶端可以訂閱/user/exchange/amq.direct/position-updates。 同樣,ActiveMQ具有用於清除非活動目標的配置選項。

在多應用程序服務器方案中,用戶目標可能仍未解析,因為用戶連接到不同的服務器。 在這種情況下,您可以配置目標以廣播未解析的消息,以便其他服務器有機會嘗試。 這可以通過Java config中的MessageBrokerRegistryuserDestinationBroadcast屬性和XML中的message-broker元素的userdestination-broadcast屬性來完成。

4.4.14消息順序

來自代理的消息將發布到“clientOutboundChannel”,從那裏將它們寫入WebSocket會話。 由於通道由ThreadPoolExecutor支持,因此消息在不同的線程中處理,並且客戶端接收的結果序列可能與發布的確切順序不匹配。

如果這是一個問題,請啟用以下標誌:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    protected void configureMessageBroker(MessageBrokerRegistry registry) {
        // ...
        registry.setPreservePublishOrder(true);
    }
}

設置標誌後,同一客戶端會話每次只發送一個消息發布到“clientOutboundChannel”,以便保證發布順序。 請註意,這會導致額外性能開銷,因此僅在需要時才啟用它。

4.4.15. 事件

發布了幾個ApplicationContext事件(如下所列),可以通過實現Spring的ApplicationListener接口來接收它們。

  • BrokerAvailabilityEvent
  • SessionConnectEvent
  • SessionConnectedEvent
  • SessionSubscribeEvent
  • SessionUnsubscribeEvent
  • SessionDisconnectEvent

註意:使用功能齊全的代理時,STOMP“代理中繼”會自動重新連接“system”連接,以防代理暫時不可用。 但是,客戶端連接不會自動重新連接。 假設啟用了心跳,客戶端通常會註意到代理在10秒內沒有響應。 客戶端需要實現自己的重新連接邏輯。

4.4.16攔截

事件提供STOMP連接生命周期的通知,而不是每個客戶端消息的通知。 應用程序還可以註冊ChannelInterceptor來攔截任何消息,以及處理鏈的任何部分。 例如,攔截來自客戶端的入站消息:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.setInterceptors(new MyChannelInterceptor());
    }
}

自定義的ChannelInterceptor可以使用StompHeaderAccessor或SimpMessageHeaderAccessor來訪問有關消息的信息。

public class MyChannelInterceptor implements ChannelInterceptor {
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        StompCommand command = accessor.getStompCommand();
        // ...
        return message;
    }
}

應用程序還可以實現ExecutorChannelInterceptor,它是ChannelInterceptor的子接口,在處理消息的線程中具有回調。 雖然為發送到通道的每個消息調用一次ChannelInterceptor,但ExecutorChannelInterceptor在訂閱來自通道的消息的每個MessageHandler的線程中提供掛鉤。

請註意,就像上面的SesionDisconnectEvent一樣,可能已從客戶端發送DISCONNECT消息,或者也可能在WebSocket會話關閉時自動生成。 在某些情況下,攔截器可能會在每個會話中多次攔截此消息。 對於多個斷開連接事件,組件應該是冪等的。

4.4.17 STOMP客戶端

Spring可以通過WebSocket客戶端或者通過TCP客戶端這2種方式提供STOMP。
要開始創建和配置WebSocketStompClient:

WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats

在上面的示例中,StandardWebSocketClient可以替換為SockJsClient,因為它也是WebSocketClient的實現。 SockJsClient可以使用WebSocket或基於HTTP的傳輸作為後備。 有關更多詳細信息,請參閱SockJsClient。

接下來建立連接並為STOMP會話提供處理程序:

String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);

當會話準備好使用時,會通知處理程序:

public class MyStompSessionHandler extends StompSessionHandlerAdapter {
    @Override
    public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
        // ...
    }
}

建立會話後,可以發送任何消息內容,並使用配置的MessageConverter對其進行序列化:

session.send("/topic/foo", "payload");

您也可以訂閱目的地。 訂閱方法需要處理訂閱消息的處理程序,並返回可用於取消訂閱的訂閱句柄。 對於每個收到的消息,處理程序可以指定消息內容應該反序列化的目標對象類型:

session.subscribe("/topic/foo", new StompFrameHandler() {
    @Override
    public Type getPayloadType(StompHeaders headers) {
    return String.class;
    }
    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
    // ...
    }
});

要啟用STOMP心跳,請使用TaskScheduler配置WebSocketStompClient,並可選擇自定義心跳間隔,寫入不活動10秒,導致發送心跳,10秒讀取不活動,關閉連接。

STOMP協議還支持收據,其中客戶端必須添加“收據”標頭,服務器在處理發送或訂閱後用RECEIPT幀響應。 為了支持這一點,StompSession提供了setAutoReceipt(boolean),它導致在每個後續發送或訂閱時添加“收據”標頭。 或者,您也可以手動將“收據”標題添加到StompHeaders。 發送和訂閱都返回一個Receiptable實例,可用於註冊接收成功和失敗回調。 對於此功能,客戶端必須配置TaskScheduler和收據到期前的時間(默認為15秒)。

請註意,StompSessionHandler本身是一個StompFrameHandler,它允許它處理ERROR幀以及處理消息的異常的handleException回調,以及包含ConnectionLostException的傳輸級錯誤的handleTransportError。

4.4.18. WebSocket Scope

每個WebSocket會話都有一個屬性映射。 映射作為標頭附加到入站客戶端消息,並且可以從控制器方法訪問,例如:

@Controller
public class MyController {
    @MessageMapping("/action")
    public void handle(SimpMessageHeaderAccessor headerAccessor) {
        Map<String, Object> attrs = headerAccessor.getSessionAttributes();
        // ...
    }
}

也可以在websocket範圍中聲明一個Spring管理的bean。 WebSocket範圍的bean可以註入控制器和“clientInboundChannel”上註冊的任何通道攔截器。 這些通常是單例,比任何單獨的WebSocket會話都更長壽。 因此,您需要為WebSocket範圍的bean使用範圍代理模式:

@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {
    @PostConstruct
    public void init() {
        // Invoked after dependencies injected
    }

    // ...
    @PreDestroy
    public void destroy() {
        // Invoked when the WebSocket session ends
    }
}

@Controller
public class MyController {
    private final MyBean myBean;

    @Autowired
    public MyController(MyBean myBean) {
        this.myBean = myBean;
    }

    @MessageMapping("/action")
    public void handle() {
        // this.myBean from the current WebSocket session
    }
}

與任何自定義作用域一樣,Spring在第一次從控制器訪問時初始化一個新的MyBean實例,並將該實例存儲在WebSocket會話屬性中。 隨後返回相同的實例,直到會話結束。 WebSocket範圍的bean將調用所有Spring生命周期方法,如上面的示例所示。

4.4.19. Performance

4.4.20. Monitoring

4.4.21. Testing

Spring Chapter4 WebSocket 胡亂翻譯 (二)