SockJS 備用方案

在公共網際網路上,您無法控制的限制性代理可能會阻止 WebSocket 互動,原因可能是它們未配置為傳遞 Upgrade 頭,或者它們關閉了看起來空閒的長期連線。

此問題的解決方案是 WebSocket 模擬——即,首先嚐試使用 WebSocket,然後在必要時回退到基於 HTTP 的技術,這些技術模仿 WebSocket 互動並暴露相同的應用層 API。

在 Servlet 棧上,Spring Framework 為 SockJS 協議提供了伺服器(以及客戶端)支援。

概述

SockJS 的目標是讓應用程式使用 WebSocket API,但在執行時必要時回退到非 WebSocket 替代方案,而無需更改應用程式程式碼。

SockJS 包括:

  • 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} 指示傳輸型別(例如,websocketxhr-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 設定為 DENYSAMEORIGINALLOW-FROM <origin> 來指示瀏覽器阻止在給定頁面上使用 IFrames。這用於防止點選劫持

Spring Security 3.2+ 支援在每個響應上設定 X-Frame-Options。預設情況下,Spring Security Java 配置將其設定為 DENY。在 3.2 中,Spring Security XML 名稱空間預設不設定該頭,但可以配置為設定。將來,它可能會預設設定。

有關如何配置 X-Frame-Options 頭設定的詳細資訊,請參閱 Spring Security 文件的預設安全頭。您還可以檢視 gh-2718 以獲取更多背景資訊。

如果您的應用程式添加了 X-Frame-Options 響應頭(應該如此!),並且依賴於基於 iframe 的傳輸,則需要將頭值設定為 SAMEORIGINALLOW-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 客戶端支援 websocketxhr-streamingxhr-polling 傳輸。其餘的僅適用於瀏覽器使用。

您可以使用以下方式配置 WebSocketTransport

  • JSR-356 執行時中的 StandardWebSocketClient

  • 使用 Jetty 9+ 原生 WebSocket API 的 JettyWebSocketClient

  • Spring 的 WebSocketClient 的任何實現。

XhrTransport,根據定義,支援 xhr-streamingxhr-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)。
© . This site is unofficial and not affiliated with VMware.