Spring Framework 4.0 M1:WebSocket 支持

工程 | Rossen Stoyanchev | 2013 年 5 月 23 日 | ...

您可能已经看到,Spring Framework 4.0 的第一个里程碑版本已经发布,与此同时我们也发布了早期的 WebSocket 支持。为什么 WebSocket 很重要?它通过网络实现了高效的双向通信,这在需要客户端(通常是浏览器)和服务器之间以高频率、低延迟交换消息的应用中至关重要。常见的例子包括交易、游戏、协作、数据可视化等,但随着时间的推移,场景和用例的范围将会不断扩大。

WebSocket 是一个非常广泛的主题!您可以观看我们在 SpringOne 2012 上发布的 InfoQ 上的 “WebSocket 入门” 以获得更全面的介绍。简单来说,能够使用 WebSocket 只是个开始。对于尚未支持它的浏览器(例如 IE < 10)以及阻止其使用的网络代理,您需要一个备选方案。此外,针对套接字编程的级别非常非常低。大多数应用将受益于更高级别的编程模型。这一点在 WebSocket 协议中也得到了认可,通过一种机制启用了“子协议”(即更高级别的协议),就像我们今天都使用 HTTP 而不是原始的 TCP 套接字一样。子协议示例包括 STOMPWAMP 和许多其他协议。

请记住,这是一个早期版本。它侧重于基础知识,包括 JSR-356 支持和浏览器内部使用的备选方案。目前尚未提供子协议支持。这是下一个里程碑版本的目标。

Java WebSocket API (JSR-356)

Java WebSocket 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://localhost: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 配置和处理选项,并且更新频率可能远高于 Java WebSocket API,我们今后可能会继续直接使用 Jetty 9 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 不允许。如果一个 Exception(或 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 streaming、long polling 或其他方式。


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 streaming 请求上发送周期性心跳消息(以防止代理认为连接已失效)、移除过期的 SockJS 会话等。

总结

包含示例和说明的项目可以在 Github 上找到。它包含配置 JSR-356 端点、Spring WebSocketHandler 以及 SockJS 服务的示例。对于所有示例,我建议使用 Google Chrome 开发者工具的网络选项卡来观察 WebSocket 和 HTTP 流量,查看错误等。

如果您有反馈或评论,我们很乐意倾听!

订阅 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

保持领先

VMware 提供培训和认证,助您飞速进步。

了解更多

获得支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部