Spring Chapter4 WebSocket 胡亂翻譯 (二)
書接上文,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。有兩個簡單的步驟可以做到這一點:
- 使用STOMP客戶端在連接時傳遞身份驗證標頭。
- 使用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中的MessageBrokerRegistry的userDestinationBroadcast屬性和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 胡亂翻譯 (二)