在公共Internet上,在你控制以外的限制性代理可能會妨礙WebSocket交互,要麼是由於它們沒有配置爲傳遞Upgrade
header,要麼是由於它們關閉了看起來空閒的長鏈接。php
這個問題的解決方案是WebSocket模擬 - 也就是說,首先嚐試使用WebSocket,而後轉而使用基於http的技術來模擬WebSocket交互並公開相同的應用程序級別的API。html
在Servlet堆棧上,Spring Framework爲SockJS協議提供了服務器(以及客戶端)支持。java
SockJS的目標是讓應用程序使用WebSocket API,但若是在運行時有必要,能夠回退到非WebSocket替代方案,而不須要修改應用程序代碼。git
SockJS包括:github
spring-websocket
模塊。spring-websocket
模塊中的SockJS Java客戶端(4.1版以來)。SockJS是爲瀏覽器設計的,它使用各類技術來支持各類瀏覽器版本,有關SockJS傳輸類型和瀏覽器的完整列表,請參閱SockJS客戶端頁面。傳輸分爲三大類:WebSocket、HTTP流媒體以及HTTP長輪詢,有關這些類別的概述,請參閱這篇博客文章。web
SockJS客戶端首先發送GET /info
以從服務器獲取基本信息,在那以後,它必須決定使用哪一種傳輸方式。若是可能的話,使用WebSocket,若是不是,在大多數瀏覽器中,至少有一個HTTP流媒體選項,若是還不是,則使用HTTP(長)輪詢。spring
全部傳輸請求都具備如下URL結構:json
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
{server-id}
用於在集羣中路由請求,但不用於其餘用途。{session-id}
關聯屬於SockJS會話的HTTP請求。{transport}
指示傳輸類型(例如websocket
、xhr-streaming
和其餘)。WebSocket傳輸只須要一個HTTP請求來完成WebSocket握手,此後全部消息都在該socket上交換。segmentfault
HTTP傳輸須要更多的請求,例如,Ajax/XHR流依賴於一個對服務器到客戶端消息的長時間運行的請求,以及對客戶端到服務器消息的額外HTTP POST請求。長輪詢與此相似,只是它在每一個服務器到客戶端發送後結束當前請求。api
SockJS添加了最少的消息框架,例如,服務器最初發送字母o
(「open」 frame),消息以["message1","message2"]
(json編碼數組)的形式發送,若是在25秒內(默認狀況下)沒有消息流,則發送字母h
("heartbeat" frame)和字母c
("close" frame)來關閉會話。
要了解更多信息,請在瀏覽器中運行一個示例並觀察HTTP請求,SockJS客戶端容許修復傳輸列表,所以能夠一次查看每一個傳輸。SockJS客戶端還提供了一個debug標誌,它在瀏覽器控制檯中啓用有用的消息,在服務器端,你能夠爲org.springframework.web.socket
啓用TRACE
日誌記錄,有關更多細節,請參閱SockJS協議敘述性測試。
你能夠經過Java配置啓用SockJS,以下面的示例所示:
@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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://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
的配置中,然而,Spring的WebSocket和SockJS支持並不依賴於Spring MVC,在SockJsHttpRequestHandler的幫助下集成到其餘HTTP服務環境中相對簡單。
在瀏覽器端,應用程序可使用sockjs-client(1.0.x版),它模擬W3C WebSocket API,並與服務器通訊,根據運行它的瀏覽器選擇最佳傳輸選項。請參閱sockjs-client頁面和瀏覽器支持的傳輸類型列表。客戶端還提供了一些配置選項 - 例如,指定要包含哪些傳輸。
Internet Explorer 8和9仍在使用中,它們是有SockJS的關鍵緣由,本節討論在這些瀏覽器中運行的重要注意事項。
SockJS客戶端使用微軟的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客戶端應該選擇IE8和IE9中的xdr-streaming
。
若是你確實使用基於iframe的傳輸,請記住,能夠經過設置HTTP響應頭X-Frame-Options
爲DENY
、SAMEORIGIN
或ALLOW-FROM <origin>
來指示瀏覽器阻止在給定頁面上使用IFrames,這是用來防止點擊劫持。
Spring Security 3.2+支持在每一個響應上設置X-Frame-Options
,默認狀況下,Spring Security Java配置將其設置爲DENY
,在3.2中,Spring Security XML命名空間默認不設置該header,但能夠配置爲這樣作,未來,它可能會默認設置它。有關如何配置
X-Frame-Options
header設置的詳細信息,請參閱Spring Security文檔的默認Security Headers,你還能夠查看SEC-2501瞭解其餘背景信息。
若是你的應用程序添加了X-Frame-Options
響應header(它應該這樣作!)並依賴於基於iframe的傳輸,你須要將header值設置爲SAMEORIGIN
或ALLOW-FROM <origin>
。Spring SockJS支持還須要知道SockJS客戶端的位置,由於它是從iframe加載的,默認狀況下,iframe被設置爲從CDN位置下載SockJS客戶端,將此選項配置爲使用與應用程序相同源的URL是個好主意。
下面的示例展現瞭如何在Java配置中實現這一點:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS() .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js"); } // ... }
XML命名空間經過<websocket:sockjs>
元素提供了相似的選項。
在初始開發期間,啓用SockJS客戶端
devel
模式,以防止瀏覽器緩存SockJS請求(好比iframe),不然會被緩存,有關如何啓用它的詳細信息,請參閱
SockJS客戶端頁面。
SockJS協議要求服務器發送心跳消息,以防止代理判定鏈接掛起,Spring SockJS配置有一個名爲heartbeatTime
的屬性,你可使用它來定製頻率。默認狀況下,在25秒後發送心跳,假設在該鏈接上沒有發送其餘消息,這個25秒的值符合如下對公共互聯網應用程序的IETF建議。
在WebSocket和SockJS上使用STOMP時,若是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來支持XHR流和輪詢傳輸中的跨域支持,所以,除非檢測到響應中存在CORS headers,不然將自動添加CORS headers,所以,若是應用程序已經配置爲提供CORS支持(例如,經過Servlet過濾器),Spring的SockJsService
就跳過了這一部分。
還能夠經過在Spring的SockJsService中設置suppressCors
屬性來禁用這些CORS headers的添加。
SockJS指望如下headers和值:
Access-Control-Allow-Origin
:從Origin
請求header的值初始化。Access-Control-Allow-Credentials
:老是設爲true
。Access-Control-Request-Headers
:從等效請求header的值初始化。Access-Control-Allow-Methods
:傳輸支持的HTTP方法(參見TransportType
枚舉)。Access-Control-Max-Age
:設置爲31536000
(1年)。有關確切的實現,請參閱源代碼中AbstractSockJsService
和TransportType
枚舉中的addCorsHeaders
。
或者,若是CORS配置容許,考慮使用SockJS端點前綴排除URL,從而讓Spring的SockJsService
處理它。
Spring提供了一個SockJS Java客戶端來鏈接到遠程SockJS端點,而無需使用瀏覽器,當須要經過公共網絡在兩臺服務器之間進行雙向通訊時(也就是說,網絡代理可能會阻止WebSocket協議的使用),這一點尤爲有用。對於測試目的(例如,模擬大量併發用戶),SockJS Java客戶端也很是有用。
SockJS Java客戶端支持websocket
、xhr-streaming
和xhr-polling
傳輸,剩下的一個只有在瀏覽器中使用纔有意義。
你能夠配置WebSocketTransport
使用:
StandardWebSocketClient
。JettyWebSocketClient
。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) .setHttpMessageCacheSize(1000) .setDisconnectDelay(30 * 1000); } // ... }
streamBytesLimit
屬性設置爲512KB
(默認爲128KB => 128 * 1024
)。httpMessageCacheSize
屬性設置爲1000
(默認爲100
)。disconnectDelay
屬性設置爲30秒(默認爲5秒 => 5 * 1000
)。