作者: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 (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
在本示例中,服务器应用程序用 Java 编写,WebSocket 协议的细节由 Java EE 7 容器中包含的 JSR 356 实现来处理。
JavaFX 客户端可依赖任何符合 JSR 356 的客户端实现来处理 WebSocket 特定的协议问题。其他客户端(例如,iOS 客户端和 HTML5 客户端)可以使用符合 RFC 6455 的其他(非 Java)实现与服务器应用程序通信。
定义 JSR 356 的专家组希望支持 Java EE 开发人员常用的模式和技术。因此,JSR 356 利用了批注和注入。
通常支持两种不同的编程模型:
Endpoint
接口以及与生命周期事件交互的方法。典型的 WebSocket 交互生命周期事件如下所示:
大多数 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。@ClientEndpoint
与 ServerEndpoint
之间的主要区别是 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
并改写 onOpen
、onClose
和 onError
方法:
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.Partial
和 MessageHandler.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 对象。
基本上有三种不同类型的消息:
使用接口驱动的模型时,每个会话可以为三种不同类型的消息中的每一种注册至多一个 MessageHandler
。
使用批注驱动的模型时,对于每种不同类型的消息,允许一种 @onMessage
批注方法。允许在批注方法中指定消息内容的参数依赖于消息的类型。
@OnMessage
批注 的 Javadoc 基于消息类型明确指定允许的消息参数(以下引自 Javadoc):
String
接收整个消息String
和布尔对分部分接收消息Reader
以阻塞流的形式接收整个消息Decoder.Text
或 Decoder.TextStream
)的任何对象参数。byte[]
或 ByteBuffer
接收整个消息byte[]
和布尔对或 ByteBuffer
和布尔对分部分接收消息InputStream
以阻塞流的形式接收整个消息Decoder.Binary
或 Decoder.BinaryStream
)的任何对象参数。PongMessage
用于处理 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。
请在 Facebook、Twitter 和 Oracle Java 博客上加入 Java 社区对话!