SockJS Fallback
在公共Internet上,在你控制之外的限制性代理可能会妨碍WebSocket交互,要么是因为它们没有配置为传递Upgrade
header,要么是因为它们关闭了看起来空闲的长连接。
这个问题的解决方案是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结构:
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
-
{server-id}
用于在集群中路由请求,但不用于其他用途。 -
{session-id}
关联属于SockJS会话的HTTP请求。 -
{transport}
指示传输类型(例如websocket
、xhr-streaming
和其他)。
WebSocket传输只需要一个HTTP请求来完成WebSocket握手,此后所有消息都在该socket上交换。
HTTP传输需要更多的请求,例如,Ajax/XHR流依赖于一个对服务器到客户端消息的长时间运行的请求,以及对客户端到服务器消息的额外HTTP POST请求。长轮询与此类似,只是它在每个服务器到客户端发送后结束当前请求。
SockJS添加了最少的消息框架,例如,服务器最初发送字母o
(“open” frame),消息以["message1","message2"]
(json编码数组)的形式发送,如果在25秒内(默认情况下)没有消息流,则发送字母h
(”heartbeat” frame)和字母c
(”close” frame)来关闭会话。
要了解更多信息,请在浏览器中运行一个示例并观察HTTP请求,SockJS客户端允许修复传输列表,因此可以一次查看每个传输。SockJS客户端还提供了一个debug标志,它在浏览器控制台中启用有用的消息,在服务器端,你可以为org.springframework.web.socket
启用TRACE
日志记录,有关更多细节,请参阅SockJS协议叙述性测试。
启用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页面和浏览器支持的传输类型列表。客户端还提供了一些配置选项 – 例如,指定要包含哪些传输。
IE 8和9
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
如果允许跨源请求(请参阅允许的源),那么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
处理它。
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)
.setHttpMessageCacheSize(1000)
.setDisconnectDelay(30 * 1000);
}
// ...
}
- 将
streamBytesLimit
属性设置为512KB
(默认为128KB =>128 * 1024
)。 - 将
httpMessageCacheSize
属性设置为1000
(默认为100
)。 - 将
disconnectDelay
属性设置为30秒(默认为5秒 =>5 * 1000
)。