1. WebSocket介紹
WebSocket協議RFC 6455提供了一種標準化的方法,可以通過一個TCP連接在客戶機和服務器之間建立一個全雙工的雙向通信通道。它是與HTTP不同的TCP協議,但設計用于在HTTP上工作,使用端口80和443,并允許重用現有的防火墻規則。
WebSocket交互從一個HTTP請求開始,該請求使用HTTP 的Upgrade
header進行升級,或者在本例中切換到WebSocket協議。下面的例子展示了這樣一種交互:
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket // Upgrade header
Connection: Upgrade // 使用這個Upgrade連接
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
與通常的200狀態代碼不同,支持WebSocket的服務器返回的輸出如下:
HTTP/1.1 101 Switching Protocols // 協議switch
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
成功握手之后,HTTP upgrade請求底層的TCP套接字將保持打開狀態,以便客戶機和服務器繼續發送和接收消息。
注意,如果WebSocket服務器運行在web服務器的后面(例如nginx),您可能需要將其配置為將WebSocket upgrade請求傳遞給WebSocket服務器。同樣,如果應用程序在云環境中運行,請檢查與WebSocket支持相關的云提供商的說明。
1.1 HTTP VS WebSocket
盡管WebSocket被設計為與HTTP兼容,并從HTTP請求開始,但重要的是要理解這兩個協議之間不同的體系結構和應用程序編程模型。
在HTTP和REST中,應用程序被建模為多個url。要與應用程序交互,客戶機訪問這些url,即請求-響應樣式。服務器根據HTTP URL、方法和頭將請求路由到適當的處理程序。
相反,在WebSockets中,通常只有一個初始連接URL。隨后,所有應用程序消息都在同一個TCP連接上流動。這指向一個完全不同的異步、事件驅動的消息傳遞體系結構。
WebSocket也是一種底層傳輸協議,與HTTP不同,它不為消息的內容指定任何語義。這意味著,除非客戶機和服務器在消息語義上達成一致,否則無法路由或處理消息。
WebSocket客戶機和服務器可以通過HTTP握手請求上的Sec-WebSocket-Protocol
頭協商使用更高級別的消息傳遞協議(例如,STOMP)。如果沒有這些,他們需要制定自己的約定。
1.2 何時使用WebSocket
WebSockets可以使web頁面具有動態性和交互性。然而,在許多情況下,Ajax和HTTP流媒體或長輪詢的組合可以提供簡單而有效的解決方案。
低延遲、高頻率和高容量的組合為WebSocket的使用提供了最佳方案.
還要記住,在Internet上,超出您控制范圍的限制性代理可能會阻止WebSocket交互,這可能是因為它們沒有配置為傳遞upgrade頭,也可能是因為它們關閉了看起來空閑的長壽命連接。這意味著在防火墻內為內部應用程序使用WebSocket比面向公共應用程序更直接。
2. WebSocket API
Spring框架提供了一個WebSocket API,您可以使用它來編寫處理WebSocket消息的客戶端和服務器端應用程序。
2.1 WebSocketHandler
創建一個WebSocket服務是簡單的, 實現WebSocketHandler接口,或者是繼承TextWebSocketHandler或BinaryWebSocketHandler。下面的例子使用TextWebSocketHandler:
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}
}
有專門的WebSocket Java配置和XML命名空間支持將前面的WebSocket處理程序映射到特定的URL,如下例所示:
- Java:
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
- xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
前面的示例用于Spring MVC應用程序,應該包含在DispatcherServlet的配置中。然而,Spring的WebSocket支持并不依賴于Spring MVC。在WebSocketHttpRequestHandler
的幫助下,將WebSocketHandler
集成到其他http服務環境中相對簡單。
當直接或間接使用WebSocketHandler API時,例如,通過STOMP傳遞消息,應用程序必須同步發送消息,因為底層標準WebSocket會話(JSR-356)不允許并發發送。一個解決方法是使用ConcurrentWebSocketSessionDecorator包裝WebSocketSession。
2.2 WebSocket Handshake
自定義初始HTTP WebSocket握手請求的最簡單方法是通過HandshakeInterceptor,它公開握手“之前”和“之后”的方法。您可以使用這個攔截器來阻止握手,或者給WebSocketSession添加屬性。下面的示例使用內置攔截器將HTTP會話屬性傳遞給WebSocket會話:
- Java:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
- xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
一個更高級的選項是擴展DefaultHandshakeHandler,它執行WebSocket握手的步驟,包括驗證客戶端起源、協商子協議和其他細節。如果應用程序需要配置定制的RequestUpgradeStrategy以適應尚未支持的WebSocket服務器引擎和版本,那么它也可能需要使用這些選項.
Spring提供了一個WebSocketHandlerDecorator基類,您可以使用這個基類用其他行為裝飾WebSocketHandler。在使用WebSocket Java配置或XML名稱空間時,默認情況下提供并添加日志記錄和異常處理實現。ExceptionWebSocketHandlerDecorator捕獲來自任何WebSocketHandler方法的所有未捕獲異常,并以狀態1011關閉WebSocket會話,這表示服務器出錯。
2.3 部署
Spring WebSocket API很容易集成到Spring MVC應用程序中,其中DispatcherServlet同時提供HTTP WebSocket握手和其他HTTP請求。通過調用WebSocketHttpRequestHandler,還可以很容易地集成到其他HTTP處理場景中。這既方便又容易理解。但是,對于JSR-356運行時要特別注意。
Java WebSocket API (JSR-356)提供了兩種部署機制。第一個在啟動時的Servlet容器類路徑掃描(Servlet 3特性)。另一個是注冊API,用于Servlet容器初始化。這兩種機制都不能為所有HTTP處理(包括WebSocket握手和所有其他HTTP請求)使用單個“前端控制器”,比如Spring MVC的DispatcherServlet。
這是JSR-356的一個重要限制,即使在JSR-356運行中運行時,Spring的WebSocket也支持特定于服務器的RequestUpgradeStrategy
實現。目前,這種策略適用于Tomcat、Jetty、GlassFish、WebLogic、WebSphere和Undertow(以及WildFly)。
第二個需要考慮的是,具有JSR-356支持的Servlet容器將執行ServletContainerInitializer
(SCI)掃描,這可能會減慢應用程序的啟動速度——在某些情況下會很明顯。如果在升級到支持JSR-356的Servlet容器版本后觀察到明顯的影響,可以通過web.xml中使用< absolelt - ordered />元素有選擇地啟用或禁用web片段(和SCI掃描)。如下例所示:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<absolute-ordering/>
</web-app>
您也可以根據名稱選擇性地啟用web片段,例如Spring自己的SpringServletContainerInitializer
,它提供了對Servlet 3 Java初始化API的支持。下面的例子說明了如何做到這一點:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>
</web-app>
2.4 服務器配置
每個底層WebSocket引擎都公開控制運行時特性的配置屬性,例如消息緩沖區大小、空閑超時等。
對于Tomcat、WildFly和GlassFish,您可以在WebSocket 配置中添加一個ServletServerContainerFactory
Bean,如下面的示例所示:
- Java:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
- xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>
</beans>
對于客戶端WebSocket配置,應該使用
WebSocketContainerFactoryBean
(XML)或ContainerProvider.getWebSocketContainer()
(Java配置)。
對于Jetty,您需要提供預先配置好的Jetty WebSocketServerFactory,并通過WebSocket Java配置將其插入Spring的DefaultHandshakeHandler。
- Java:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(),
"/echo").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
- xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/echo" handler="echoHandler"/>
<websocket:handshake-handler ref="handshakeHandler"/>
</websocket:handlers>
<bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
<constructor-arg ref="upgradeStrategy"/>
</bean>
<bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
<constructor-arg ref="serverFactory"/>
</bean>
<bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
<constructor-arg>
<bean class="org.eclipse.jetty...WebSocketPolicy">
<constructor-arg value="SERVER"/>
<property name="inputBufferSize" value="8092"/>
<property name="idleTimeout" value="600000"/>
</bean>
</constructor-arg>
</bean>
</beans>
2.5 Allowed Origins
從Spring Framework 4.1.5起,WebSocket和SockJS的默認行為是只接受同源請求。也可以允許所有或指定的源列表。這個檢查主要是為瀏覽器客戶端設計的。沒有任何東西可以阻止其他類型的客戶機修改源頭值.
這三種可能的行為是:
- 只允許同源請求(默認): 在這種模式下,當啟用SockJS時,Iframe HTTP響應頭X-Frame-Options被設置為SAMEORIGIN, JSONP傳輸被禁用,因為它不允許檢查請求的起源。因此,當啟用此模式時,不支持IE6和IE7。
- 允許指定的來源列表: 每個被允許的源必須以http://或https://開頭。在此模式下,啟用SockJS,將禁用IFrame傳輸。因此,當啟用此模式時,不支持IE6到IE9。
- 允許所有原點:要啟用此模式,您應該提供*作為允許的原點值。在此模式下,所有傳輸都可用。
您可以配置WebSocket和SockJS允許的起源,如下例所示:
- Java:
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
- xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers allowed-origins="https://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
3. SockJS Fallback
3.1 簡介
SockJS的目標是讓應用程序使用WebSocket API,但在運行時需要時返回到非WebSocket替代方案,而不需要更改應用程序代碼。
SockJS包括:
- SockJS協議。
- SockJS JavaScript client : 用于瀏覽器的客戶端庫。
- SockJS服務器實現,包括Spring框架中的一個Spring -websocket模塊。
- spring-websocket模塊中的SockJS Java客戶端(自4.1版以來)。
SockJS是為瀏覽器設計的。它使用多種技術來支持多種瀏覽器版本。傳輸分為三大類:WebSocket、HTTP流和HTTP長輪詢。
SockJS客戶端首先發送GET /info從服務器獲取基本信息。然后,它必須決定使用什么傳輸工具。如果可能,使用WebSocket。如果沒有,在大多數瀏覽器中,至少有一個HTTP流選項。如果沒有,則使用HTTP (long)輪詢。
所有的傳輸請求都有以下URL結構:
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
- {server-id}: 用于路由集群中的請求,但不用于其他用途。
- {session-id}: 關聯屬于SockJS會話的HTTP請求。
- {transport}: 指示傳輸類型(例如,websocket、xhr-streaming等)。
WebSocket傳輸只需要一個HTTP請求就可以完成WebSocket握手。此后所有消息都在該套接字上交換。
SockJS添加了最小的消息幀。例如,服務器最初發送字母o(“打開”幀),消息以[“message1”、“message2”(json編碼的數組)、字母h(“heartbeat”幀)如果25秒內沒有消息流(默認情況下),以及字母c(“關閉”幀)來關閉會話。
SockJS客戶機允許固定傳輸列表,因此可以一次查看每個傳輸。SockJS客戶機還提供了調試標志,它在瀏覽器控制臺中啟用了有用的消息。在服務器端,您可以為org.springframework.web.socket啟用跟蹤日志記錄。
3.2 啟用 SockJS
- Java:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
- xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
前面的示例用于Spring MVC應用程序,應該包含在DispatcherServlet的配置中。
3.3 IE8 和 IE9
3.4 心跳
SockJS協議要求服務器發送心跳消息,以防止代理將連接掛起。Spring SockJS配置有一個名為heartbeatTime的屬性,您可以使用該屬性定制頻率。默認情況下,假設該連接上沒有發送其他消息, 心跳每隔25秒發送一次。
當在WebSocket和SockJS上使用STOMP時,如果STOMP客戶端和服務器定義了要交換的心跳,SockJS心跳將被禁用。
Spring SockJS支持還允許您配置TaskScheduler來調度心跳任務。任務調度程序由線程池支持,默認設置基于可用處理器的數量。您應該考慮根據您的特定需求定制設置。
3.5 客戶端斷開
HTTP流和HTTP長輪詢SockJS傳輸要求連接保持比通常打開的時間更長。
在Servlet容器中,這是通過Servlet 3異步支持完成的,它允許退出Servlet容器線程,處理請求,并繼續從另一個線程寫入響應。
一個問題是Servlet API不為已經斷開的客戶機提供通知.但是,Servlet容器在隨后嘗試寫入響應時引發異常。由于Spring的SockJS服務支持服務器發送的心跳(默認為每25秒一次),這意味著客戶端斷開連接通常在這段時間內檢測到(如果有消息發送則更早)。
3.6 SockJS 和 CORS
如果允許跨域請求, SockJS協議使用CORS在XHR流傳輸和輪詢傳輸中提供跨域支持。因此,除非檢測到響應中存在CORS header,否則將自動添加CORS標頭。因此,如果應用程序已經配置為提供CORS支持(例如,通過Servlet過濾器),Spring的SockJsService將跳過這一部分。
通過在Spring的SockJsService中設置suppressCors屬性,還可以禁用添加這些CORS頭。
SockJS希望的header和值:
- Access-Control-Allow-Origin:從源請求頭的值初始化。
- Access-Control-Allow-Credentials: 總是設置為true。
- Access-Control-Request-Headers:Initialized from values from the equivalent request header.
- Access-Control-Allow-Methods: 傳輸支持的HTTP方法(請參閱TransportType enum)。
- Access-Control-Max-Age: 設置為31536000(1年)。
有關確切的實現,請參見AbstractSockJsService中的addcorsheader和源代碼中的TransportType enum。
如果CORS配置允許,考慮排除帶有SockJS端點前綴的url,從而讓Spring的SockJsService處理它。
3.7 SockJsClient
Spring提供了一個SockJS Java客戶端來連接到遠程SockJS端點,而無需使用瀏覽器。
SockJS Java客戶機支持websocket、xhr-streaming和xhr-polling傳輸。剩下的只有在瀏覽器中才有意義。
你可以配置WebSocketTransport:
- StandardWebSocketClient: 在JSR-356中運行。
- JettyWebSocketClient :使用Jetty 9+本機WebSocket API
- Spring的WebSocketClient的任何實現
根據定義,XhrTransport既支持xhr流,也支持xhr輪詢,因為從客戶機的角度看,除了用于連接到服務器的URL之外,沒有什么區別。目前有兩種實現方式:
- RestTemplateXhrTransport: 使用Spring的RestTemplate進行HTTP請求.
- JettyXhrTransport : 使用Jetty的HttpClient進行HTTP請求.
下面的示例展示了如何創建SockJS客戶機并連接到SockJS端點:
List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());
SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
SockJS對消息使用JSON格式的數組。默認情況下,使用Jackson 2,并且需要在類路徑上。或者,您可以配置SockJsMessageCodec的自定義實現,并在SockJsClient上配置它。
要使用SockJsClient來模擬大量并發用戶,您需要配置底層HTTP客戶機(用于XHR傳輸),以允許足夠數量的連接和線程。下面的例子展示了如何使用Jetty:
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
下面的示例顯示了服務器端與sockjs相關的屬性(詳細信息請參閱javadoc),您還應該考慮定制這些屬性:
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024) //將streamBytesLimit屬性設置為512KB(默認值為128KB - 128 * 1024)。
.setHttpMessageCacheSize(1000) //將httpMessageCacheSize屬性設置為1,000(缺省值為100)。
.setDisconnectDelay(30 * 1000); //將disconnectDelay屬性設置為30秒(默認為5秒- 5 * 1000)。
}
// ...
}
4. STOMP
Simple Text Oriented Messaging Protocol,簡單文本定向消息協議.
WebSocket協議定義了兩種類型的消息(文本和二進制),但是沒有定義它們的內容。協議定義了一種機制,讓客戶機和服務器協商一個子協議(即更高級別的消息傳遞協議),以便在WebSocket之上使用它來定義每種消息可以發送什么類型的消息、格式是什么、每種消息的內容等等。子協議的使用是可選的,但無論如何,客戶機和服務器都需要就定義消息內容的某些協議達成一致。
4.1 概述
STOMP(簡單的面向文本的消息傳遞協議)最初是為腳本語言(如Ruby、Python和Perl)創建的,用于連接到企業消息代理。它被用于處理常用消息傳遞模式的最小子集。STOMP可以用于任何可靠的雙向流網絡協議,如TCP和WebSocket。雖然STOMP是一個面向文本的協議,但是消息有效負載可以是文本的,也可以是二進制的。
STOMP是一種基于框架的協議,它的框架是基于HTTP建模的。下面的清單顯示了STOMP框架的結構:
COMMAND
header1:value1
header2:value2
Body^@
客戶端可以使用SEND或SUBSCRIBE命令發送或訂閱消息。這啟用了一個簡單的發布-訂閱機制,您可以使用該機制通過代理向其他連接的客戶機發送消息,或者向服務器發送消息以請求執行某些工作。
當您使用Spring的STOMP支持時,Spring WebSocket應用程序充當客戶機的STOMP代理。消息被路由到@Controller消息處理方法或簡單的內存代理,該代理跟蹤訂閱并向訂閱的用戶廣播消息。您還可以將Spring配置為使用專用的STOMP代理(如RabbitMQ、ActiveMQ等)來實際廣播消息。在這種情況下,Spring維護到代理的TCP連接,向代理傳遞消息,并將消息從代理向下傳遞到已連接的WebSocket客戶機。因此,Spring web應用程序可以依賴統一的基于http的安全性、公共驗證和熟悉的消息處理編程模型。
下面的例子顯示了一個訂閱接收股票報價的客戶端,服務器可能會定期發出股票報價(例如,通過一個調度任務將消息通過SimpMessagingTemplate發送給代理):
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@
下面的示例顯示了發送交易請求的客戶機,服務器可以通過@MessageMapping方法處理該請求:
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
執行之后,服務器可以向客戶端廣播交易確認消息和詳細信息。
目的地的含義在STOMP規范中故意保持不透明。它可以是任何字符串,完全取決于STOMP服務器來定義它們支持的目標的語義和語法。
STOMP服務器可以使用MESSAGE命令向所有訂閱者廣播消息。下面的例子顯示了服務器發送股票報價給訂閱的客戶端:
MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
{"ticker":"MMM","price":129.45}^@
服務器不能發送未經請求的消息。來自服務器的所有消息必須響應特定的客戶機訂閱,服務器消息的訂閱id頭必須匹配客戶機訂閱的id頭。
4.2 好處
使用STOMP作為子協議使Spring框架和Spring安全性提供了比使用原始WebSockets更豐富的編程模型。同樣的觀點也適用于HTTP和原始TCP,以及它如何讓Spring MVC和其他web框架提供豐富的功能。以下是一些好處:
- 不需要創建自定義消息傳遞協議和消息格式。
- 可以使用STOMP客戶端,包括Spring框架中的Java客戶端。
- 您可以(選擇性地)使用消息代理(如RabbitMQ、ActiveMQ和其他)來管理訂閱和廣播消息。
- 可以在任意數量的@Controller實例中組織應用程序邏輯,并且可以根據STOMP目標頭將消息路由到它們,而不是使用一個WebSocketHandler為給定連接處理原始WebSocket消息。
- 您可以使用Spring Security基于STOMP目的地和消息類型來保護消息。
4.3 啟用STOMP
spring-messaging和spring-websocket模塊中提供了對基于WebSocket的STOMP支持。一旦有了這些依賴項,就可以使用SockJS Fallback在WebSocket上公開STOMP端點,如下面的示例所示:
- Java
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// /portfolio是WebSocket(或SockJS)客戶端需要連接的端點的HTTP URL,用于WebSocket握手。
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 以/app開頭的消息被路由到@Controller類中的@MessageMapping方法。
config.setApplicationDestinationPrefixes("/app");
// 使用內置的message broker訂閱和廣播消息,并將目標頭以/topic '或' /queue開頭的消息路由到代理。
config.enableSimpleBroker("/topic", "/queue");
}
}
- xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio">
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/topic, /queue"/>
</websocket:message-broker>
</beans>
對于內置的簡單代理,/topic和/queue前綴沒有任何特殊意義。它們只是區分發布-訂閱和點到點消息傳遞(即,許多訂閱者和一個消費者)之間的約定。使用外部代理時,請檢查代理的STOMP頁面,以了解它支持哪種類型的STOMP目的地和前綴。
要從瀏覽器連接,對于SockJS,可以使用SockJS -client。對于STOMP,許多應用程序都使用了jmesnil/ STOMP -websocket庫(也稱為stomp.js),該庫功能齊全,已在生產中使用多年,但不再維護。目前,JSteunou/webstomp-client是該庫最積極維護和發展的繼承者。下面的例子代碼就是基于它的:
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);
stompClient.connect({}, function(frame) {
}
如果您通過WebSocket連接(沒有SockJS),可以使用以下代碼:
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
}
注意,上面示例中的stompClient不需要指定login
和passcode
header.即使它這樣做了,它們也會在服務器端被忽略(或者,更確切地說,被覆蓋)。
4.4 WebSocket服務器
對于Jetty,你需要通過StompEndpointRegistry
設置HandshakeHandler
和WebSocketPolicy
:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
4.5 消息流
一旦公開了STOMP端點,Spring應用程序就成為連接客戶機的STOMP代理。本節描述服務器端上的消息流。
spring-messaging
模塊包含對消息傳遞應用程序的基本支持.下面的列表簡要描述了一些可用的消息傳遞抽象:
- Message: 消息的簡單表示,包括header和有效負載。
- MessageHandler: 處理消息.
- MessageChannel: 用于發送消息的契約,該消息支持生產者和消費者之間的松散耦合。
- SubscribableChannel: 帶有MessageHandler訂閱者的MessageChannel。
- ExecutorSubscribableChannel: 使用
Executor
傳遞消息的SubscribableChannel。
Java配置(即@EnableWebSocketMessageBroker)和XML名稱空間配置(即<websocket:message-broker>)都使用前面的組件組裝消息工作流。下圖顯示了啟用簡單內置message broker時使用的組件:
上圖顯示了三個消息通道:
- clientInboundChannel: 用于傳遞從WebSocket客戶端接收到的消息。
- clientOutboundChannel:用于向WebSocket客戶端發送消息。
- brokerChannel:用于從服務器端應用程序代碼中向消息代理發送消息。
下圖顯示了配置外部代理(如RabbitMQ)來管理訂閱和廣播消息時使用的組件:
前面兩個圖之間的主要區別是使用“代理中繼”將消息通過TCP傳遞到外部STOMP代理,并將消息從代理傳遞到訂閱的客戶機。
當從WebSocket連接接收到消息時,它們被解碼為STOMP幀,轉換為Spring消息表示,并發送到clientInboundChannel進行進一步處理。例如,目標頭以/app開頭的STOMP消息可以路由到控制器中的帶@MessageMapping注釋方法,而/topic和/queue消息可以直接路由到消息代理。
處理來自客戶機的STOMP消息的帶注釋的@Controller可以通過brokerChannel向消息代理發送消息,代理通過clientOutboundChannel將消息廣播給匹配的訂閱者。相同的控制器也可以對HTTP請求執行相同的響應,因此客戶機可以執行HTTP POST,然后@PostMapping方法可以向message broker發送消息,以便向訂閱的客戶機廣播。
我們可以通過一個簡單的例子來跟蹤流程。考慮下面的例子,它設置了一個服務器:
@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;
}
}
上面的例子支持以下流程:
- a. 客戶機連接到http://localhost:8080/portfolio,一旦建立了WebSocket連接,STOMP幀就開始在上面流動。
- b. 客戶端發送一個帶有目標標題/主題/問候語的訂閱框架。接收和解碼后,消息被發送到clientInboundChannel,然后路由到存儲客戶機訂閱的message broker。