JSR 356 (Java API for WebSocket)

作者:Johan Vos

了解如何将 WebSocket 集成到应用程序中。

2013 年 4 月发布

对于许多基于 Web 的客户端-服务器应用程序,旧的 HTTP 请求-响应模型有其局限性。信息必须在两次请求之间从服务器传输到客户端,而不是只在请求之时。

过去曾使用一些“窍门”来规避此问题,例如,长轮询和 Comet。然而,只是增加了对于客户端和服务器之间基于标准的双向全双工通道的需求。

2011 年,IETF 将 WebSocket 协议标准化为 RFC 6455。从那时起,大多数 Web 浏览器都在实现支持 WebSocket 协议的客户端 API。而且,还开发了一些实现 WebSocket 协议的 Java 库。

WebSocket 协议利用 HTTP 升级技术将 HTTP 连接升级到 WebSocket。升级之后,该连接就能够彼此独立地(全双工)双向发送消息(数据帧)。无需标头或 Cookie,显著降低了所需带宽。通常,WebSocket 用于定期发送小消息(例如,几个字节)。其他标头经常会导致开销大于有效负载。

JSR 356

JSR 356 (Java API for WebSocket) 指定 Java 开发人员在希望将 WebSocket 集成到应用程序(同时在服务器端和 Java 客户端)时可以使用的 API。WebSocket 协议的每一个声称符合 JSR 356 的实现都必须实现此 API。因此,开发人员可以独立于基础 WebSocket 实现编写基于 WebSocket 的应用程序。这有极大的好处,因为可以防止供应商锁定,并允许在库和应用程序服务器上有更多的选择和自由。

JSR 356 是即将推出的 Java EE 7 标准的一部分;因此,所有 Java EE 7 兼容的应用程序服务器都将有一个遵循 JSR 356 标准的 WebSocket 协议实现。一旦建立,WebSocket 客户端和服务器同级节点就是对称的。因此客户端 API 和服务器 API 之间的差异非常小。JSR 356 还定义了 Java 客户端 API,是 Java EE 7 中所需的完整 API 的一个子集。

利用 WebSocket 的客户端-服务器应用程序通常包含一个服务器组件和一个或多个客户端组件,如图 1 所示:

图 1

图 1

在本示例中,服务器应用程序用 Java 编写,WebSocket 协议的细节由 Java EE 7 容器中包含的 JSR 356 实现来处理。

JavaFX 客户端可依赖任何符合 JSR 356 的客户端实现来处理 WebSocket 特定的协议问题。其他客户端(例如,iOS 客户端和 HTML5 客户端)可以使用符合 RFC 6455 的其他(非 Java)实现与服务器应用程序通信。

编程模型

定义 JSR 356 的专家组希望支持 Java EE 开发人员常用的模式和技术。因此,JSR 356 利用了批注和注入。

通常支持两种不同的编程模型:

  • 批注驱动。使用批注的 POJO,开发人员可以与 WebSocket 生命周期事件交互。
  • 接口驱动。开发人员可以实现 Endpoint 接口以及与生命周期事件交互的方法。

生命周期事件

典型的 WebSocket 交互生命周期事件如下所示:

  • 一个同级节点(客户端)通过发送 HTTP 握手请求发起连接。
  • 另一个同级节点(服务器)以握手响应回复。
  • 连接建立。从现在开始,连接完全对称。
  • 两个同级节点发送和接收消息。
  • 其中一个同级节点关闭连接。

大多数 WebSocket 生命周期事件都可以映射到 Java 方法,无论是批注驱动的方法还是接口驱动的方法。

批注驱动的方法

接受传入 WebSocket 请求的端点可以是使用 @ServerEndpoint 批注进行批注的 POJO。此批注告诉容器给定的类应视为 WebSocket 端点。所需的 value 元素指定 WebSocket 端点的路径。

请考虑以下代码片段:

@ServerEndpoint("/hello") 
public class MyEndpoint { }

该代码将在相对路径 hello 处发布一个端点。此路径可包括后续方法调用中所用的路径参数;例如,/hello/{userid} 是一个有效路径,其中的 {userid} 值可以使用 @PathParam 批注在生命周期方法调用中获取。

在 GlassFish 中,如果使用 contextroot mycontextroot 将应用程序部署在于 localhost 的 8080 端口监听的 Web 容器中,则可以使用 ws://localhost:8080/mycontextroot/hello 访问该 WebSocket。

应发起 WebSocket 连接的端点可以是使用 @ClientEndpoint 批注进行批注的 POJO。@ClientEndpointServerEndpoint 之间的主要区别是 ClientEndpoint 不接受路径值元素,因为它不监听传入请求。

@ClientEndpoint 
public class MyClientEndpoint {}

可以利用批注驱动的 POJO 方法在 Java 中发起 WebSocket 连接,如下所示:

javax.websocket.WebSocketContainer container = 
javax.websocket.ContainerProvider.getWebSocketContainer();

container.conntectToServer(MyClientEndpoint.class, 
new URI("ws://localhost:8080/tictactoeserver/endpoint"));

之后,使用 @ServerEndpoint@ClientEndpoint 批注的类将称作加了批注的端点。

一旦建立 WebSocket 连接,将创建一个 Session,且将调用加批注端点上使用 @OnOpen 批注的方法。该方法可以包含一些参数:

  • javax.websocket.Session 参数,指定创建的 Session
  • EndpointConfig 实例,包含有关端点配置的信息
  • 零个或更多个使用 @PathParam 批注的字符串参数,指向端点路径上的路径参数

以下方法实现将在 WebSocket“打开”时输出会话的标识符:

@OnOpen
public void myOnOpen (Session session) {
   System.out.println ("WebSocket opened: "+session.getId());
}

只要 WebSocket 未关闭,Session 实例将一直有效。Session 类包含一些有趣的方法,允许开发人员获取有关连接的更多信息。同时,通过 getUserProperties() 方法返回一个 Map<String, Object>Session 还包含一个连接到应用程序特定数据的钩子。这允许开发人员使用应在各方法调用之间共享的会话和应用程序特定的信息填充 Session 实例。

当 WebSocket 端点收到消息时,将调用以 @OnMessage 批注的方法。以 @OnMessage 批注的方法可以包含以下参数:

  • javax.websocket.Session 参数。
  • 零个或更多个使用 @PathParam 批注的字符串参数,指向端点路径上的路径参数。
  • 消息本身。要概要了解可能的消息类型,请参见下文。

当其他同级节点发送了一条文本消息时,将通过以下代码片段输出消息内容:

@OnMessage
public void myOnMessage (String txt) {
   System.out.println ("WebSocket received message: "+txt);
} 

如果以 @OnMessage 批注的方法的返回类型不是 void,则 WebSocket 实现将向其他同级节点发送返回值。以下代码片段将以大写形式将收到的文本消息返回发送方:

@OnMessage
public String myOnMessage (String txt) {
   return txt.toUpperCase();
} 

下面显示了通过 WebSocket 连接发送消息的另一种方式:

RemoteEndpoint.Basic other = session.getBasicRemote();
other.sendText ("Hello, world");

在此方法中,我们先从 Session 对象开始,可以通过生命周期回调方法(例如,以 @OnOpen 批注的方法)获取。Session 实例上的 getBasicRemote() 方法返回 WebSocket 另一部分的表示 RemoteEndpoint。该 RemoteEndpoint 实例可用于发送文本或其他类型的消息,如下所述。

当 WebSocket 连接关闭时,将调用带 @OnClose 批注的方法。此方法可以接受以下参数:

  • javax.websocket.Session 参数。注意,一旦 @OnClose 批注的方法返回,WebSocket 真正关闭时,就不能再使用此参数。
  • javax.websocket.CloseReason 参数,描述关闭 WebSocket 的原因,如正常关闭、协议错误、服务过载等等。
  • 零个或更多个使用 @PathParam 批注的字符串参数,指向端点路径上的路径参数。

以下代码片段将输出 WebSocket 关闭的原因:

@OnClose
public void myOnClose (CloseReason reason) {
   System.out.prinlnt ("Closing a WebSocket due to "+reason.getReasonPhrase());
}

最后,还有一个生命周期批注:如果收到错误,将调用带 @OnError 批注的方法。

接口驱动的方法

批注驱动的方法允许使用生命周期批注为 Java 类和方法添加批注。使用接口驱动的方法,开发人员扩展 javax.websocket.Endpoint 并改写 onOpenonCloseonError 方法:

public class myOwnEndpoint extends javax.websocket.Endpoint {
   public void onOpen(Session session, EndpointConfig config) {...}
   public void onClose(Session session, CloseReason closeReason) {...}
   public void onError (Session session, Throwable throwable) {...}
}

为了拦截消息,需要在 onOpen 实现中注册 javax.websocket.MessageHandler

public void onOpen (Session session, EndpointConfig config) {
   session.addMessageHandler (new MessageHandler() {...});
}

MessageHandler 是一个接口,包含 MessageHandler.PartialMessageHandler.Whole 两个子接口。当开发人员希望收到有关消息部分提交的通知时,应使用 MessageHandler.Partial 接口,并应使用的 MessageHandler.Whole 的实现来获取完整消息到达的通知。

以下代码片段监听传入文本消息,并将文本消息的大写版本发回另一个同级节点:

public void onOpen (Session session, EndpointConfig config) {
   final RemoteEndpoint.Basic remote = session.getBasicRemote();
   session.addMessageHandler (new MessageHandler.Whole<String>() {
      public void onMessage(String text) {
                 try {
                     remote.sendString(text.toUpperCase());
                 } catch (IOException ioe) {
                     // handle send failure here
                 }
             }

   });
}

消息类型、编码器和解码器

Java API for WebSocket 非常强大,因为它允许以 WebSocket 消息的形式发送或接收任何 Java 对象。

基本上有三种不同类型的消息:

  • 基于文本的消息
  • 二进制消息
  • Pong 消息,关于 WebSocket 连接本身

使用接口驱动的模型时,每个会话可以为三种不同类型的消息中的每一种注册至多一个 MessageHandler

使用批注驱动的模型时,对于每种不同类型的消息,允许一种 @onMessage 批注方法。允许在批注方法中指定消息内容的参数依赖于消息的类型。

@OnMessage 批注 的 Javadoc 基于消息类型明确指定允许的消息参数(以下引自 Javadoc):

  • “如果该方法在处理文本消息:

    • String 接收整个消息
    • Java 基元或类等同于接收转换为该类型的整个消息
    • String 和布尔对分部分接收消息
    • Reader 以阻塞流的形式接收整个消息
    • 端点有文本解码器(Decoder.TextDecoder.TextStream)的任何对象参数。
  • 如果该方法在处理二进制消息:

  • 如果该方法在处理 pong 消息:

任何 Java 对象均可使用编码器编码成基于文本的消息或二进制消息。这种基于文本的消息或二进制消息被传输到另一个同级节点,然后再次被解码成 Java 对象,也可以由另一个 WebSocket 库解释。XML 或 JSON 通常用于传输 WebSocket 消息,编码将 Java 对象编组成 XML 或 JSON,解码将 XML 或 JSON 解组成 Java 对象。

编码器定义为 javax.websocket.Encoder 接口的实现,解码器是 javax.websocket.Decoder 接口的实现。有时,端点实例需要了解有哪些可能的编码器和解码器。使用批注驱动的方法,将通过 @ClientEndpoint@ServerEndpoint 批注中的编码器和解码器元素传递编码器和解码器的列表。

清单 1 中的代码显示如何注册一个 MessageEncoder 类,以定义 MyJavaObject 实例到文本消息的转换。对于相反转换,则注册 MessageDecoder 类。

@ServerEndpoint(value="/endpoint", encoders = MessageEncoder.class, decoders= MessageDecoder.class)
public class MyEndpoint {
...
}

class MessageEncoder implements Encoder.Text<MyJavaObject> {
   @override
   public String encode(MyJavaObject obj) throws EncodingException {
      ...
   }
}

class MessageDecoder implements Decoder.Text<MyJavaObject> {
   @override 
   public MyJavaObject decode (String src) throws DecodeException {
      ...
   }

   @override 
   public boolean willDecode (String src) {
      // return true if we want to decode this String into a MyJavaObject instance
   }
}

清单 1

Encoder 接口有一些子接口:

  • Encoder.Text,用于将 Java 对象转换成文本消息
  • Encoder.TextStream,用于向字符流添加 Java 对象
  • Encoder.Binary,用于将 Java 对象转换成二进制消息
  • Encoder.BinaryStream,用于向二进制流添加 Java 对象

类似地,Decoder 接口有四个子接口:

  • Decoder.Text,用于将文本消息转换成 Java 对象
  • Decoder.TextStream,用于从字符流读取 Java 对象
  • Decoder.Binary,用于将二进制消息转换成 Java 对象
  • Decoder.BinaryStream,用于从二进制流读取 Java 对象

总结

Java API for WebSocket 为 Java 开发人员提供了与 IETF WebSocket 标准集成的一个标准 API。为此,利用任何 WebSocket 实现的 Web 客户端或本机客户端可以轻松与 Java 后端通信。

Java API 可配置性强,非常灵活,允许 Java 开发人员使用自己喜好的模式。

另请参见

关于作者

Johan Vos 于 1995 年开始使用 Java。他是 LodgON 的创始人之一,为社交网络软件开发基于 Java 的解决方案。Vos 爱好嵌入式和企业开发,专注于使用 JavaFX 和 Java EE 的端到端 Java。更多信息,请访问他的博客或关注 http://twitter.com/johanvos

分享交流

请在 FacebookTwitterOracle Java 博客上加入 Java 社区对话!