背景
我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。
Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务器端一个HTTP长连接,服务器端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小了很多。
对于许多基于客户端-服务器程序来说,老的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实例.
当WebSocket端收到消息时,将会调用以@OnMessage注解的方法.以@OnMessage注解的方法可包含下面的参数:
- javax.websocket.Session参数.
- 0个或多个以@PathParam注解的字符串参数,指的是端点路径的path参数
- 消息本身. 下面有可能消息类型描述.
当其它端发送了文本消息时,下面的代码片断会打印消息内容:
@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参数。注意,一旦WebSocket真正关闭了,此参数就不能被使用了,这通常发生在@OnClose注解方法返回之后.
- 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) {
}
} });
}
消息类型,编码器,解码器
WebSocket的JavaAPI非常强大,因为它允许发送任或接收任何对象作为WebSocket消息。基本上,有三种不同类型的消息:
- 基于文本的消息
- 二进制消息
- Pong 消息,它是WebSocket连接自身
当使用interface-driven模式,每个session最多只能为这三个不同类型的消息注册一个MessageHandler。
当使用annotation-driven模式,针对不同类型的消息,只允许出现一个@onMessage注解方法. 在注解方法中,消息内容中允许的参数依赖于消息类型。
Javadoc for the @OnMessage annotation明确指定了消息类型上允许出现的消息参数:
- 如果方法用于处理文本消息:
- String用于接收整个消息
- Java 原型或等价的类用于接收整个消息并将其转换为此类型
- String和 boolean 对用于部分接收消息
- Reader用于以阻塞流的方式接收整个消息
- 端点的任何对象参数存在文本解码器 (Decoder.Text 或 Decoder.TextStream).
- 如果方法用于处理二进制消息:
- byte[]或ByteBuffer用于接收整个消息
- byte[]和 boolean 对,或者ByteBuffer和boolean对用于部分接收消息
- InputStream用于按阻塞流的方式接收整个消息
- 端点的任何对象参数存在二进制解码器(Decoder.Binary or Decoder.BinaryStream).
- 如果方法是用于处理pong消息:
- PongMessage 用于处理pong消息"
任何Java对象使用编码器都可以编码为基于文本或二进制的消息.这种基于文本或二进制的消息将转输到其它端点,在其它端点,它可以解码成Java对象-或者被另外的WebSocket 包解释.
通常情况下,XML或JSON用于来传送WebSocket消息,编码/解码然后会将Java对象编组成XML或JSON并在另一端解码为Java对象。
encoder是以javax.websocket.Encoder接口的实现来定义,decoder是以javax.websocket.Decoder接口的实现来定义的。
有时,端点实例必须知道encoders和decoders是什么?使用annotation-driven方式, 可向@ClientEndpoint和@ServerEndpointl注解中的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) {
}
}
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开发者使用他们喜欢的模式。
同时JavaEE 7中出了jsr-356:Java API for WebSocket规范。不少Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat从7.0.27开始支持WebSocket,从7.0.47开始支持JSR-356,tomcat8真正支持jsr-356(包含对websocket的支持),tomcat7部分版本的websocket实现不兼容jsr-356, websocket实现tomcat7.x与tomcat8.x有很大差异。
1、在tomcat7中使用websocket需要定义一个servlet,然后继承WebSocketServlet。
2、在tomcat8中使用websocke,在JavaEE规范集中我们这里主要看Java API for WebSocket(JSR 356)。Websocket-api提供了Java实现Websocket的接口,其中最重要的几个类和注解:
- 4个注解OnClose,OnError, OnOpen, OnMessage用来标注一个POJO用来处理WebSocket请求的方法;
- Endpoint和EndpointConfig分别定义了端点和端点相关配置的接口方法;
- ClientEndpoint和ServerEndpoint分别定义了客户端和服务器端端点的接口方法;
- Decoder和Encoder分别是解码和编码的接口方法定义;
- Session是与Endpoint相关的WebSocket Session接口方法定义。(此处session不是servlet的session)
另外还有其它接口,这里作为初步了解Java websocket api仅列出最有必要的一些。
Tomcat8.x提供了JavaEE7的标准实现,其中WebSocket 1.1规范给予实现。在使用其开发的依赖环境是Tomcat8.x JDK7。Tomcat8.x提供了服务器端的实现,客户端实现需要借助其他实现如java_websocket。Tomcat8.x对WebSocket实现感觉很明朗化了,既然API中定义了WebSocket相关的注解和Session那么Tomcat8.x实现中自然会有相应的处理和实现。
- tomcat的WsSession类实现了Java WebSocket API中的Session接口
- PojoEndpointBase以及其子类处理与Endpoint相关的类或注解EndpointConfig,Endpoint都与Session的实现类之间存在依赖关系
通过了解Java Websocket API和Tomcat8.x对其的实现,认为使用Java WebSocket需要熟悉其中关键类或接口如:Endpoint,EndpointConfig,Client和Server,Encoder和Decoder,Session,MessageHandler。