走在前面
VMware 提供培训和认证,以加速您的进步。
了解更多您可能已经看到,Spring Framework 4.0 的第一个里程碑版本已发布公告,并且我们发布了早期的 WebSocket 支持。为什么 WebSocket 很重要?它支持高效的双向 Web 通信,这在需要在客户端(通常是浏览器)和服务器之间高频率、低延迟交换消息的应用程序中至关重要。常见的示例包括交易、游戏、协作、数据可视化等,但随着时间的推移,场景和用例范围将会扩大。
WebSocket 是一个非常广泛的主题!您可以观看我们从 SpringOne 2012 在 InfoQ 上的“WebSocket 入门”,以获得更全面的介绍。简单来说,能够使用 WebSocket 仅仅是一个开始。您需要为尚不支持 WebSocket 的浏览器(例如 IE < 10)以及阻止其使用的网络代理制定回退策略。此外,面向套接字的编程非常非常底层。大多数应用程序将受益于更高级别的编程模型。这一点在 WebSocket 协议中也得到了认可,该协议通过一种机制支持使用“子协议”(即更高级别的协议),就像我们今天都使用 HTTP 而不是原始 TCP 套接字一样。子协议示例包括STOMP、WAMP 等。
请记住,这是一个早期版本。它专注于基础知识,包括 JSR-356 支持和在浏览器中使用的回退选项。目前尚无子协议支持。这是下一个里程碑版本的重点。
WebSocket 的 Java API(JSR-356)
WebSocket 的 Java API 最近已完成,是 Java EE 7 的一部分。它定义了两种类型的端点——Endpoint
的子类以及带注释的端点,即 @ClientEndpoint
和 @ServerEndpoint
。对它的适当介绍超出了本文的范围。我只会提及理解如何在 Spring 应用程序中配置和使用端点所需的最低限度内容。
在 JSR-356 中有两种方法可以部署服务器端点——通过 Servlet 容器扫描(Servlet 3.0 功能)和在启动时以编程方式部署。对于 Servlet 容器扫描,规范要求带注释的端点具有默认构造函数。但是,Endpoint 子类无法自动部署。相反,Servlet 容器扫描检测 ServerApplicationConfig
类型,而这些类型又需要为每个 Endpoint
提供 Server/ClientEndpointConfig
。
在您尝试理解所有这些内容之前,您可能想知道它与您的 Spring 应用程序有什么关系。M1 版本完全支持通过 Spring 初始化这两种类型的端点,包括正确的构造函数依赖项注入以及每个连接和单例端点生命周期。此外,您应该能够关闭 Servlet 容器扫描,它相当繁重,会扫描所有类,包括第三方依赖项。
给我看看代码!
要使用 Spring 初始化带注释的端点,只需在类型级注释中配置一个 SpringConfigurator
import javax.websocket.server.ServerEndpoint;
import org.springframework.web.socket.server.endpoint.SpringConfigurator;
@ServerEndpoint(value = "/echo", configurator = SpringConfigurator.class)
public class EchoEndpoint {
private final EchoService echoService;
@Autowired
public EchoEndpoint(EchoService echoService) {
this.echoService = echoService;
}
@OnMessage
public void handleMessage(Session session, String message) {
// ...
}
}
上面的代码确实假设使用 SpringContextLoaderListener
加载 Spring 配置,但在 Web 应用程序中通常是这样。除此之外,无需其他操作。Servlet 容器扫描找到带注释的端点,SpringConfigurator
为每个 WebSocket 会话初始化一个新实例,这也是规范中定义的默认生命周期。
如果要使用单个实例或要关闭 Servlet 容器扫描,请将 EchoEndpoint
声明为 Spring bean,并添加 ServerEndpointExporter
的(一次性!)bean 声明。下面的示例使用Spring 的 Java 配置,但您也可以将等效声明添加到基于 XML 的配置中
import org.springframework.web.socket.server.endpoint.ServerEndpointExporter;
@Configuration
public class EndpointConfig {
@Bean
public EchoEndpoint echoEndpoint() {
return new EchoEndpoint(echoService());
}
@Bean
public EchoService echoService() {
// ...
}
@Bean
public ServerEndpointExporter endpointExporter() {
return new ServerEndpointExporter();
}
}
Endpoint 子类可以通过 EndpointRegistration
部署,并附带 ServerEndpointExporter
的(一次性!)声明
import org.springframework.web.socket.server.endpoint.ServerEndpointExporter;
import org.springframework.web.socket.server.endpoint.ServerEndpointRegistration;
@Configuration
public class EndpointConfig {
@Bean
public EndpointRegistration echoEndpoint() {
return new EndpointRegistration("/echo", EchoEndpoint.class);
}
@Bean
public ServerEndpointExporter endpointExporter() {
return new ServerEndpointExporter();
}
// ..
}
EndpointRegistration
还具有一个接受端点实例的构造函数。这允许您选择在每个 WebSocket 会话中使用新实例,还是使用单个实例为所有会话提供服务。
客户端方面怎么样?
JSR-356 为连接到服务器提供了以下 API
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
container.connectToServer(EchoEndpoint.class, new URI("ws:localhost:8080/webapp/echo"));
这很简单,但如果我们也能使其声明化会更好。一个常见的用例是——每当 Web 应用程序启动时,它都应该自动连接到远程端点,开始处理消息,并在应用程序关闭时停止。
您可以使用连接管理器来做到这一点,如下所示,其中 WebSocket 连接分别在 Spring ApplicationContext 刷新或关闭时建立和关闭
import org.springframework.web.socket.client.endpoint.AnnotatedEndpointConnectionManager;
@Configuration
public class EndpointConfig {
// For Endpoint sub-classes use EndpointConnectionManager instead
@Bean
public AnnotatedEndpointConnectionManager connectionManager() {
return new AnnotatedEndpointConnectionManager(echoEndpoint(), "ws://127.0.0.1:8080/webapp/echo");
}
@Bean
public EchoEndpoint echoEndpoint() {
// ...
}
}
您还可以使用 autoStartup
属性来启用/禁用自动连接。如果禁用,则可以手动调用 start()
和 stop()
。
这结束了对 JSR-356 支持的概述。
Spring WebSocket API
除了 JSR-356 支持之外,此版本还提供了 Spring WebSocket API 的基础,这引发了一些显而易见的问题。
为什么我们需要自己的 API?我们在内部将其用作更高级别服务(如 SockJS)的基础。它允许我们插入其他 Java WebSocket 实现并在可能的情况下添加其他功能。例如,JSR-356 没有提供从现有 Servlet 发起 WebSocket 握手的方法,我们在添加 SockJS 支持时发现这非常有用。此外,尽管 Jetty 尚未提供 JSR-356 支持,但我们能够插入(全新的)Jetty 9 WebSocket API,并在此版本中包含 Jetty 9 支持。我们以后可能会坚持直接使用 Jetty 9 API,因为它提供了更丰富的 WebSocket 配置和处理选项,并且可能比 WebSocket 的 Java API 更新得更频繁。
为什么只基于类型(即没有注释)?Spring WebSocket API 主要面向框架使用。应用程序当然可以使用它,但我们认为面向套接字的编程对于大多数应用程序来说都过于底层,无法组织其逻辑并提供强大的消息处理。为了更好地理解这一点,请考虑以下情况:如果应用程序公开单个 WebSocket 连接(在大多数情况下应该如此),则它必须处理来自单个类的所有应用程序消息类型。即使使用注释,这也不能扩展到现实世界应用程序的复杂性。想象一下没有名词(URL)和动词(HTTP 方法)的 REST,只是一个原始套接字。这就是为什么子协议支持和更高级别的编程模型非常重要的原因,这就是我们更有可能使用注释的地方。
希望这能解决“为什么”的问题。现在让我们看看一些代码。
Spring WebSocket API 中的核心接口是 WebSocketHandler
。下面是处理文本消息的实现,其中基类的方法为空,除了通过状态 1003(不可接受)关闭会话来拒绝二进制消息,如协议中所定义的那样
import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter;
public class EchoHandler extends TextWebSocketHandlerAdapter {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
session.sendMessage(message);
}
}
请注意,handleTextMessage
允许异常传播。这与 JSR-356 不同,JSR-356 不允许。如果异常(或 Throwable)逃脱了该方法,则会话将自动关闭,状态为 1011(服务器错误)。这意味着您可以选择处理异常(如果有任何有意义的操作需要执行),或者让它以默认方式处理。默认异常处理是通过 WebSocketHandlerDecorator
机制提供的。它可以扩展和/或替换。这些只是拥有我们自己的 API 使我们能够做到的几个示例。
WebSocketHandler
处理程序可以通过 WebSocketHttpRequestHandler
插入到 Spring MVC 中
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
@Configuration
public class WebConfig {
@Bean
public SimpleUrlHandlerMapping handlerMapping() {
Map<String, Object> urlMap = new HashMap<String, Object>();
urlMap.put("/echo", new WebSocketHttpRequestHandler(new EchoHandler()));
SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
hm.setUrlMap(urlMap);
return hm;
}
}
SockJS 回退选项
SockJS 是一个浏览器 JavaScript 库,它提供了一个类似于 WebSocket 的编程模型,以及一系列特定于浏览器的传输,如果浏览器不支持 WebSocket,或者网络问题阻止其使用,则可以使用这些传输。我们很高兴地宣布在此版本中支持 SockJS。有关 SockJS 和各种传输选项的更多详细信息,请访问sockjs-client 项目页面。
要启用 SockJS 支持,只需声明一个 SockJsService
,将其映射到某个 URL,并提供一个 WebSocketHandler
来处理传入的消息。请注意,WebSocketHandler
与上面讨论的处理程序相同。换句话说,当使用 SockJS 时,编程模型保持不变,但底层传输可能会根据需要更改为 HTTP 流、长轮询或其他内容。
import org.springframework.web.socket.sockjs.SockJsService;
// ...
@Configuration
public class WebConfig {
@Bean
public SimpleUrlHandlerMapping handlerMapping() {
SockJsService sockJsService = new DefaultSockJsService(taskScheduler());
Map<String, Object> urlMap = new HashMap<String, Object>();
urlMap.put("/echo/**", new SockJsHttpRequestHandler(sockJsService, new EchoHandler()));
SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
hm.setUrlMap(urlMap);
return hm;
}
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setThreadNamePrefix("SockJS-");
return taskScheduler;
}
}
如果您想知道,上面的任务调度程序用于各种与 SockJS 相关的任务,例如在 HTTP 流请求上发送周期性心跳消息(以防止代理认为连接已失效)、删除过期的 SockJS 会话等。
结束语
可以在Github 上找到包含示例和说明的项目。它包括配置 JSR-356 端点、Spring WebSocketHandler 以及 SockJS 服务的示例。对于所有示例,我建议使用 Google Chrome 开发者工具的“网络”选项卡,以便观察 WebSocket 和 HTTP 流量、查看错误等。
如果您有任何反馈或意见,我们非常乐意倾听!