这部分参考文档包括对Servlet堆栈的支持,包括原始WebSocket交互的WebSocket消息传递,通过SockJS的WebSocket仿真,以及通过STOMP作为WebSocket上的子协议的pub-sub消息传递。
4.1。介绍
WebSocket协议RFC 6455提供了一种标准化方法,可通过单个TCP连接在客户端和服务器之间建立全双工双向通信通道。它是来自HTTP的不同TCP协议,但设计为使用端口80和443通过HTTP工作,并允许重用现有防火墙规则。
WebSocket交互以HTTP请求开始,该HTTP请求使用HTTP "Upgrade"
标头升级,或者在这种情况下切换到WebSocket协议:
GET / spring-websocket-portfolio / portfolio HTTP / 1.1 主持人:localhost:8080 升级:websocket 连接:升级 Sec-WebSocket-Key:Uc9l9TMkWGbHFD2qnFHltg == Sec-WebSocket-Protocol:v10.stomp,v11.stomp Sec-WebSocket-Version:13 来源:http:// localhost:8080
具有WebSocket支持的服务器返回:而不是通常的200状态代码:
HTTP / 1.1 101交换协议 升级:websocket 连接:升级 Sec-WebSocket-Accept:1qVdfYHU9hPOl4JYYNXF623Gzn0 = Sec-WebSocket-Protocol:v10.stomp
成功握手后,HTTP升级请求下的TCP套接字保持打开状态,客户端和服务器都可以继续发送和接收消息。
有关WebSockets如何工作的完整介绍超出了本文档的范围。请阅读RFC 6455,HTML5的WebSocket章节,或Web上的许多介绍和教程之一。
请注意,如果WebSocket服务器在Web服务器(例如nginx)后面运行,您可能需要将其配置为将WebSocket升级请求传递到WebSocket服务器。同样,如果应用程序在云环境中运行,请检查与WebSocket支持相关的云提供程序的说明。
4.1.1。HTTP vs WebSocket
即使WebSocket被设计为HTTP兼容并以HTTP请求开始,但重要的是要理解这两种协议会导致非常不同的体系结构和应用程序编程模型。
在HTTP和REST中,应用程序被建模为多个URL。要与应用程序客户端进行交互,请访问这些URL,请求 - 响应样式。服务器根据HTTP URL,方法和标头将请求路由到适当的处理程序。
相比之下,在WebSockets中,通常只有一个URL用于初始连接,随后所有应用程序消息都在同一TCP连接上流动。这指向完全不同的异步,事件驱动的消息传递体系结构。
WebSocket也是一种低级传输协议,与HTTP不同,它没有规定消息内容的任何语义。这意味着除非客户端和服务器就消息语义达成一致,否则无法路由或处理消息。
WebSocket客户端和服务器可以通过"Sec-WebSocket-Protocol"
HTTP握手请求上的标头协商使用更高级别的消息传递协议(例如STOMP),或者在没有它们的情况下,他们需要提出自己的约定。
4.1.2。什么时候用?
WebSockets可以使网页动态和交互。但是,在许多情况下,Ajax和HTTP流和/或长轮询的组合可以提供简单有效的解决方案。
例如,新闻,邮件和社交订阅源需要动态更新,但每隔几分钟就可以完全正常更新。另一方面,协作,游戏和财务应用程序需要更接近实时。
仅延迟不是决定因素。如果消息量相对较低(例如,监视网络故障),则HTTP流式传输或轮询可以提供有效的解决方案。它是低延迟,高频率和高容量的组合,是使用WebSocket的最佳选择。
还要记住,通过Internet,控制之外的限制性代理可能会阻止WebSocket交互,因为它们未配置为传递 Upgrade
标头,或者因为它们关闭看似空闲的长期连接?这意味着将WebSocket用于防火墙内的内部应用程序比面向公众的应用程序更直接。
4.2。WebSocket API
Spring Framework提供了一个WebSocket API,可用于编写处理WebSocket消息的客户端和服务器端应用程序。
4.2.1。WebSocketHandler
创建的WebSocket服务器是为实现简单WebSocketHandler
或更可能要么延长TextWebSocketHandler
或BinaryWebSocketHandler
:
`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-config和XML名称空间支持,用于将上述WebSocket处理程序映射到特定的URL:
`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配置等效:
`
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
`
以上内容适用于Spring MVC应用程序,应包含在DispatcherServlet的配置中。但是,Spring的WebSocket支持不依赖于Spring MVC。WebSocketHandler
在WebSocketHttpRequestHandler的帮助下将其集成到其他HTTP服务环境中 相对简单。
4.2.2。WebSocket握手
自定义初始HTTP WebSocket握手请求的最简单方法是通过a HandshakeInterceptor
,它暴露握手方法的“之前”和“之后”。这样的拦截器可用于阻止握手或使任何属性可用于WebSocketSession
。例如,有一个内置拦截器,用于将HTTP会话属性传递给WebSocket会话:
`@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer {
@Override
**public** **void** registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(**new** MyHandler(), "/myHandler")
.addInterceptors(**new** HttpSessionHandshakeInterceptor());
}
}`
并且XML配置等效:
`
<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"/>
`
更高级的选项是扩展DefaultHandshakeHandler
执行WebSocket握手步骤,包括验证客户端来源,协商子协议等。如果应用程序需要配置自定义RequestUpgradeStrategy
以适应WebSocket服务器引擎和尚不支持的版本,则应用程序可能还需要使用此选项(有关此主题的更多信息,请参阅部署)。Java-config和XML命名空间都可以配置自定义 HandshakeHandler
。
Spring提供了一个WebSocketHandlerDecorator
可用于装饰WebSocketHandler
附加行为的基类。使用WebSocket Java-config或XML命名空间时,默认情况下会提供并添加日志记录和异常处理实现。在ExceptionWebSocketHandlerDecorator
捕获所有捕获的异常任何WebSocketHandler方法所产生的,并与状态关闭WebSocket的会议1011
指示服务器错误。
4.2.3。部署
Spring WebSocket API易于集成到Spring MVC应用程序中,其中DispatcherServlet
既提供HTTP WebSocket握手,也提供其他HTTP请求。通过调用也可以轻松地集成到其他HTTP处理场景中WebSocketHttpRequestHandler
。这很方便易懂。但是,特殊注意事项适用于JSR-356运行时。
Java WebSocket API(JSR-356)提供了两种部署机制。第一个涉及启动时的Servlet容器类路径扫描(Servlet 3功能); 另一个是在Servlet容器初始化时使用的注册API。这些机制都不能使用单个“前端控制器”进行所有HTTP处理 - 包括WebSocket握手和所有其他HTTP请求 - 例如Spring MVC DispatcherServlet
。
这是JSR-356的一个重要限制,Spring的WebSocket支持RequestUpgradeStrategy
即使在JSR-356运行时运行时也能解决特定于服务器的问题。目前,Tomcat,Jetty,GlassFish,WebLogic,WebSphere和Undertow(以及WildFly)都有这样的策略。
已经创建了一个克服Java WebSocket API中的上述限制的请求,可以在WEBSOCKET_SPEC-211上进行跟踪 。Tomcat,Undertow和WebSphere提供了自己的API替代方案,使其成为可能,而Jetty也是如此。我们希望更多服务器也会这样做。
第二个考虑因素是具有JSR-356支持的Servlet容器应该执行ServletContainerInitializer
(SCI)扫描,这可能会减慢应用程序启动速度,在某些情况下会显着降低。如果在升级到支持JSR-356的Servlet容器版本后观察到重大影响,则应该可以通过使用以下<absolute-ordering />
元素选择性地启用或禁用Web片段(和SCI扫描)web.xml
:
`
<absolute-ordering/>
`
然后,您可以根据需要有选择地启用Web片段,例如Spring自己 SpringServletContainerInitializer
提供对Servlet 3 Java初始化API的支持(如果需要):
`
<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>
`
4.2.4。服务器配置
每个底层WebSocket引擎都公开控制运行时特性的配置属性,例如消息缓冲区大小,空闲超时等。
对于Tomcat,WildFly和GlassFish ServletServerContainerFactoryBean
,在WebSocket Java配置中添加一个:
`@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer {
@Bean
**public** ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = **new** ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
**return** container;
}
}`
或WebSocket XML命名空间:
`
<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>
`
对于客户端WebSocket配置,您应该使用WebSocketContainerFactoryBean
(XML)或ContainerProvider.getWebSocketContainer()
(Java配置)。
对于Jetty,您需要提供预配置的Jetty WebSocketServerFactory
并DefaultHandshakeHandler
通过WebSocket Java配置将其插入Spring :
`@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)));
}
}`
或WebSocket XML命名空间:
`
<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>
`
4.2.5。允许来源
从Spring Framework 4.1.5开始,WebSocket和SockJS的默认行为是仅接受_相同的原始_请求。也可以允许_所有_或指定的起源列表。此检查主要是为浏览器客户端设计的。没有什么可以阻止其他类型的客户端修改Origin
标头值(有关更多详细信息,请参阅 RFC 6454:Web Origin Concept)。
3种可能的行为是:
仅允许相同的原始请求(默认):在此模式下,启用SockJS时,Iframe HTTP响应标头
X-Frame-Options
设置为SAMEORIGIN
,并且JSONP传输被禁用,因为它不允许检查请求的来源。因此,启用此模式时不支持IE6和IE7。允许指定的原始列表:每个提供的_允许来源_必须以
http://
或开头https://
。在此模式下,启用SockJS时,将禁用基于IFrame和JSONP的传输。因此,启用此模式时,不支持IE6到IE9。允许所有来源:要启用此模式,您应该提供
*
允许的原始值。在此模式下,所有传输都可用。
WebSocket和SockJS允许的起源可以配置如下所示:
`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("http://mydomain.com");
}
@Bean
**public** WebSocketHandler myHandler() {
**return** **new** MyHandler();
}
}`
XML配置等效:
`
<websocket:handlers allowed-origins="http://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
`
4.3。SockJS后备
在公共Internet上,受控制之外的限制性代理可能会阻止WebSocket交互,因为它们未配置为传递Upgrade
标头,或者因为它们关闭看似空闲的长期连接。
这个问题的解决方案是WebSocket仿真,即首先尝试使用WebSocket,然后依靠基于HTTP的技术来模拟WebSocket交互并公开相同的应用程序级API。
在Servlet堆栈上,Spring Framework为SockJS协议提供服务器(以及客户端)支持。
4.3.1。概观
SockJS的目标是让应用程序使用WebSocket API,但在运行时必要时可以回退到非WebSocket替代品,即无需更改应用程序代码。
SockJS包括:
该SockJS JavaScript客户端 -在浏览器中使用客户端库。
SockJS服务器实现,包括Spring Framework
spring-websocket
模块中的一个。从4.1开始
spring-websocket
还提供了一个SockJS Java客户端。
SockJS专为在浏览器中使用而设计。它竭尽全力使用各种技术支持各种浏览器版本。有关SockJS传输类型和浏览器的完整列表,请参阅SockJS客户端页面。传输分为3大类:WebSocket,HTTP Streaming和HTTP Long Polling。有关这些类别的概述,请参阅 此博客文章。
SockJS客户端首先发送"GET /info"
以从服务器获取基本信息。之后,它必须决定使用什么传输。如果可能,使用WebSocket。如果没有,在大多数浏览器中至少有一个HTTP流选项,如果没有,则使用HTTP(长)轮询。
所有传输请求都具有以下URL结构:
HTTP://主机:端口/对myApp / myEndpoint / {服务器ID} / {会话ID} / {}运输
{server-id}
- 对于在群集中路由请求有用,但在其他情况下不使用。{session-id}
- 关联属于SockJS会话的HTTP请求。{transport}
- 表示传输类型,例如“websocket”,“xhr-streaming”等。
WebSocket传输只需要一个HTTP请求即可进行WebSocket握手。之后的所有消息都在该套接字上交换。
HTTP传输需要更多请求。例如,Ajax / XHR流依赖于一个长期运行的服务器到客户端消息请求以及针对客户端到服务器消息的额外HTTP POST请求。长轮询是类似的,除了它在每个服务器到客户端发送之后结束当前请求。
SockJS增加了最小的消息框架。例如,服务器最初发送字母o(“打开”帧),消息作为[“message1”,“message2”](JSON编码数组)发送,字母h(“心跳”帧)如果没有消息流默认为25秒,字母c(“关闭”框架)关闭会话。
要了解更多信息,请在浏览器中运行示例并观察HTTP请求。SockJS客户端允许修复传输列表,因此可以一次查看每个传输。SockJS客户端还提供了一个调试标志,可在浏览器控制台中启用有用的消息。在服务器端启用 TRACE
日志记录org.springframework.web.socket
。有关更多详细信息,请参阅SockJS协议 叙述测试。
4.3.2。启用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配置等价:
`
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
`
以上内容适用于Spring MVC应用程序,应包含在DispatcherServlet的配置中。但是,Spring的WebSocket和SockJS支持并不依赖于Spring MVC。在SockJsHttpRequestHandler的帮助下,集成到其他HTTP服务环境中相对简单 。
在浏览器端,应用程序可以使用模拟W3C WebSocket API 的 sockjs-client(版本1.0.x)并与服务器通信,以根据其运行的浏览器选择最佳传输选项。查看 sockjs-client页面和浏览器支持的传输类型列表。客户端还提供了几个配置选项,例如,指定要包含的传输。
4.3.3。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的技术。
最先"/info"
从SockJS客户端请求是针对可能影响客户的传输选择信息的请求。其中一个细节是服务器应用程序是否依赖于cookie,例如用于身份验证或使用粘性会话进行群集。Spring的SockJS支持包括一个名为的属性sessionCookieNeeded
。默认情况下启用它,因为大多数Java应用程序都依赖于JSESSIONID
cookie。如果您的应用程序不需要它,您可以关闭此选项,SockJS客户端应xdr-streaming
在IE 8和9中选择。
如果你使用基于iframe的运输,并且在任何情况下,这是好事,知道浏览器可以指示通过设置HTTP响应标题来阻止特定网页上的使用iframe X-Frame-Options
来DENY
, SAMEORIGIN
或ALLOW-FROM <origin>
。这用于防止 点击劫持。
Spring Security 3.2+支持设置X-Frame-Options
每个响应。默认情况下,Spring Security Java配置将其设置为DENY
。在3.2中,Spring Security XML命名空间默认情况下不设置该标头,但可以配置为执行此操作,并且将来可以默认设置它。
见7.1节。 有关如何配置X-Frame-Options
标头设置的详细信息,请参阅Spring Security文档的“默认安全标头”。您还可以查看或观看 SEC-2501以获取更多背景信息。
如果您的应用程序添加X-Frame-Options
响应标头(应该!)并依赖于基于iframe的传输,则需要将标头值设置为 SAMEORIGIN
或ALLOW-FROM <origin>
。除此之外,Spring SockJS支持还需要知道SockJS客户端的位置,因为它是从iframe加载的。默认情况下,iframe设置为从CDN位置下载SockJS客户端。最好将此选项配置为与应用程序源相同的URL。
在Java配置中,这可以如下所示完成。XML命名空间通过<websocket:sockjs>
元素提供类似的选项:
`@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");
}
_// ..._
}`
在初始开发期间,请启用SockJS客户端devel
模式,以防止浏览器缓存否则将被缓存的SockJS请求(如iframe)。有关如何启用它的详细信息,请参阅 SockJS客户端页面。
4.3.4。心跳
SockJS协议要求服务器发送心跳消息以阻止代理断定连接挂起。Spring SockJS配置有一个名为的属性heartbeatTime
,可用于自定义频率。默认情况下,假设在该连接上没有发送其他消息,则在25秒后发送心跳。此25秒值符合以下 IETF对公共Internet应用程序的建议。
在WebSocket / SockJS上使用STOMP时,如果STOMP客户端和服务器协商要交换的心跳,则会禁用SockJS心跳。
Spring SockJS支持还允许配置TaskScheduler
用于调度心跳任务。任务计划程序由线程池支持,默认设置基于可用处理器的数量。应用程序应考虑根据其特定需求自定义设置。
4.3.5。客户端断开连接
HTTP流式传输和HTTP长轮询SockJS传输要求连接保持打开时间比平时长。有关这些技术的概述,请参阅 此博客文章。
在Servlet容器中,这是通过Servlet 3异步支持完成的,它允许退出Servlet容器线程处理请求并继续写入来自另一个线程的响应。
一个特定的问题是Servlet API不为已经消失的客户端提供通知,请参阅SERVLET_SPEC-44。但是,Servlet容器在后续尝试写入响应时引发异常。由于Spring的SockJS服务支持服务器发送的心跳(默认情况下每25秒),这意味着如果更频繁地发送消息,通常会在该时间段或更早的时间内检测到客户端断开连接。
因此,网络IO故障可能仅仅因为客户端已断开连接而发生,这可能会使用不必要的堆栈跟踪填充日志。Spring尽最大努力识别代表客户端断开连接(特定于每个服务器)的网络故障,并使用中DISCONNECTED_CLIENT_LOG_CATEGORY
定义的专用日志类别记录最小消息AbstractSockJsSession
。如果需要查看堆栈跟踪,请将该日志类别设置为TRACE。
4.3.6。SockJS和CORS
如果允许跨源请求(请参阅允许的来源),则SockJS协议使用CORS在XHR流和轮询传输中进行跨域支持。因此,除非检测到响应中存在CORS头,否则将自动添加CORS头。因此,如果应用程序已配置为提供CORS支持,例如通过Servlet过滤器,Spring的SockJsService将跳过此部分。
也可以通过suppressCors
Spring的SockJsService中的属性禁用这些CORS头 的添加。
以下是SockJS预期的标题和值列表:
"Access-Control-Allow-Origin"
- 从“Origin”请求标头的值初始化。"Access-Control-Allow-Credentials"
- 始终设置为true
。"Access-Control-Request-Headers"
- 从等效请求标头中的值初始化。"Access-Control-Allow-Methods"
- 传输支持的HTTP方法(参见TransportType
enum)。"Access-Control-Max-Age"
- 设置为31536000(1年)。
对于确切的执行看到addCorsHeaders
的AbstractSockJsService
还有TransportType
在源代码中枚举。
或者,如果CORS配置允许它考虑使用SockJS端点前缀排除URL,从而让Spring SockJsService
处理它。
4.3.7。SockJsClient
提供了SockJS Java客户端,以便在不使用浏览器的情况下连接到远程SockJS端点。当需要通过公共网络在2个服务器之间进行双向通信时,即在网络代理可能妨碍使用WebSocket协议的情况下,这尤其有用。SockJS Java客户端对于测试目的也非常有用,例如模拟大量并发用户。
SockJS Java客户端支持“websocket”,“xhr-streaming”和“xhr-polling”传输。其余的仅适用于浏览器。
的WebSocketTransport
可被配置成与:
StandardWebSocketClient
在JSR-356运行时中JettyWebSocketClient
使用Jetty 9+本机WebSocket APISpring的任何实现
WebSocketClient
根据XhrTransport
定义,支持“xhr-streaming”和“xhr-polling”,因为从客户端的角度来看,除了用于连接服务器的URL之外没有其他区别。目前有两种实现方式:
RestTemplateXhrTransport
使用Spring的RestTemplate
HTTP请求。JettyXhrTransport
使用Jetty的HttpClient
HTTP请求。
下面的示例显示了如何创建SockJS客户端并连接到SockJS端点:
`List
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);
}
_// ..._
}`
4.4。STOMP
WebSocket协议定义了两种类型的消息,文本和二进制,但它们的内容是未定义的。定义了客户端和服务器协商子协议的机制 - 即更高级别的消息传递协议,在WebSocket之上使用以定义每个消息可以发送什么类型的消息,每个消息的格式和内容是什么,等等上。子协议的使用是可选的,但无论是客户端还是服务器都需要就定义消息内容的某些协议达成一致。
4.4.1。概观
STOMP是一种简单的,面向文本的消息传递协议,最初是为Ruby,Python和Perl等脚本语言创建的,用于连接企业消息代理。它旨在解决常用消息传递模式的最小子集。STOMP可用于任何可靠的双向流网络协议,如TCP和WebSocket。虽然STOMP是面向文本的协议,但消息有效负载可以是文本或二进制。
STOMP是一种基于帧的协议,其帧在HTTP上建模。STOMP框架的结构:
命令 头1:VALUE1 标题2:VALUE2
车身^ @
客户端可以使用SEND或SUBSCRIBE命令发送或订阅消息以及“目标”标头,该标头描述消息的内容以及应由谁接收消息。这启用了一个简单的发布 - 订阅机制,可用于通过代理将消息发送到其他连接的客户端,或者向服务器发送消息以请求执行某些工作。
使用Spring的STOMP支持时,Spring WebSocket应用程序充当客户端的STOMP代理。消息被路由到[@Controller](https://my.oschina.net/u/1774615)
消息处理方法或简单的内存中间代理,该代理跟踪订阅并向订阅用户广播消息。您还可以将Spring配置为使用专用的STOMP代理(例如RabbitMQ,ActiveMQ等)来实现消息的实际广播。在这种情况下,Spring维护与代理的TCP连接,向其中继消息,并将消息从其传递到连接的WebSocket客户端。因此,Spring Web应用程序可以依赖于基于HTTP的统一安全性,通用验证以及熟悉的编程模型消息处理工作。
以下是订阅接收股票报价的客户的示例,服务器可以例如通过计划任务通过a SimpMessagingTemplate
向经纪人发送消息来周期性地发出报价:
订阅 ID:分1 目的地:/topic/price.stock.*
^ @
以下是客户端发送交易请求的示例,服务器可以通过该@MessageMapping
方法处理该交易请求,之后,在执行之后,向客户端广播交易确认消息和详细信息:
发送 目的地:/队列/贸易 内容类型:application / JSON 内容长度:44
{ “动作”: “买入”, “股票”: “MMM”, “股份”,44} ^ @
在STOMP规范中故意将目的地的含义保持不透明。它可以是任何字符串,完全取决于STOMP服务器,以定义它们支持的目标语义和语法。然而,很常见的是,目标是类似路径的字符串,其中"/topic/.."
暗示发布 - 订阅(_一对多_)并且"/queue/"
暗示点对点(_一对一_)消息交换。
STOMP服务器可以使用MESSAGE命令向所有订户广播消息。以下是服务器向订阅客户端发送股票报价的示例:
信息 消息ID:nxahklf6-1 订阅:分1 目的地:/topic/price.stock.MMM
{ “股票”: “MMM”, “价格”:129.45} ^ @
知道服务器无法发送未经请求的消息非常重要。来自服务器的所有消息必须响应特定的客户端订阅,并且服务器消息的“subscription-id”头必须与客户端订阅的“id”头匹配。
以上概述旨在提供对STOMP协议的最基本的了解。建议完整地查看协议 规范。
4.4.2。优点
使用STOMP作为子协议使Spring Framework和Spring Security能够提供比使用原始WebSocket更丰富的编程模型。关于HTTP与原始TCP的关系以及它如何使Spring MVC和其他Web框架能够提供丰富的功能,可以做出同样的观点。以下是一系列好处:
无需发明自定义消息传递协议和消息格式。
可以使用STOMP客户端,包括 Spring Framework中的Java客户端。
可以使用诸如RabbitMQ,ActiveMQ等消息代理(可选)来管理订阅和广播消息。
应用程序逻辑可以
[@Controller](https://my.oschina.net/u/1774615)
根据STOMP目标标头与处理原始WebSocket消息的任意数量的消息和消息进行组织,其中单个WebSocketHandler
用于给定连接。使用Spring Security根据STOMP目标和消息类型保护消息。
4.4.3。启用STOMP
STOMP在WebSocket的支持是可用的spring-messaging
和 spring-websocket
模块。一旦拥有了这些依赖项,就可以通过带有SockJS Fallback的 WebSocket公开STOMP端点,如下所示:
`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) {
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
**public** **void** configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app");
config.enableSimpleBroker("/topic", "/queue");
}
}`
"/portfolio"
是WebSocket(或SockJS)客户端需要连接到的端点的HTTP URL,以进行WebSocket握手。
目标头开头的STOMP消息"/app"
将路由到 类中的@MessageMapping
方法[@Controller](https://my.oschina.net/u/1774615)
。
使用内置的消息代理进行订阅和广播; 将目标标头以“/ topic”或“/ queue”开头的邮件路由到代理。
XML中的相同配置:
`
<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>
`
对于内置的简单代理,“/ topic”和“/ queue”前缀没有任何特殊含义。它们仅仅是区分pub-sub和点对点消息传递的惯例(即许多订阅者与一个消费者)。使用外部代理时,请检查代理的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
标题。即使它确实如此,它们也会在服务器端被忽略或被覆盖。有关身份验证的详细信息,请参阅“ 连接到代理和 身份验证”部分。
有关更多示例代码,请参阅:
股票组合样本申请。
4.4.4。消息流
一旦暴露了STOMP端点,Spring应用程序就成为连接客户端的STOMP代理。本节介绍服务器端的消息流。
该spring-messaging
模块包含对源自Spring Integration的消息传递应用程序的基础支持,后来被提取并整合到Spring Framework中,以便在许多Spring项目和应用程序场景中得到更广泛的使用 。下面列出了一些可用的消息传递抽象:
消息 - 包含标头和有效负载的消息的简单表示。
MessageHandler - 处理消息的合同。
MessageChannel - 发送消息的合同,该消息支持生产者和消费者之间的松散耦合。
SubscribableChannel -
MessageChannel
与MessageHandler
订阅者。ExecutorSubscribableChannel -
SubscribableChannel
使用的Executor
邮件传递。
Java配置(即@EnableWebSocketMessageBroker
)和XML命名空间配置(即<websocket:message-broker>
)都使用上述组件来组装消息工作流。下图显示了启用简单的内置消息代理时使用的组件:
上图中有3个消息通道:
"clientInboundChannel"
- 用于传递从WebSocket客户端收到的消息。"clientOutboundChannel"
- 用于向WebSocket客户端发送服务器消息。"brokerChannel"
- 用于从服务器端的应用程序代码向消息代理发送消息。
下图显示了配置外部代理(例如RabbitMQ)以管理订阅和广播消息时使用的组件:
上图中的主要区别是使用“代理中继”通过TCP将消息传递到外部STOMP代理,以及将消息从代理传递到订阅的客户端。
当从WebSocket connectin接收消息时,它们被解码为STOMP帧,然后变成Spring Message
表示,并发送到"clientInboundChannel"
进行进一步处理。例如STOMP消息,其目的地标题开头"/app"
可被路由到@MessageMapping
在注释的控制器的方法,而"/topic"
和"/queue"
消息可以被直接路由到消息代理。
@Controller
从客户端处理STOMP消息的带注释的消息可以通过消息代理向消息代理发送消息"brokerChannel"
,并且代理将通过消息向匹配的订阅者广播消息"clientOutboundChannel"
。相同的控制器也可以响应HTTP请求执行相同的操作,因此客户端可以执行HTTP POST,然后@PostMapping
方法可以向消息代理发送消息以向订阅的客户端广播。
让我们通过一个简单的例子来追踪流程。鉴于以下服务器设置:
`@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;
}
}`
客户端连接到,
"http://localhost:8080/portfolio"
并且一旦建立了WebSocket连接,STOMP帧就开始在其上流动。客户端发送带有目标头的SUBSCRIBE帧
"/topic/greeting"
。一旦接收并解码,消息就被发送到"clientInboundChannel"
,然后路由到存储客户端订阅的消息代理。客户端发送SEND帧
"/app/greeting"
。该"/app"
前缀有助于它的路线注解控制器。"/app"
删除前缀后,目标的剩余"/greeting"
部分将映射到@MessageMapping
方法中GreetingController
。返回的值
GreetingController
将转换为SpringMessage
,其有效负载基于返回值和默认目标标头"/topic/greeting"
(从输入目标派生,"/app"
替换为"/topic"
)。生成的消息将发送到“brokerChannel”并由消息代理处理。消息代理找到所有匹配的订户,并通过
"clientOutboundChannel"
from将消息编码为STOMP帧并在WebSocket连接上发送的每个发送MESSAGE帧。
下一节提供了有关注释方法的更多详细信息,包括支持的参数类型和返回值。
4.4.5。带注释的控制器
应用程序可以使用带注释的@Controller
类来处理来自客户端的消息。这些类可以声明@MessageMapping
,@SubscribeMapping
和@ExceptionHandler
方法,如下所述。
@MessageMapping
该@MessageMapping
批注可在方法基于他们的目的地将消息路由使用。它在方法级别和类型级别受支持。在类型级别@MessageMapping
用于表示控制器中所有方法的共享映射。
默认情况下,目标映射应为Ant样式的路径模式,例如“/ foo *”,“/ foo / **”。模式包括对模板变量的支持,例如“/ foo / {id}”,可以使用@DestinationVariable
方法参数引用。
应用程序可以选择切换到以点分隔的目标约定。请参见Dot as Separator。
@MessageMapping
方法可以使用以下参数进行灵活签名:
方法参数
描述
Message
用于访问完整的消息。
MessageHeaders
用于访问内部的标头Message
。
MessageHeaderAccessor
,SimpMessageHeaderAccessor
,StompHeaderAccessor
用于通过类型化访问器方法访问标头。
@Payload
用于访问消息的有效负载,通过配置转换(例如,从JSON)MessageConverter
。
不需要存在此注释,因为如果没有其他参数匹配,则默认采用它。
Payload参数可以使用@javax.validation.Valid
或Spring 注释,@Validated
以便自动验证。
@Header
用于访问特定标头值以及使用org.springframework.core.convert.converter.Converter
必要时的类型转换 。
@Headers
用于访问消息中的所有标头。此参数必须可分配给java.util.Map
。
@DestinationVariable
用于访问从消息目标中提取的模板变量。必要时,值将转换为声明的方法参数类型。
java.security.Principal
反映WebSocket HTTP握手时登录的用户。
当@MessageMapping
方法返回一个值时,默认情况下,该值通过已配置的序列化为有效负载MessageConverter
,然后作为a发送Message
到 "brokerChannel"
它向订阅者广播的位置。出站消息的目的地与入站消息的目的地相同,但前缀为"/topic"
。
您可以使用@SendTo
方法批注来自定义要将有效负载发送到的目标。@SendTo
也可以在类级别使用以共享发送消息的默认目标目标。@SendToUser
是仅向与消息关联的用户发送消息的变体。有关详细信息,请参阅用户目标
方法的返回值@MessageMapping
可以用ListenableFuture
, 包装CompletableFuture
,或者CompletionStage
以异步方式生成有效负载。
作为从@MessageMapping
方法返回有效负载的替代方法,您还可以使用the发送消息SimpMessagingTemplate
,这也是在封面下处理返回值的方式。请参阅发送消息。
@SubscribeMapping
的@SubscribeMapping
注释结合使用@MessageMapping
,以缩小的映射到订阅消息。在这种情况下,@MessageMapping
注释指定目的地,同时@SubscribeMapping
仅指示对订阅消息的兴趣。
的@SubscribeMapping
方法,通常是没有任何不同@MessageMapping
相对于映射和输入参数的方法。例如,您可以将它与类型级别组合@MessageMapping
以表示共享目标前缀,并且可以使用与任何@ MessageMapping`方法相同的方法参数。
关键的区别@SubscribeMapping
在于方法的返回值被序列化为有效载荷并且不是发送到“brokerChannel”而是发送到“clientOutboundChannel”,有效地直接回复到客户端而不是通过代理进行广播。这对于实现一次性请求 - 回复消息交换非常有用,并且永远不会保留订阅。此模式的常见方案是在必须加载和显示数据时应用程序初始化。
阿@SubscribeMapping
与也可以注释的方法@SendTo
在这种情况下的返回值发送到"brokerChannel"
与所述显式指定的目标目的地。
@MessageExceptionHandler
应用程序可以使用@MessageExceptionHandler
方法来处理方法中的异常 @MessageMapping
。感兴趣的异常可以在注释本身中声明,或者如果要获取对异常实例的访问权限,则可以通过方法参数声明:
`@Controller public class MyController {
_// ..._
@MessageExceptionHandler
**public** ApplicationError handleException(MyException exception) {
_// ..._
**return** appError;
}
}`
@MessageExceptionHandler
方法支持灵活的方法签名,并支持相同的方法参数类型和返回值作为@MessageMapping
方法。
通常,@MessageExceptionHandler
方法适用于@Controller
声明它们的类(或类层次结构)。如果您希望这些方法在控制器之间全局应用,则可以在标记为的类中声明它们@ControllerAdvice
。这与Spring MVC中的类似支持相当。
4.4.6。发送信息
如果要从应用程序的任何部分向连接的客户端发送消息,该怎么办?任何应用程序组件都可以向其发送消息"brokerChannel"
。最简单的方法是SimpMessagingTemplate
注入,并使用它来发送消息。通常,应该很容易按类型注入,例如:
`@Controller public class GreetingController {
**private** SimpMessagingTemplate template;
@Autowired
**public** GreetingController(SimpMessagingTemplate template) {
this.template = template;
}
@RequestMapping(path="/greetings", method=POST)
**public** **void** greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}
}`
但如果存在相同类型的另一个bean,它也可以通过其名称“brokerMessagingTemplate”进行限定。
4.4.7。简单的经纪人
内置的简单消息代理处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目标的连接客户端。代理支持类似路径的目标,包括对Ant样式目标模式的订阅。
应用程序也可以使用点分隔目标(vs斜杠)。请参见Dot as Separator。
4.4.8。外部经纪人
简单的代理非常适合入门,但仅支持STOMP命令的子集(例如,没有ack,收据等),依赖于简单的消息发送循环,并且不适合于群集。作为替代方案,应用程序可以升级到使用功能齐全的消息代理。
检查STOMP文档以查找您选择的消息代理(例如 RabbitMQ, ActiveMQ等),安装代理,并在启用STOMP支持的情况下运行它。然后在Spring配置中启用STOMP代理中继而不是简单代理。
以下是启用功能齐全的代理的示例配置:
`@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
**public** **void** registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
**public** **void** configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}`
XML配置等效:
`
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio" />
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>
`
上述配置中的“STOMP代理中继”是Spring MessageHandler ,它通过将消息转发到外部消息代理来处理消息。为此,它建立到代理的TCP连接,将所有消息转发给它,然后通过其WebSocket会话将从代理接收的所有消息转发给客户端。从本质上讲,它充当“转发”,可以在两个方向上转发消息。
请 为项目添加io.projectreactor.ipc:reactor-netty
和io.netty:netty-all
依赖项以进行TCP连接管理。
此外,应用程序组件(例如HTTP请求处理方法,业务服务等)也可以向代理中继发送消息,如发送消息中所述,以便向订阅的WebSocket客户端广播消息。
实际上,代理中继实现了健壮且可扩展的消息广播。
4.4.9。连接到经纪人
STOMP代理中继维护与代理的单个“系统”TCP连接。此连接仅用于源自服务器端应用程序的消息,而不用于接收消息。您可以为此连接配置STOMP凭据,即STOMP帧login
和passcode
标头。这在XML命名空间和Java配置中都显示为具有默认值/ 的 systemLogin
/ systemPasscode
properties 。guest``guest
STOMP代理中继还为每个连接的WebSocket客户端创建单独的TCP连接。您可以配置STOMP凭据以用于代表客户端创建的所有TCP连接。这在XML命名空间和Java配置中都显示为具有默认值/ 的clientLogin
/ clientPasscode
properties 。guest``guest
STOMP代理中继始终 在代表客户端转发给代理的每个帧上设置login
和passcode
标头CONNECT
。因此,WebSocket客户端无需设置这些标头; 他们会被忽略。正如身份验证 部分所述,WebSocket客户端应该依赖HTTP身份验证来保护WebSocket端点并建立客户端身份。
STOMP代理中继还通过“系统”TCP连接向消息代理发送和接收心跳。您可以配置发送和接收心跳的间隔(默认情况下每个10秒)。如果与代理的连接丢失,代理中继将继续尝试每5秒重新连接一次,直到成功为止。
任何Spring bean都可以实现ApplicationListener<BrokerAvailabilityEvent>
,以便在与代理的“系统”连接丢失并重新建立时接收通知。例如,股票报价服务广播股票报价可以在没有活动的“系统”连接时停止尝试发送消息。
默认情况下,STOMP代理中继始终连接,并在连接丢失时根据需要重新连接到同一主机和端口。如果您希望提供多个地址,则在每次尝试连接时,您都可以配置地址供应商,而不是固定的主机和端口。例如:
`@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
_// ..._
@Override
**public** **void** configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
registry.setApplicationDestinationPrefixes("/app");
}
**private** ReactorNettyTcpClient<**byte****[]**> createTcpClient() {
Consumer<ClientOptions.Builder<?>> builderConsumer = builder -> {
builder.connectAddress(()-> {
_// Select address to connect to ..._
});
};
**return** **new** ReactorNettyTcpClient<>(builderConsumer, **new** StompReactorNettyCodec());
}
}`
STOMP代理中继也可以配置virtualHost
属性。该属性的值将被设置为host
每个CONNECT
帧的标题,并且可能在例如云环境中是有用的,其中建立TCP连接的实际主机与提供基于云的STOMP服务的主机不同。
4.4.10。点作为分隔符
当消息路由到@MessageMapping
方法时,它们会匹配, AntPathMatcher
默认情况下,模式应使用斜杠“/”作为分隔符。这是Web应用程序中的一个很好的约定,类似于HTTP URL。但是,如果您更习惯于消息传递约定,则可以切换到使用点“。” 作为分隔符。
在Java配置中:
`@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
_// ..._
@Override
**public** **void** configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(****new** AntPathMatcher("."));**
registry.enableStompBrokerRelay("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}`
在XML中:
`
<websocket:message-broker application-destination-prefix="/app" path-matcher="**pathMatcher**">
<websocket:stomp-endpoint path="/stomp"/>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>
**<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"> <constructor-arg index="0" value="."/> </bean>**
`
之后,控制器可以使用点“。” 作为@MessageMapping
方法中的分隔符:
`@Controller @MessageMapping("foo") public class FooController {
@MessageMapping("bar.{baz}")
**public** **void** handleBaz(@DestinationVariable String baz) {
_// ..._
}
}`
客户端现在可以向其发送消息"/app/foo.bar.baz123"
。
在上面的示例中,我们没有更改“代理中继”上的前缀,因为它们完全依赖于外部消息代理。检查您正在使用的代理的STOMP文档页面,以查看它为目标标头支持的约定。
另一方面,“简单代理”确实依赖于配置,PathMatcher
因此如果您切换也将应用于代理的分隔符,并且将消息中的目标与订阅中的模式匹配。
4.4.11。认证
WebSocket消息传递会话中的每个STOMP都以HTTP请求开始 - 可以是升级到WebSockets的请求(即WebSocket握手),或者在SockJS回退一系列SockJS HTTP传输请求的情况下。
Web应用程序已经具有用于保护HTTP请求的身份验证和授权。通常,用户通过Spring Security使用某种机制(例如登录页面,HTTP基本身份验证或其他)进行身份验证。经过身份验证的用户的安全上下文保存在HTTP会话中,并与同一个基于cookie的会话中的后续请求相关联。
因此,对于WebSocket握手或SockJS HTTP传输请求,通常会有可通过身份验证的用户访问HttpServletRequest#getUserPrincipal()
。Spring自动将该用户与为其创建的WebSocket或SockJS会话相关联,随后通过用户头与该会话上传输的所有STOMP消息相关联。
简而言之,典型的Web应用程序不需要做任何特殊的事情,而不仅仅是它已经为安全做的事情。用户在HTTP请求级别进行身份验证,并通过基于cookie的HTTP会话维护安全上下文,然后将该会话与为该用户创建的WebSocket或SockJS会话相关联,并在每次Message
流经应用程序时生成用户标头。
请注意,STOMP协议在CONNECT
帧上具有“登录”和“密码”标头。这些最初设计用于并且仍然需要例如用于TCP上的STOMP。但是,对于STOMP over WebSocket,Spring默认忽略STOMP协议级别的授权标头,并假定用户已在HTTP传输级别进行了身份验证,并期望WebSocket或SockJS会话包含经过身份验证的用户。
Spring Security提供 WebSocket子协议授权 ,该授权使用a ChannelInterceptor
根据其中的用户头来授权消息。此外,Spring Session还提供 WebSocket集成 ,以确保在WebSocket会话仍处于活动状态时,用户HTTP会话不会过期。
4.4.12。令牌认证
Spring Security OAuth 支持基于令牌的安全性,包括JSON Web Token(JWT)。这可以用作Web应用程序中的身份验证机制,包括STOMP over WebSocket交互,就像上一节中所述,即通过基于cookie的会话维护身份。
同时,基于cookie的会话并不总是最适合,例如在不希望完全维护服务器端会话的应用程序中,或者在通常使用标头进行身份验证的移动应用程序中。
该WebSocket协议RFC 6455 “没有规定该服务器可以在WebSocket的握手过程中验证客户端的任何特定的方式。” 实际上,浏览器客户端只能使用标准身份验证标头(即基本HTTP身份验证)或cookie,并且不能提供自定义标头。同样,SockJS JavaScript客户端不提供使用SockJS传输请求发送HTTP头的方法,请参阅 sockjs-client问题196。相反,它确实允许发送可用于发送令牌但具有其自身缺点的查询参数,例如因为令牌可能无意中使用服务器日志中的URL进行了记录。
上述限制适用于基于浏览器的客户端,不适用于基于Spring Java的STOMP客户端,该客户端支持使用WebSocket和SockJS请求发送标头。
因此,希望避免使用cookie的应用程序可能无法在HTTP协议级别进行身份验证。他们可能更喜欢在STOMP消息传递协议级别使用标头进行身份验证,而不是使用Cookie。有两个简单的步骤可以做到这一点:
使用STOMP客户端在连接时传递身份验证标头。
使用a处理身份验证标头
ChannelInterceptor
。
下面是注册自定义身份验证拦截器的示例服务器端配置。请注意,拦截器只需要在CONNECT上进行身份验证并设置用户头Message
。Spring将记录并保存经过身份验证的用户,并将其与同一会话中的后续STOMP消息相关联:
`@Configuration @EnableWebSocketMessageBroker public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
**public** **void** configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(**new** ChannelInterceptorAdapter() {
@Override
**public** Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
**if** (StompCommand.CONNECT.equals(accessor.getCommand())) {
Authentication user = ... ; _// access authentication header(s)_
accessor.setUser(user);
}
**return** message;
}
});
}
}`
另请注意,在使用Spring Security的邮件授权时,您需要确保在ChannelInterceptor
Spring Security之前订购身份验证配置。最好通过在自己的WebSocketMessageBrokerConfigurer
标记为 的实现中声明自定义拦截器来完成@Order(Ordered.HIGHEST_PRECEDENCE + 99)
。
4.4.13。用户目的地
应用程序可以发送针对特定用户的消息,Spring的STOMP支持可识别"/user/"
为此目的而作为前缀的目标。例如,客户端可能订阅目标"/user/queue/position-updates"
。该目的地将由该处理UserDestinationMessageHandler
并且转换为用户会话唯一的目的地,例如"/queue/position-updates-user123"
。这提供了订阅一般命名的目的地的便利性,同时确保不与订阅相同目的地的其他用户发生冲突,使得每个用户可以接收唯一的库存位置更新。
在发送侧,可以将消息发送到目的地,例如"/user/{username}/queue/position-updates"
,该目的地 将由UserDestinationMessageHandler
一个或多个目的地翻译,一个目的地用于与用户相关联的每个会话。这允许应用程序中的任何组件发送针对特定用户的消息,而不必知道除其名称和通用目标之外的任何内容。通过注释和消息传递模板也支持这一点。
例如,消息处理方法可以向与通过@SendToUser
注释处理的消息相关联的用户发送消息(在类级别上也支持共享公共目的地):
`@Controller public class PortfolioController {
@MessageMapping("/trade")
@SendToUser("/queue/position-updates")
**public** TradeResult executeTrade(Trade trade, Principal principal) {
_// ..._
**return** tradeResult;
}
}`
如果用户具有多个会话,则默认情况下,所有订阅给定目标的会话都是目标。但是,有时可能需要仅定位发送正在处理的消息的会话。这可以通过将broadcast
属性设置为false 来完成,例如:
`@Controller public class MyController {
@MessageMapping("/action")
**public** **void** handleAction() **throws** Exception{
_// raise MyBusinessException here_
}
@MessageExceptionHandler
@SendToUser(destinations="/queue/errors", broadcast=false)
**public** ApplicationError handleException(MyBusinessException exception) {
_// ..._
**return** appError;
}
}`
虽然用户目的地通常意味着经过身份验证的用户,但并不严格要求。与经过身份验证的用户无关的WebSocket会话可以订阅用户目标。在这种情况下,@SendToUser
注释的行为与其完全相同broadcast=false
,即仅定位发送正在处理的消息的会话。
例如,还可以通过注入SimpMessagingTemplate
由Java配置或XML命名空间创建的消息,从任何应用程序组件向用户目标发送消息("brokerMessagingTemplate"
如果需要,则bean名称是必需的@Qualifier
):
`@Service public class TradeServiceImpl implements TradeService {
**private** **final** SimpMessagingTemplate messagingTemplate;
@Autowired
**public** TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
_// ..._
**public** **void** afterTradeExecuted(Trade trade) {
this.messagingTemplate.convertAndSendToUser(
trade.getUserName(), "/queue/position-updates", trade.getResult());
}
}`
将用户目标与外部消息代理一起使用时,请检查代理文档,了解如何管理非活动队列,以便在用户会话结束时删除所有唯一用户队列。例如,当使用目的地时,RabbitMQ会创建自动删除队列/exchange/amq.direct/position-updates
。所以在那种情况下客户可以订阅/user/exchange/amq.direct/position-updates
。同样,ActiveMQ具有 用于清除非活动目标的配置选项。
在多应用程序服务器方案中,用户目标可能仍未解析,因为用户连接到不同的服务器。在这种情况下,您可以配置目标以广播未解析的消息,以便其他服务器有机会尝试。这可以通过做userDestinationBroadcast
财产 MessageBrokerRegistry
在Java中的配置和user-destination-broadcast
该属性message-broker
的XML元素。
4.4.14。事件和拦截
发布了几个ApplicationContext
事件(如下所列),可以通过实现Spring的ApplicationListener
接口来接收。
BrokerAvailabilityEvent
- 表示经纪人何时可用/不可用。虽然“简单”代理在启动时立即可用,并且在应用程序运行时仍然如此,但STOMP“代理中继”可能会丢失与全功能代理的连接,例如,如果代理重新启动。代理中继具有重新连接逻辑,并在它返回时重新建立与代理的“系统”连接,因此只要状态从连接变为断开连接,反之亦然,就会发布此事件。使用该组件的组件SimpMessagingTemplate
应订阅此事件,并避免在代理不可用时发送消息。在任何情况下,他们都应该准备好在MessageDeliveryException
发送消息时进行处理。SessionConnectEvent
- 在收到新的STOMP CONNECT时发布,指示新客户端会话的开始。该事件包含表示连接的消息,包括会话ID,用户信息(如果有)以及客户端可能已发送的任何自定义标头。这对于跟踪客户端会话很有用。订阅此事件的组件可以使用SimpMessageHeaderAccessor
或 包装所包含的消息StompMessageHeaderAccessor
。SessionConnectedEvent
-SessionConnectEvent
在经纪人发送STOMP CONNECTED框架以响应CONNECT 之后不久发布的。此时,可以认为STOMP会话已完全建立。SessionSubscribeEvent
- 在收到新的STOMP SUBSCRIBE时发布。SessionUnsubscribeEvent
- 收到新的STOMP UNSUBSCRIBE时发布。SessionDisconnectEvent
- 在STOMP会话结束时发布。DISCONNECT可能已从客户端发送,也可能在WebSocket会话关闭时自动生成。在某些情况下,每个会话可能会多次发布此事件。对于多个断开连接事件,组件应该是幂等的。
使用功能齐全的代理时,STOMP“代理中继”会自动重新连接“系统”连接,以防代理暂时不可用。但是,客户端连接不会自动重新连接。假设启用了心跳,客户端通常会注意到代理在10秒内没有响应。客户端需要实现自己的重新连接逻辑。
上述事件反映了STOMP连接生命周期中的点。它们并不意味着为客户端发送的每条消息提供通知。相反,应用程序可以注册a ChannelInterceptor
来拦截每个传入和传出的STOMP消息。例如,拦截入站消息:
`@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
**public** **void** configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(**new** MyChannelInterceptor());
}
}`
自定义ChannelInterceptor
可以使用StompHeaderAccessor
或SimpMessageHeaderAccessor
访问有关邮件的信息。
`public class MyChannelInterceptor extends ChannelInterceptorAdapter {
@Override
**public** Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getStompCommand();
_// ..._
**return** message;
}
}`
请注意,与SesionDisconnectEvent
上面一样,可能已从客户端发送DISCONNECT消息,或者也可能在WebSocket会话关闭时自动生成。在某些情况下,拦截器可能会在每个会话中多次拦截此消息。对于多个断开连接事件,组件应该是幂等的。
4.4.15。STOMP客户端
Spring通过WebSocket客户端提供STOMP,通过TCP客户端提供STOMP。
要开始创建和配置WebSocketStompClient
:
WebSocketClient webSocketClient = **new** StandardWebSocketClient(); WebSocketStompClient stompClient = **new** WebSocketStompClient(webSocketClient); stompClient.setMessageConverter(**new** StringMessageConverter()); stompClient.setTaskScheduler(taskScheduler); _// for heartbeats_
在上面的例子StandardWebSocketClient
可以替换为,SockJsClient
因为这也是一个实现WebSocketClient
。所述SockJsClient
可以使用的WebSocket或基于HTTP的传输作为后备。有关更多详细信息,请参阅 SockJsClient。
接下来建立连接并为STOMP会话提供处理程序:
String url = "ws://127.0.0.1:8080/endpoint"; StompSessionHandler sessionHandler = **new** MyStompSessionHandler(); stompClient.connect(url, sessionHandler);
当会话准备好使用时,会通知处理程序:
`public class MyStompSessionHandler extends StompSessionHandlerAdapter {
@Override
**public** **void** afterConnected(StompSession session, StompHeaders connectedHeaders) {
_// ..._
}
}`
建立会话后,可以发送任何有效负载,并使用配置的序列化MessageConverter
:
session.send("/topic/foo", "payload");
您也可以订阅目的地。这些subscribe
方法需要处理订阅消息的处理程序,并返回Subscription
可用于取消订阅的句柄。对于每个收到的消息,处理程序可以指定有效负载应该反序列化的目标对象类型:
`session.subscribe("/topic/foo", new StompFrameHandler() {
@Override
**public** Type getPayloadType(StompHeaders headers) {
**return** String.class;
}
@Override
**public** **void** handleFrame(StompHeaders headers, Object payload) {
_// ..._
}
});`
为了使STOMP心跳配置WebSocketStompClient
有TaskScheduler
和任选定制心跳间隔10秒,其将导致一个心跳写入不活动待发送和10秒不活动读取其关闭连接。
当使用WebSocketStompClient
性能测试来模拟来自同一台计算机的数千个客户端时,请考虑关闭心跳,因为每个连接都会调度自己的心跳任务,并且不会针对在同一台计算机上运行的大量客户端进行优化。
STOMP协议还支持收据,其中客户端必须添加“收据”标头,服务器在处理发送或订阅后用RECEIPT帧响应。为了支持这个StompSession
提议 setAutoReceipt(boolean)
导致要在以后每发送添加或订阅了“回执”标头。或者,您也可以手动添加“收据”标题StompHeaders
。发送和订阅都返回一个实例Receiptable
,可用于注册接收成功和失败回调。对于此功能,客户端必须配置a TaskScheduler
和收据到期前的时间(默认为15秒)。
请注意,除了处理消息的异常回调以及传输级错误(包括)之外,它StompSessionHandler
本身还StompFrameHandler
允许它处理ERROR帧。handleException``handleTransportError``ConnectionLostException
4.4.16。WebSocket范围
每个WebSocket会话都有一个属性映射。映射作为标头附加到入站客户端消息,并且可以从控制器方法访问,例如:
`@Controller public class MyController {
@MessageMapping("/action")
**public** **void** handle(SimpMessageHeaderAccessor headerAccessor) {
Map<String, Object> attrs = headerAccessor.getSessionAttributes();
_// ..._
}
}`
也可以在websocket
作用域中声明一个Spring管理的bean 。WebSocket范围的bean可以注入控制器和“clientInboundChannel”上注册的任何通道拦截器。这些通常是单身,比任何单独的WebSocket会话都更长寿。因此,您需要为WebSocket范围的bean使用范围代理模式:
`@Component @Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS) public class MyBean {
@PostConstruct
**public** **void** init() {
_// Invoked after dependencies injected_
}
_// ..._
@PreDestroy
**public** **void** destroy() {
_// Invoked when the WebSocket session ends_
}
}
@Controller public class MyController {
**private** **final** MyBean myBean;
@Autowired
**public** MyController(MyBean myBean) {
this.myBean = myBean;
}
@MessageMapping("/action")
**public** **void** handle() {
_// this.myBean from the current WebSocket session_
}
}`
与任何自定义作用域一样,Spring MyBean
在第一次从控制器访问时初始化一个新实例,并将该实例存储在WebSocket会话属性中。随后返回相同的实例,直到会话结束。WebSocket范围的bean将调用所有Spring生命周期方法,如上面的示例所示。
4.4.17。性能
在性能方面没有银弹。许多因素可能会影响它,包括消息的大小,数量,应用程序方法是否执行需要阻止的工作,以及外部因素,如网络速度等。本部分的目标是提供可用配置选项的概述以及有关如何推理缩放的一些想法。
在消息传递应用程序中,消息通过用于由线程池支持的异步执行的通道传递。配置此类应用程序需要充分了解通道和消息流。因此,建议查看消息流。
显而易见的起点是配置支持"clientInboundChannel"
和的线程池 "clientOutboundChannel"
。默认情况下,两者都配置为可用处理器数量的两倍。
如果注释方法中的消息处理主要是CPU绑定的,则"clientInboundChannel"
应该保持接近处理器数量的线程数。如果他们所做的工作更多是IO绑定并且需要阻塞或等待数据库或其他外部系统,则需要增加线程池大小。
ThreadPoolExecutor
有3个重要的属性。这些是核心和最大线程池大小以及队列存储没有可用线程的任务的容量。
常见的混淆点是配置核心池大小(例如10)和最大池大小(例如20)导致具有10到20个线程的线程池。实际上,如果容量保留为其默认值Integer.MAX_VALUE,则线程池将永远不会超出核心池大小,因为所有其他任务都将排队。
请查看Javadoc,ThreadPoolExecutor
了解这些属性如何工作并了解各种排队策略。
另一方面,"clientOutboundChannel"
它是关于向WebSocket客户端发送消息。如果客户端位于快速网络上,则线程数应保持接近可用处理器的数量。如果它们很慢或带宽较低,则消耗消息所需的时间会更长,并给线程池带来负担。因此,增加线程池大小是必要的。
虽然“clientInboundChannel”的工作负载可以预测 - 毕竟它基于应用程序的工作 - 如何配置“clientOutboundChannel”更难,因为它基于应用程序无法控制的因素。因此,有两个与发送消息相关的附加属性。那些是"sendTimeLimit"
和"sendBufferSizeLimit"
。这些用于配置允许发送多长时间以及在向客户端发送消息时可以缓冲多少数据。
一般的想法是,在任何给定时间,只有一个线程可用于发送给客户端。同时,所有其他消息都会被缓冲,您可以使用这些属性来决定允许发送消息的时间长度以及可以在平均时间内缓冲多少数据。有关重要的其他详细信息,请查看此配置的XML架构的Javadoc和文档。
以下是示例配置:
`@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
**public** **void** configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
}
_// ..._
}`
`
<websocket:message-broker>
<websocket:transport send-timeout="15000" send-buffer-size="524288" />
_<!-- ... -->_
</websocket:message-broker>
`
上面显示的WebSocket传输配置还可用于配置传入STOMP消息的最大允许大小。虽然理论上WebSocket消息的大小几乎是无限的,但实际上WebSocket服务器会施加限制 - 例如,Tomcat上的8K和Jetty上的64K。因此,诸如JavaScript webstomp-client 等STOMP 客户端在16K边界处拆分较大的STOMP消息,并将它们作为多个WebSocket消息发送,因此需要服务器缓冲和重新组装。
Spring的STOMP over WebSocket支持这样做,因此应用程序可以配置STOMP消息的最大大小,而不管WebSocket服务器特定的消息大小。请记住,必要时将自动调整WebSocket消息大小,以确保它们至少可以携带16K WebSocket消息。
以下是示例配置:
`@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
**public** **void** configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(128 * 1024);
}
_// ..._
}`
`
<websocket:message-broker>
<websocket:transport message-size="131072" />
_<!-- ... -->_
</websocket:message-broker>
`
关于扩展的一个重点是使用多个应用程序实例。目前,使用简单代理无法做到这一点。但是,当使用RabbitMQ等功能齐全的代理时,每个应用程序实例都会连接到代理,并且从一个应用程序实例广播的消息可以通过代理广播到通过任何其他应用程序实例连接的WebSocket客户端。
4.4.18。监控
使用@EnableWebSocketMessageBroker
或<websocket:message-broker>
关键基础架构组件时,会自动收集统计信息和计数器,以便深入了解应用程序的内部状态。该配置还声明了一个类型的bean,WebSocketMessageBrokerStats
它在一个地方收集所有可用信息,默认情况下INFO
每30分钟将其记录一次。这个bean可以通过Spring导出到JMX MBeanExporter
,以便在运行时查看,例如通过JDK jconsole
。以下是可用信息的摘要。
客户端WebSocket会话
当前
表示当前有多少客户端会话,其中包括WebSocket与HTTP流和轮询SockJS会话进一步细分的计数。
总
表示已建立的会话总数。
异常关闭
连接失败
这些会话已经建立但在60秒内没有收到任何消息后关闭。这通常表示代理或网络问题。
超出发送限制
超过配置的发送超时或缓慢客户端可能发生的发送缓冲区限制后会话关闭(请参阅上一节)。
运输错误
在传输错误(例如无法读取或写入WebSocket连接或HTTP请求/响应)之后会话关闭。
STOMP框架
处理的CONNECT,CONNECTED和DISCONNECT帧总数,表示STOMP级别连接的客户端数量。请注意,当会话异常关闭或客户端关闭而不发送DISCONNECT帧时,DISCONNECT计数可能会更低。
STOMP经纪人接力
TCP连接
表示代表客户端WebSocket会话建立多少个TCP连接到代理。这应该等于客户端WebSocket会话的数量+ 1个用于从应用程序内发送消息的额外共享“系统”连接。
STOMP框架
代表客户端转发到代理或从代理接收的CONNECT,CONNECTED和DISCONNECT帧的总数。请注意,无论客户端WebSocket会话如何关闭,都会将DISCONNECT帧发送到代理。因此,较低的DISCONNECT帧计数表示代理主动关闭连接,可能是因为没有及时到达的心跳,无效的输入帧或其他。
客户端入站通道
来自线程池的统计信息支持“clientInboundChannel”,提供对传入消息处理的运行状况的深入了解。在此排队的任务表明应用程序可能太慢而无法处理消息。如果有I / O绑定任务(例如,慢速数据库查询,对第三方REST API的HTTP请求等),请考虑增加线程池大小。
客户出站频道
支持“clientOutboundChannel”的线程池中的统计信息,提供对客户端广播消息运行状况的深入了解。在此排队的任务表明客户端消耗消息的速度太慢。解决此问题的一种方法是增加线程池大小以适应预期的并发慢客户端数量。另一种选择是减少发送超时和发送缓冲区大小限制(参见上一节)。
SockJS任务计划程序
来自SockJS任务调度程序的线程池的统计信息,用于发送心跳。请注意,在STOMP级别协商心跳时,将禁用SockJS心跳。
4.4.19。测试
使用Spring的STOMP over WebSocket支持测试应用程序有两种主要方法。第一种是编写服务器端测试来验证控制器的功能及其带注释的消息处理方法。第二种是编写涉及运行客户端和服务器的完整端到端测试。
这两种方法并不相互排斥。相反,每个人都在整体测试策略中占有一席之地。服务器端测试更集中,更易于编写和维护。另一方面,端到端集成测试更完整,测试更多,但它们也更多地参与编写和维护。
最简单的服务器端测试形式是编写控制器单元测试。然而,由于控制器的大部分功能取决于其注释,因此这没有用。纯单元测试根本无法测试。
理想情况下,测试中的控制器应该在运行时调用,就像测试使用Spring MVC测试框架处理HTTP请求的控制器的方法一样。即没有运行Servlet容器,而是依赖Spring Framework来调用带注释的控制器。就像Spring MVC Test一样,有两种可能的替代方案,使用“基于上下文”或“独立”设置:
在Spring TestContext框架的帮助下加载实际的Spring配置,将“clientInboundChannel”作为测试字段注入,并使用它发送要由控制器方法处理的消息。
手动设置调用控制器(即
SimpAnnotationMethodMessageHandler
)所需的最小Spring框架基础结构,并将控制器的消息直接传递给它。
在股票投资组合 示例应用程序的测试中演示了这两种设置方案 。
第二种方法是创建端到端集成测试。为此,您需要以嵌入模式运行WebSocket服务器,并将其作为WebSocket客户端连接到它,发送包含STOMP帧的WebSocket消息。股票组合 示例应用程序的测试还演示了使用Tomcat作为嵌入式WebSocket服务器和用于测试目的的简单STOMP客户端的这种方法。