SockJS 備用方案
在公共網際網路上,您無法控制的限制性代理可能會阻止 WebSocket 互動,原因可能是它們未配置為傳遞 Upgrade 頭,或者它們關閉了看起來空閒的長期連線。
此問題的解決方案是 WebSocket 模擬——即,首先嚐試使用 WebSocket,然後在必要時回退到基於 HTTP 的技術,這些技術模仿 WebSocket 互動並暴露相同的應用層 API。
在 Servlet 棧上,Spring Framework 為 SockJS 協議提供了伺服器(以及客戶端)支援。
概述
SockJS 的目標是讓應用程式使用 WebSocket API,但在執行時必要時回退到非 WebSocket 替代方案,而無需更改應用程式程式碼。
SockJS 包括:
-
SockJS JavaScript 客戶端——用於瀏覽器中的客戶端庫。
-
SockJS 伺服器實現,包括 Spring Framework
spring-websocket模組中的一個。 -
spring-websocket模組中的 SockJS Java 客戶端(自 4.1 版起)。
SockJS 專為瀏覽器使用而設計。它使用各種技術來支援廣泛的瀏覽器版本。有關 SockJS 傳輸型別和瀏覽器的完整列表,請參閱 SockJS 客戶端頁面。傳輸分為三大類:WebSocket、HTTP 流和 HTTP 長輪詢。有關這些類別的概述,請參閱這篇部落格文章。
SockJS 客戶端首先發送 GET /info 以從伺服器獲取基本資訊。之後,它必須決定使用哪種傳輸。如果可能,將使用 WebSocket。如果不可能,在大多數瀏覽器中,至少有一個 HTTP 流選項。如果都沒有,則使用 HTTP(長)輪詢。
所有傳輸請求都具有以下 URL 結構:
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
其中:
-
{server-id}對於在叢集中路由請求很有用,但在其他情況下不使用。 -
{session-id}關聯屬於 SockJS 會話的 HTTP 請求。 -
{transport}指示傳輸型別(例如,websocket、xhr-streaming等)。
WebSocket 傳輸只需一個 HTTP 請求即可完成 WebSocket 握手。此後,所有訊息都在該套接字上交換。
HTTP 傳輸需要更多請求。例如,Ajax/XHR 流依賴於一個用於伺服器到客戶端訊息的長時間執行請求,以及用於客戶端到伺服器訊息的額外 HTTP POST 請求。長輪詢類似,只是它在每次伺服器到客戶端傳送後結束當前請求。
SockJS 添加了最小的訊息幀。例如,伺服器最初發送字母 o(“開啟”幀),訊息以 a["message1","message2"](JSON 編碼陣列)形式傳送,如果 25 秒內(預設)沒有訊息流,則傳送字母 h(“心跳”幀),以及傳送字母 c(“關閉”幀)以關閉會話。
要了解更多資訊,請在瀏覽器中執行一個示例並觀察 HTTP 請求。SockJS 客戶端允許固定傳輸列表,因此可以一次檢視一個傳輸。SockJS 客戶端還提供了一個除錯標誌,可在瀏覽器控制檯中啟用有用的訊息。在伺服器端,您可以為 org.springframework.web.socket 啟用 TRACE 日誌記錄。有關更多詳細資訊,請參閱 SockJS 協議敘述性測試。
啟用 SockJS
您可以透過配置啟用 SockJS,如以下示例所示:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
@Configuration
@EnableWebSocket
class WebSocketConfiguration : WebSocketConfigurer {
override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS()
}
@Bean
fun myHandler(): WebSocketHandler {
return MyHandler()
}
}
<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.docs.web.websocket.websocketserverhandler.MyHandler"/>
</beans>
上述示例用於 Spring MVC 應用程式,應包含在 DispatcherServlet 的配置中。然而,Spring 的 WebSocket 和 SockJS 支援不依賴於 Spring MVC。在 SockJsHttpRequestHandler 的幫助下,將其整合到其他 HTTP 服務環境相對簡單。
在瀏覽器端,應用程式可以使用 sockjs-client(版本 1.0.x)。它模擬 W3C WebSocket API 並與伺服器通訊,根據執行的瀏覽器選擇最佳傳輸選項。請參閱 sockjs-client 頁面以及瀏覽器支援的傳輸型別列表。客戶端還提供了幾個配置選項——例如,指定要包含哪些傳輸。
IE 8 和 9
Internet Explorer 8 和 9 仍在被使用。它們是使用 SockJS 的一個關鍵原因。本節涵蓋了在這些瀏覽器中執行的重要注意事項。
SockJS 客戶端透過使用 Microsoft 的 XDomainRequest 來支援 IE 8 和 9 中的 Ajax/XHR 流。這可以跨域工作,但不支援傳送 cookie。Cookie 對於 Java 應用程式通常至關重要。然而,由於 SockJS 客戶端可以與許多伺服器型別(不僅僅是 Java 伺服器)一起使用,它需要知道 cookie 是否重要。如果重要,SockJS 客戶端傾向於使用 Ajax/XHR 進行流傳輸。否則,它依賴於基於 iframe 的技術。
SockJS 客戶端的第一個 /info 請求是獲取可能影響客戶端傳輸選擇的資訊的請求。其中一個細節是伺服器應用程式是否依賴 cookie(例如,用於身份驗證目的或使用粘性會話進行叢集)。Spring 的 SockJS 支援包含一個名為 sessionCookieNeeded 的屬性。由於大多數 Java 應用程式依賴於 JSESSIONID cookie,因此預設啟用此屬性。如果您的應用程式不需要它,您可以關閉此選項,然後 SockJS 客戶端應在 IE 8 和 9 中選擇 xdr-streaming。
如果您確實使用基於 iframe 的傳輸,請記住,可以透過將 HTTP 響應頭 X-Frame-Options 設定為 DENY、SAMEORIGIN 或 ALLOW-FROM <origin> 來指示瀏覽器阻止在給定頁面上使用 IFrames。這用於防止點選劫持。
|
Spring Security 3.2+ 支援在每個響應上設定 |
如果您的應用程式添加了 X-Frame-Options 響應頭(應該如此!),並且依賴於基於 iframe 的傳輸,則需要將頭值設定為 SAMEORIGIN 或 ALLOW-FROM <origin>。Spring SockJS 支援還需要知道 SockJS 客戶端的位置,因為它從 iframe 載入。預設情況下,iframe 設定為從 CDN 位置下載 SockJS 客戶端。建議配置此選項以使用與應用程式同源的 URL。
以下示例展示瞭如何在 Java 配置中實現:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("https://:8080/myapp/js/sockjs-client.js");
}
// ...
}
XML 名稱空間透過 <websocket:sockjs> 元素提供了類似的選項。
在初始開發期間,請啟用 SockJS 客戶端 devel 模式,該模式可防止瀏覽器快取 SockJS 請求(如 iframe),否則這些請求將被快取。有關如何啟用它的詳細資訊,請參閱 SockJS 客戶端頁面。 |
心跳
SockJS 協議要求伺服器傳送心跳訊息,以防止代理認為連線已掛起。Spring SockJS 配置有一個名為 heartbeatTime 的屬性,您可以使用它來自定義頻率。預設情況下,如果該連線上沒有其他訊息傳送,則在 25 秒後傳送心跳。此 25 秒的值符合以下針對公共網際網路應用程式的 IETF 建議。
| 在使用 STOMP over WebSocket 和 SockJS 時,如果 STOMP 客戶端和伺服器協商交換心跳,則 SockJS 心跳將被停用。 |
Spring SockJS 支援還允許您配置 TaskScheduler 以排程心跳任務。任務排程器由一個執行緒池支援,預設設定基於可用處理器數量。您應該考慮根據您的具體需求自定義設定。
客戶端斷開連線
HTTP 流和 HTTP 長輪詢 SockJS 傳輸需要連線保持開啟狀態的時間比通常更長。有關這些技術的概述,請參閱這篇部落格文章。
在 Servlet 容器中,這是透過 Servlet 3 非同步支援完成的,該支援允許退出 Servlet 容器執行緒,處理請求,並從另一個執行緒繼續寫入響應。
一個具體的問題是 Servlet API 不提供客戶端斷開連線的通知。請參閱 eclipse-ee4j/servlet-api#44。但是,Servlet 容器在後續嘗試寫入響應時會引發異常。由於 Spring 的 SockJS 服務支援伺服器端心跳(預設每 25 秒),這意味著客戶端斷開連線通常在該時間段內檢測到(或者如果訊息傳送更頻繁,則更早)。
因此,客戶端斷開連線可能導致網路 I/O 故障,這會使日誌充滿不必要的堆疊跟蹤。Spring 盡力識別表示客戶端斷開連線的此類網路故障(特定於每個伺服器),並使用專用日誌類別 DISCONNECTED_CLIENT_LOG_CATEGORY(在 AbstractSockJsSession 中定義)記錄最少的訊息。如果您需要檢視堆疊跟蹤,可以將該日誌類別設定為 TRACE。 |
SockJS 和 CORS
如果您允許跨源請求(請參閱允許的來源),SockJS 協議在 XHR 流和輪詢傳輸中為跨域支援使用 CORS。因此,除非檢測到響應中存在 CORS 頭,否則會自動新增 CORS 頭。因此,如果應用程式已配置為提供 CORS 支援(例如,透過 Servlet 過濾器),Spring 的 SockJsService 將跳過此部分。
透過在 Spring 的 SockJsService 中設定 suppressCors 屬性,也可以停用這些 CORS 頭的新增。
SockJS 期望以下頭和值:
-
Access-Control-Allow-Origin:從Origin請求頭的值初始化。 -
Access-Control-Allow-Credentials:始終設定為true。 -
Access-Control-Request-Headers:從等效請求頭的值初始化。 -
Access-Control-Allow-Methods:傳輸支援的 HTTP 方法(請參閱TransportType列舉)。 -
Access-Control-Max-Age:設定為 31536000(1 年)。
有關確切實現,請參閱 AbstractSockJsService 中的 addCorsHeaders 和原始碼中的 TransportType 列舉。
或者,如果 CORS 配置允許,請考慮排除帶有 SockJS 端點字首的 URL,從而讓 Spring 的 SockJsService 處理它。
SockJsClient
Spring 提供了一個 SockJS Java 客戶端,用於連線遠端 SockJS 端點而無需使用瀏覽器。當兩個伺服器之間需要透過公共網路進行雙向通訊時(即,網路代理可能阻止使用 WebSocket 協議),這尤其有用。SockJS Java 客戶端對於測試目的也非常有用(例如,模擬大量併發使用者)。
SockJS Java 客戶端支援 websocket、xhr-streaming 和 xhr-polling 傳輸。其餘的僅適用於瀏覽器使用。
您可以使用以下方式配置 WebSocketTransport:
-
JSR-356 執行時中的
StandardWebSocketClient。 -
使用 Jetty 9+ 原生 WebSocket API 的
JettyWebSocketClient。 -
Spring 的
WebSocketClient的任何實現。
XhrTransport,根據定義,支援 xhr-streaming 和 xhr-polling,因為從客戶端的角度來看,除了用於連線伺服器的 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) (1)
.setHttpMessageCacheSize(1000) (2)
.setDisconnectDelay(30 * 1000); (3)
}
// ...
}
| 1 | 將 streamBytesLimit 屬性設定為 512KB(預設值為 128KB — 128 * 1024)。 |
| 2 | 將 httpMessageCacheSize 屬性設定為 1,000(預設值為 100)。 |
| 3 | 將 disconnectDelay 屬性設定為 30 秒(預設值為 5 秒 — 5 * 1000)。 |