Spring 4 官方文档学习(十四)WebSocket支持

2023-02-14,,,,

个人提示:如果需要用到页面推送,高频且要低延迟,WebSocket无疑是最佳选择。否则还是轮询和long polling吧。

做了一个小demo放在码云上,有兴趣的可以看一下,简单易懂:websocket-demo。


本部分覆盖了web应用中Spring框架对WebSocket-style messaging的支持,包括使用STOMP作为应用及WebSocket子协议。

介绍 部分,给出了一个关于WebSocket的框架,覆盖了adoption challenges、design considerations、以及思考什么时候合适。

WebSocket API 部分,回顾了服务器侧Spring WebSocket API。

SockJS Fallback Options 部分,解释了SockJS协议,以及如何配置和使用它。

STOMP概览 部分,介绍了STOMP messaging 协议。

在WebSocket上启用STOMP 部分,演示了如何在Spring中配置STOMP支持。

注解消息处理 部分和后面的部分,解释了如何编写注解的消息处理方法、发送消息、选择消息broker options,以及work with the special "user" destinations(与特别的user目的地一起工作?)。

测试带注解的Controller方法 部分,列出了测试STOMP/WebSocket应用的三种方式。

1、介绍

WebSocket协议 RFC 6455,为web应用定义了一个重要的、全新的能力:全双工、双向通信(服务器和客户端之间)。在经历了让web更加交互的这段很长的技术历史之后,WebSocket是非常激动人心的新功能。(技术历史:Java Applet、XMLHttpRequest、Adobe Flash、ActiveXObject、不同的Comet技术、服务器发送的事件、等等)

对于WebSocket协议的更多介绍不在本文档范围之内。但至少,要理解HTTP仅用于初始化握手 -- 依赖于HTTP内建的机制来请求协议升级(或者协议切换)到服务器能够响应HTTP status 101 (切换协议)-- 当然前提是服务器允许。假定握手成功,HTTP upgrade request底层的TCP socket仍然保持打开状态,然后服务器和客户端都能使用它给对方发送消息

Spring Framework 4 包含了一个新的spring-websocket模块 -- 提供了广泛的WebSocket支持。它兼容Java WebSocket API 标准(JSR-356),也提供了额外的value-add -- 稍后有解释。

1.1、WebSocket Fallback Options

采用WebSocket的一个巨大挑战是某些浏览器不支持WebSocket。第一个支持WebSocket的IE版本是10.更多地,一些限制性的代理可能配置了拒绝HTTP upgrade或会在一段时间后破坏连接。建议看一下这篇文章"How HTML5 Web Sockets Interact With Proxy Servers"。

因此,今天想要构建一个WebSocket应用的话,要求fallback options 以在需要的时候模拟WebSocket API。Spring Framework提供了这样的透明的fallback options -- 基于SockJS protocol。这些选项可以通过配置来启用,不需要修改应用。

1.2、一个消息架构

除了来自short-to-midterm adoption的挑战,使用WebSocket还带来了重要的设计考虑,that are important to recognize early on, especially in contrast to what we know about building web applications today。

在今天,在构建web应用时,REST是一个被广泛地接受、理解、并支持的架构。该结构依赖于拥有很多URLs、几个HTTP methods、以及其他规则如使用超媒体(链接)、保持无状态、等等。

与此相反,WebSocket应用可能会使用单一的URL来初始化HTTP握手。此后所有的消息会分享和流动在同一个TCP连接上。这是一种完全不同的、异步的、事件驱动的消息架构。更类似与传统的消息应用(如JMS、AMQP)。

Spring Framework 4 包含了一个新的 spring-messaging 模块,带有来自Spring Integration项目的关键抽象(如Message、MessageChannel、MessageHandler、以及其他能够作为消息架构基础的东西)。该模块还包含了一组注解,可用于将消息映射到方法,类似于Spring MVC基于注解的编程模型。

1.3、Sub-Protocol Support in WebSocket -- WebSocket中的子协议支持

WebSocket是一个消息架构,但不强制使用任何特定的消息协议!

它是TCP之上非常薄的一层,会讲字节流转成消息(文本或二进制)流,仅此。它依赖于应用来解释消息的含义。

HTTP是应用层协议,与此不同,在WebSocket协议中在incoming消息中没有足够的信息让框架或容器来明白如何route 或处理它。因此,WebSocket也许不那么低级,但很trivial(零碎)。你可以直接使用它,也可以在上面创建一个框架。这类似于大多数应用都是用web框架,而不直接使用Servlet API。

有鉴于此,WebSocket RFC定义了sub-protocols的使用。在握手期间,客户端和服务器可以使用header Sec-WebSocket-Protocol,来允许使用一个sub-protocol,就是说,一个高级的应用层协议。虽然不要求使用sub-protocol,但是如果不使用,应用仍然需要选择一种消息格式 -- 服务器和客户端都懂的。该消息格式可以是自定义、框架特有的、或者标准的消息协议。

Spring框架提供了一种支持:使用STOMP,STOMP是一个简单的消息协议,最早是受HTTP启发而被创建的脚本语言。

在web上,STOMP被广泛的支持,且良好地适用于WebSocket。

1.4、我应该使用WebSocket吗?

在围绕使用WebSocket的设计考虑中,很值得问一句,“什么时候适合使用?”。

在web应用中最适合WebSocket的场景是 客户端和服务器需要高频率低延迟交换事件的时候。基本的候选包括但不限于,金融、游戏、合作、以及其他应用。这些应用对时间延迟很敏感,还需要以高频率交换大量的消息。

对其他应用类型,这可能不合适。例如,新闻或社交feed 显示新闻只需要每隔几分钟简单地poll一下即可。虽然延迟也很重要,但几分钟后再显示也无所谓。

甚至在一些延迟要求很严格的场景里,例如消息的提交相对较低(如监控网络失败的信息),仍然可以考虑使用long polling,因为相对简单。

只有在同时考虑低延迟高频率消息时,可以使用WebSocket协议。就算这些应用,选择仍然存在:所有的client-server通信是否通过WebSocket messages来完成,还是使用HTTP和REST来完成?答案是,视情况不同而不同。然而,可能存在某些功能同时由WebSocket和一个REST API来完成,以给客户端更多选择。更多地,一个REST API调用可能需要广播一条消息到感兴趣的客户端 -- 通过WebSocket。

Spring框架允许@Controller和@RestController classes拥有HTTP请求处理方法和WebSocket消息处理方法。

一个Spring MVC请求处理方法,或者任何应用方法,都可以轻易的广播一条消息到所有感兴趣的WebSocket客户端或者到特定的用户。

2、WebSocket API

Spring框架提供了一个WebSocket API,用于适用不同的WebSocket引擎。目前该列表包括了WebSocket runtimes,如:Tomcat 7.0.47+、Jetty 9.1+、GlassFish 4.1+、WebLogic 12.1.3+,以及Undertow 1.0+ (还有WildFly 8.0+)。以后可能添加其他支持。

如同在介绍中所解释的,直接使用WebSocket API太低级了 -- 除非假定了消息格式,否则框架很难解释消息或者route它们 -- 通过注解。这就是为什么应该考虑使用Sub-Protocol和Spring的STOMP over WebSocket支持。

当使用一个高级的协议时,WebSocket API的细节变得不相关起来,这和TCP通信的细节没有暴露在使用HTTP的应用中很相像。无论如何,本部分会覆盖使用WebSocket的细节。

2.1、创建和配置一个WebSocketHandler

创建一个WebSocket server很简单,只要实现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 namespace支持,可以将上面的WebSocket handler映射到特定的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();
} }

或者:

<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:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>

上面,是针对Spring MVC 应用的,所以应该包含在DispatcherServlet的配置中。然而,Spring的WebSocket支持不依赖于Spring MVC。通过WebSocketHttpRequestHandler可以非常简单地将一个WebSocketHandler集成到其他HTTP服务环境。

2.2、定制WebSocket握手

定制WebSocket初始化HTTP握手请求的最简单的办法是通过一个HandshakeInterceptor,该拦截器暴露了before和after握手方法。该拦截器可以用来阻拦握手或者让任意attributes可用于WebSocketSession。例如,有一个内建拦截器可以传递HTTP session attributes到WebSocket session:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer { @Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
} }

<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:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>

更高级的选项是继承DefaultHandshakeHandler,它操作了WebSocket握手的步骤,包括验证client origin、协商一个sub-protocol、等等。一个应用可能也需要使用该选项--如果它需要配置一个自定义的RequestUpgradeStrategy,以使用尚不支持的WebSocket server引擎和版本(2.4部分有更多信息)。Java-config和XML namespace都可以配置自定义的HandshakeHandler。

2.3、WebSocketHandler Decoration -- 装饰?

Spring提供了一个WebSocketHandlerDecorator基类,可以用额外的行为来装饰一个WebSocketHandler。使用WebSocket Java-config或XML namespace时,默认就添加了日志和异常处理实现。ExceptionWebSocketHandlerDecorator会捕获任意WebSocketHandler method抛出的所有的未捕获的异常,还会关闭WebSocket session并使用status 1011 来表明一个服务器错误。

2.4、部署考虑

Spring WebSocket API可以很简单地集成到Spring MVC应用中,然后DispatcherServlet会同时服务HTTP WebSocket handshake和其他HTTP请求。

将其集成到其他HTTP处理场景也是一样的,调用 WebSocketHttpRequestHandler 即可。简洁易懂。但关于JSR-356 runtimes需要特别的考虑。

Java WebSocket API (JSR-356) 提供了两种部署机制。第一种是使用一个Servlet容器在启动时进行类路径扫描(Servlet 3 功能);另一种是在Servlet容器初始化时使用一个注册API。二者都不能使用单一的前端控制器来处理所有的HTTP -- 包括WebSocket握手和所有其他HTTP请求 -- 例如Spring MVC的DispatcherServlet。

这是JSR-356最明显的限制。-- 翻译不达意:This is a significant limitation of JSR-356 that Spring’s WebSocket support addresses by providing a server-specific RequestUpgradeStrategy even when running in a JSR-356 runtime.

A request to overcome the above limitation in the Java WebSocket API has been created and can be followed at WEBSOCKET_SPEC-211. Also note that Tomcat and Jetty already provide native API alternatives that makes it easy to overcome the limitation. We are hopeful that more servers will follow their example regardless of when it is addressed in the Java WebSocket API. -- 大意是说请求要越过上面的限制,可以按照标准来创建。另外,Tomcat和Jetty已经提供了原生API可以很简单地越过限制。

另一个考虑是,希望支持JSR-356的Servlet容器执行一个 ServletContainerInitializer (SCI)扫描 会拖慢应用的启动,某些情况下非常显著。当升级到支持JSR-356的Servlet容器版本时,如果观察到明显的影响,那应该有选择的启用或禁用web碎片(和SCI扫描) -- 使用web.xml中的<absolute-ordering/>元素。如下:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"> <absolute-ordering/> </web-app>

你可以有选择的启用web碎片--通过名字,例如Spring自己的SpringServletContainerInitializer--它提供了对Servlet 3 Java 初始化 API的支持,如果需要的话。如下:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"> <absolute-ordering>
<name>spring_web</name>
</absolute-ordering> </web-app>

2.5、配置WebSocket引擎

每个底层的WebSocket引擎都会暴露一些配置properties,可以控制运行时特性,例如消息缓冲大小、空闲超时、等等。

对于Tomcat、WildFly、还有GlassFish来说,在你的WebSocket Java config中添加一个ServletServerContainerFactoryBean

@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 namespace:

<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"> <bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean> </beans>

对于客户端侧WebSocket配置来说,应该使用WebSocketContainerFactoryBean (XML) 或 ContainerProvider.getWebSocketContainer() (Java config)。

对于Jetty,需要提供一个预配置的Jetty WebSocketServerFactory,并通过你的WebSocket Java config将其插入到Spring的DefaultHandshakeHandler:

@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 namespace:

<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="/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> </beans>

2.6、配置允许的origins

自Spring Framework 4.1.5起,WebSocket和SockJS的默认行为是接受同源(same origin)请求。也可以允许所有或特定的origins列表。该检查主要是为浏览器客户端设计的。但也不会阻止其他类型的客户端修改Origin header value (详见RFC 6454: The Web Origin Concept)。

3种可能的行为是:

仅允许同源请求(默认):在该模式下,当启用SockJS时,Iframe HTTP response header X-Frame-Options 被设置成SAMEORIGIN,然后JSONP传输会被禁止--因其不允许检查请求的origin。因此,启用该模式时,不支持IE6/7。
运行指定的origins列表:每个提供的allowed origin必须以 http://或https://开头。在这种模式下,当启用SockJS时,IFrame和JSONP都被禁止!因此不支持IE6/7/8/9。
允许所有origins:设置为*即可。

允许WebSocket和SockJS的origins,可以这样配置:

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();
} }

<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 allowed-origins="http://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers> <bean id="myHandler" class="org.springframework.samples.MyHandler"/> </beans>

3、SockJS Fallback Options

如介绍中所解释的,不是所有的浏览器都支持WebSocket,且可能被一些代理拒绝。这就是为什么Spring提供了fallback options -- 基于SockJS protocol (version 0.3.3)尽可能地模拟WebSocket API。

3.1、SockJS概览

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-fallback-sockjs-overview

SockJS的目标是让应用可以使用WebSocket API,但会在必要的时候回滚到non-WebSocket替代 -- 在运行时, 就是说,不需要改变应用代码!

SockJS的组成:

以executable narrated tests 形式定义的SockJS protocol。
SockJS JavaScript client  -- 在浏览器中使用的客户端库。
SockJS服务器实现 -- 包括在Spring框架 spring-websocket 模块中的一个。
自 4.1 起,spring-websocket 也提供了一个SockJS Java client。

SockJS被设计用在浏览器中。

传输共分3大类别:WebSocket、HTTP Streaming、HTTP Long Polling。详见 this blog post。

SockJS client以发送 "GET /info" 从服务器获取基本的信息开始。之后,它必须决定使用什么样的传输。如果可以,会使用WebSocket。如果不可用,在多数浏览器中至少有一个HTTP Streaming选项,如果还不行,那只能使用HTTP (long) polling了!

所有传输请求都有下面的URL结构:

http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

{server-id} - useful for routing requests in a cluster but not used otherwise.
{session-id} - correlates HTTP requests belonging to a SockJS session.
{transport} - indicates the transport type, e.g. "websocket", "xhr-streaming", etc.

WebSocket transport需要的是仅仅一个单一的HTTP请求来进行WebSocket握手。此后所有的消息都在那个socket上面交换。

HTTP transport需要更多请求。例如,Ajax/XHR streaming 依赖于一个长期运行的请求来完成服务器到客户端的消息,额外的HTTP POST请求来完成客户端到服务器的消息。Long polling是类似的 -- 只是 它会在每次服务器到客户端的发送之后结束当前请求。

SockJS添加了最小的消息框架。例如,服务器发送字母 o (打开)开始,消息通过JSON编码的数组发送,字母h(心跳)-- 如果如果消息流默认就是25秒,字母c(关闭)来关闭session。

想要学习更多,在浏览器中运行一个例子,然后观察HTTP请求即可。SockJS客户端允许固定transport列表,这样就可以一次观察一个transport。

SockJS客户端还提供了debug flag,会在浏览器的控制台启用帮助信息。在服务器侧为org.springframework.web.socket启用TRACE日志级别。更多详见SockJS协议 narrated test。

3.2、启用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 (version 1.0.x)来模拟W3C WebSocket API以及与服务器通信来选择最佳transport选项 -- 取决于运行的浏览器。回顾一下sockjs-client页面和浏览器支持的transport类型列表。该客户端还支持几个配置选项,如指定包含哪些个transports。

3.3、IE 8/9中的HTTP Streaming:Ajax/XHR vs IFrame

未完待续

关于WebSocket:

WebSocket

官方文档链接:

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html

 

Spring 4 官方文档学习(十四)WebSocket支持的相关教程结束。

《Spring 4 官方文档学习(十四)WebSocket支持.doc》

下载本文的Word格式文档,以方便收藏与打印。