学习如何在你的应用程序中集成WebSockets.
Published April 2013
对于许多基于客户端-服务器程序来说,老的HTTP 请求-响应模型已经有它的局限性. 信息必须通过多次请求才能将其从服务端传送到客户端.
过去许多的黑客使用某些技术来绕过这个问题,例如:长轮询(long polling)、基于 HTTP 长连接的服务器推技术(Comet).
然而,基于标准的、双向的、客户端和服务器之间全双工的信道需求再不断增加。
在2011年, IETF发布了标准WebSocket协议-RFC 6455. 从那时起,大多数Web浏览器都实现了支持WebSocket协议的客户端APIs.同时,许多Java 包也开始实现了WebSocket协议.
WebSocket协议利用HTTP升级技术来将HTTP连接升级到WebSocket. 一旦升级后,连接就有了在两个方向上相互独立(全双式)发送消息(数据桢)的能力.
不需要headers 或cookies,这大大降低了所需的带宽. 通常,WebSockets来周期性地发送小消息 (例如,几个字节).
额外的headers常常会使开销大于有效负载(payload)。
JSR 356
JSR 356, WebSocket的Java API, 明确规定了API,当Java开发者需要在应用程序中集成WebSocket时,就可以使用此API—服务端和客户端均可. 每个声明兼容JSR 356的WebSocket协议,都必须实现这个API.
因此,开发人员可以自己编写独立于底层WebSocket实现的WebSocket应用。这是一个巨大的好处,因为它可以防止供应商锁定,并允许更多的选择、自由的库、应用程序服务器。
JSR 356是即将到来的java EE 7标准的一部分,因此,所有与Java EE 7兼容的应用服务器都有JSR 365标准WebSocket的实现.一旦建立,WebSocket客户端和服务器节点已经是对称的了。客户端API与服务器端API的区别是很小的,JSR 356定义的Java client API只是Java EE7完整API的子集.
客户段-服务器端程序使用WebSockets,通常会包含一个服务器组件和多个客户端组件, 如图1所示:
图1
在这个例子中,server application 是通过Java编写的,WebSocket 协议细节是由包含在Java EE 7容器中JSR 356 实现来处理的.
JavaFX 客户端可依赖任何与JSR 356兼容的客户端实现来处理WebSocket协议问题.
其它客户端(如,iOS 客户端和HTML5客户端)可使用其它 (非Java)与RFC6455兼容的实现来与server application通信.
编程模型
JSR 356定义的专家小组,希望支持Java EE开发人员常用的模式和技术。因此,JSR 356使用了注释和注入。
一般来说,支持两种编程模型:
- 注解驱动(annotation-driven). 通过使用注解POJOs, 开发者可与WebSocket生命周期事件交互.
- 接口驱动(interface-driven). 开发者可实现
Endpoint接口和与生命周期交互的方法.
生命周期事件
典型的WebSocket 交互生命周期如下:
- 一端 (客户端) 通过发送HTTP握手请求来初始化连接.
- 其它端(服务端) 回复握手响应.
- 建立连接.从现在开始,连接是完全对称的.
- 两端都可发送和接收消息.
- 其中一端关闭连接.
大部分WebSocket生命周期事件都与Java方法对应,不管是 annotation-driven 还是interface-driven.
Annotation-Driven 方式
接受WebSocket请求的端点可以是以 @ServerEndpoint
注解的POJO.
此注解告知容器,此类应该被认为是WebSocket端点.
必须的value
元素指定了WebSocket端点的路径.
考虑下面的代码片断:
@ServerEndpoint("/hello") public class MyEndpoint { }
此代码将会以相对路径hello来发布一个端点.在后续方法调用中,此路径可携带路径参数,如: /hello/{userid}是一个有效路径,在这里
{userid}
的值,可在生命周期方法使用@PathParam
注解获取.
在GlassFish中,如果你的应用程序是用上下文mycontextroot
部署的,且在localhost的8080端口上监听
, WebSocket可通过使用ws://localhost:8080/mycontextroot/hello来访问
.
初始化WebSocket连接的端点可以是以 @ClientEndpoint
注解的POJO.@ClientEndpoint
和 @ServerEndpoint的主要区别是
ClientEndpoint
不接受路径路值元素,因为它监听进来的请求。
@ClientEndpoint public class MyClientEndpoint {}
Java中使用注解驱动POJO方式来初始化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
实例包含了关于端点配置的信息- 0个或多个以
@PathParam注解的
字符串参数,指的是端点路径的path参数
下面的方法实现了当打开WebSocket时,将会打印session的标识符:
@OnOpen public void myOnOpen (Session session) { System.out.println ("WebSocket opened: "+session.getId()); }
Session实例只要WebSocket未关闭就会一直有效
. Session类中包含了许多有意思的方法,以允许开发者获取更多关于的信息
。
同时,Session
也包含了应用程序特有的数据钩子,即通过getUserProperties()
方法来返回 Map<String, Object>
.
这允许开发者可以使用session-和需要在多个方法调用间共享的应用程序特定信息来填充Session
实例.
i当WebSocket端收到消息时,将会调用以@OnMessage
注解的方法.以@OnMessage
注解的方法可包含下面的参数:
javax.websocket.Session
参数.- 0个或多个以
@PathParam注解的
字符串参数,指的是端点路径的path参数 - 消息本身. 下面有可能消息类型描述.
当其它端发送了文本消息时,下面的代码片断会打印消息内容:
@OnMessage public void myOnMessage (String txt) { System.out.println ("WebSocket received message: "+txt); }
如果以@OnMessage
i注解的方法返回值不是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
参数. 注意,一旦WebSocket真正关闭了,此参数就不能被使用了,这通常发生在@OnClose
注解方法返回之后.- A
javax.websocket.CloseReason
参数,用于描述关闭WebSocket的原因,如:正常关闭,协议错误,服务过载等等. - 0个或多个以
@PathParam注解的
字符串参数,指的是端点路径的path参数
下面的代码片段打印了WebSocket关闭的原因:
@OnClose public void myOnClose (CloseReason reason) { System.out.prinlnt ("Closing a WebSocket due to "+reason.getReasonPhrase()); }
完整情况下,这里还有一个生命周期注解:如果收到了错误,将会调用 @OnError
注解的方法。
Interface-Driven 方式
annotation-driven 方式允许我们注解一个Java类,以及使用生命周期注解来注解方法.
使用interface-driven方式,开发者可继承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 } } }); }
消息类型,编码器,解码器
WebSocket的JavaAPI非常强大,因为它允许发送任或接收任何对象作为WebSocket消息.
基本上,有三种不同类型的消息:
- 基于文本的消息
- 二进制消息
- Pong 消息,它是WebSocket连接自身
当使用interface-driven模式,每个session最多只能为这三个不同类型的消息注册一个MessageHandler
.
当使用annotation-driven模式,针对不同类型的消息,只允许出现一个@onMessage
注解方法. 在注解方法中,消息内容中允许的参数依赖于消息类型。
Javadoc for the @OnMessage
annotation 明确指定了消息类型上允许出现的消息参数:
- "如果方法用于处理文本消息:
- 如果方法用于处理二进制消息:
- 如果方法是用于处理pong消息:
任何Java对象使用编码器都可以编码为基于文本或二进制的消息.这种基于文本或二进制的消息将转输到其它端点,在其它端点,它可以解码成Java对象-或者被另外的WebSocket 包解释.
通常情况下,XML或JSON用于来传送WebSocket消息, 编码/解码然后会将Java对象编组成XML或JSON并在另一端解码为Java对象.
encoder是以javax.websocket.Encoder
接口的实现来定义,decoder是以javax.websocket.Decoder
接口的实现来定义的.
有时,端点实例必须知道encoders和decoders是什么.使用annotation-driven方式, 可向@ClientEndpoint
和 @ServerEndpoint
l注解中的encode和decoder元素传递 encoders和decoders的列表。
Listing 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 } }
Listing 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对象
结论
WebSocket Java API为Java开发者提供了标准API来集成IETF WebSocket标准.通过这样做,Web 客户端或本地客户端可使用任何WebSocket实现来轻易地与Java后端通信。
Java Api是高度可配置的,灵活的,它允许java开发者使用他们喜欢的模式。
也可参考
posted on 2016-07-24 01:35
胡小军 阅读(2736)
评论(0) 编辑 收藏 所属分类:
WebSocket