20220507 4. Web Servlet - WebSockets
前言
参考文档的这一部分包括对 Servlet 栈、 WebSocket 消息传递 (包括原始的 WebSocket 交互) 、通过 SockJS 进行的 WebSocket 仿真,以及作为 WebSocket 上的子协议通过 STOMP 进行的发布-订阅消息传递的支持
相关依赖:
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocket 简介
WebSocket 协议 RFC 6455 提供了一种标准化的方法,可以通过单个 TCP 连接在客户机和服务器之间建立全双工、双向通信通道。它是一个不同于 HTTP 的 TCP 协议,但是被设计为通过 HTTP 工作,使用端口 80 和 443 ,并允许重用现有的防火墙规则。
一个 WebSocket 的交互开始于一个 HTTP 请求,该请求使用 HTTP Upgrade
标头来升级,或者,在这种情况下,切换到 WebSocket 协议。下面的例子说明了这种相互作用:
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket # Upgrade 标头
Connection: Upgrade # 使用 Upgrade 连接
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
与通常的 200 状态代码不同,支持 WebSocket 的服务器返回类似于下面的输出:
HTTP/1.1 101 Switching Protocols # 协议转换
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
在成功握手之后,位于 HTTP 升级请求之下的 TCP 套接字将保持打开状态,以便客户机和服务器继续发送和接收消息
注意,如果一个 WebSocket 服务器运行在 web 服务器之后 (例如 nginx) ,您可能需要对其进行配置,以便将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,请查看与 WebSocket 支持相关的云提供商的说明。
HTTP vs WebSocket
尽管 WebSocket 被设计为与 HTTP 兼容并以 HTTP 请求开始,但是理解这两个协议导致了非常不同的架构和应用程序编程模型是很重要的。
在 HTTP 和 REST 中,一个应用程序被建模为许多 url 。为了与应用程序交互,客户端访问这些 url ,采用请求-响应的方式。服务器根据 HTTP URL 、方法和标头将请求路由到适当的处理器。
相比之下,在 WebSocket 中,初始连接通常只有一个 URL 。随后,所有应用程序消息都在同一个 TCP 连接上流动。这指向一个完全不同的异步、事件驱动的消息传递架构。
WebSocket 也是一种底层传输协议,与 HTTP 不同,它不对消息内容规定任何语义。这意味着除非客户端和服务器在消息语义上达成一致,否则没有办法路由或处理消息。
WebSocket 客户端和服务器可以通过 HTTP 握手请求上的 Sec-WebSocket-Protocol
标头协商使用更高级别的消息传递协议 (例如,STOMP) 。如果没有这些,他们需要制定自己的约定。
何时使用 WebSocket
WebSocket 可以使网页变得动态和互动。然而,在许多情况下,结合 Ajax 和 HTTP 流或长轮询可以提供简单有效的解决方案。
例如,新闻、邮件和社交提要需要动态更新,但是每隔几分钟就更新一次可能完全没问题。另一方面,协作、游戏和金融应用需要更接近实时。
延迟本身并不是决定因素。如果消息量相对较低 (例如,监视网络故障) ,HTTP 流或轮询可以提供有效的解决方案。低延迟、高频率和高容量的组合是使用 WebSocket 的最佳选择。
还要记住,在互联网上,您无法控制的限制性代理可能会阻止 WebSocket 交互,这可能是因为它们没有配置为传递 Upgrade
标头,也可能是因为它们关闭了似乎空闲的长期连接。这意味着,与面向公众的应用程序相比,将 WebSocket 用于防火墙内的内部应用程序是一个更直接的决定。
WebSocket API
Spring 框架提供了 WebSocket API,您可以使用它来编写处理 WebSocket 消息的客户端和服务器端应用程序。
WebSocketHandler
创建一个 WebSocket 服务器和实现 WebSocketHandler
一样简单,或者更有可能的是,扩展 TextWebSocketHandler
或 BinaryWebSocketHandler
。下面的示例使用 TextWebSocketHandler
:
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 配置和 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 配置:
<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:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
前面的示例用于 Spring MVC 应用程序,应该包含在 DispatcherServlet
的配置中。然而,Spring 的 WebSocket 支持并不依赖于 Spring MVC 。在 WebSocketHttpRequestHandler
的帮助下,将 WebSocketHandler
集成到其他 http 服务环境中相对简单。
WebSocket 握手
定制初始 HTTP WebSocket 握手请求的最简单方法是通过 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 配置:
<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: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 握手的步骤,包括验证客户端来源、协商子协议以及其他细节。如果应用程序需要配置自定义的 RequestUpgradeStrategy
以适应 WebSocket 服务器引擎和尚不支持的版本,则可能还需要使用此选项 (有关此主题的更多信息,请参见 Deployment ) 。Java 配置和 XML 命名空间使配置自定义 HandshakeHandler
成为可能。
Spring 提供了一个 WebSocketHandlerDecorator
基类,您可以使用该基类用附加行为装饰 WebSocketHandler
。在使用 WebSocket Java 配置或 XML 名称空间时,默认情况下提供并添加了日志记录和异常处理实现。ExceptionWebSocketHandlerDecorator
捕获任何 WebSocketHandler
方法产生的所有未捕获的异常,并关闭状态为 1011
的 WebSocket 会话,这表明存在服务器错误。
部署(Deployment)
Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,其中 DispatcherServlet
同时服务于 HTTP WebSocket 握手和其他 HTTP 请求。通过调用 WebSocketHttpRequestHandler
,还可以轻松地集成到其他 HTTP 处理场景中。这很方便,也很容易理解。但是,对于 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) 中。
已经创建了一个 request 来克服 Java WebSocket API 中的上述限制,并且可以在 eclipse-ee4j/websocket-api#211 中跟踪。Tomcat、 Undertow 和 WebSphere 提供了它们自己的 API 替代方案,这使得实现这一点成为可能,Jetty 也可以实现这一点。我们希望更多的服务器也能做同样的事情。
另一个需要考虑的问题是,支持 JSR-356 的 Servlet 容器需要执行 ServletContainerInitializer
(SCI) 扫描,这可能会大大降低应用程序的启动速度。在某些情况下。如果在升级到支持 JSR-356 的 Servlet 容器版本之后发现了重大影响,那么应该可以通过使用 web.xml
中的 <absolute-ordering />
标签有选择地启用或禁用 web 片段 (和 SCI 扫描) ,如下面的示例所示:
<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
https://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
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>
</web-app>
服务器配置
每个底层 WebSocket 引擎都公开了控制运行时特征的配置属性,比如消息缓冲区大小、空闲超时等。
对于 Tomcat、 WildFly 和 GlassFish,你可以在你的 WebSocket Java 配置中添加一个 ServletServerContainerFactoryBean
,如下面的例子所示:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
相同的 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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://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 配置)
对于 Jetty,您需要提供一个预先配置的 Jetty WebSocketServerFactory
,并通过 WebSocket Java 配置将其插入 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)));
}
}
相同的 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
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="/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>
Allowed Origins
略
SockJS Fallback
略
STOMP
WebSocket 协议定义了两种类型的消息 (文本和二进制) ,但是它们的内容没有定义。该协议定义了一种机制,客户端和服务器可以协商一个子协议 (即更高级别的消息传递协议) ,以便在 WebSocket 之上使用,从而定义每个协议可以发送什么类型的消息、格式是什么、每条消息的内容等等。子协议的使用是可选的,但无论如何,客户端和服务器都需要就定义消息内容的某个协议达成一致。
概述
STOMP (Simple Text Oriented Messaging Protocol) 最初是为脚本语言 (如 Ruby、 Python 和 Perl) 创建的,用于连接到企业消息代理。它被设计用于解决常用消息传递模式的最小子集。STOMP 可以用于任何可靠的双向流网络协议,如 TCP 和 WebSocket 。尽管 STOMP 是一个面向文本的协议,但消息有效负载可以是文本或二进制。
STOMP 是一种基于帧的协议,其帧是基于 HTTP 建模的。下面的清单显示了一个 STOMP 框架的结构:
COMMAND
header1:value1
header2:value2
Body^@
客户端可以使用 SEND
或 SUBSCRIBE
命令发送或订阅消息,以及描述消息内容和应该接收消息的人的目标标头。这启用了一个简单的发布-订阅机制,您可以使用该机制通过代理将消息发送给其他连接的客户机,或者向服务器发送消息,请求执行某些工作。
其他内容略