atmosphere js
在此《 JAX Magazine》教程中,JeanFrancois Arcand向我们介绍了Atmosphere Framework,这是一个Java / Javascript框架,允许使用Groovy,Scala和Java创建可移植的异步应用程序。
Atmosphere Framework随附有一个JavaScript组件,该组件支持所有现代浏览器,并且还包含几个服务器组件,这些组件支持所有主要的基于Java的WebServer。 该框架的目的是允许开发人员编写应用程序,并让该框架透明地发现客户端和服务器之间的最佳通信渠道。
例如,开发人员可以编写与支持该协议的浏览器或服务器一起使用时将使用WebSocket协议的应用程序,并在不支持WebSocket协议的情况下透明地回退到HTTP。 例如,一个Atmosphere应用程序可以通过HTTP与Internet Explorer 6、7、8和9正常运行,并且与Internet Explorer 10一起使用时可以使用WebSocket协议。
要了解Atmosphere的强大功能,让我们构建一个简单的聊天应用程序。 假设我们的聊天应用程序仅支持一个聊天室,以简化逻辑。
首先,让我们编写服务器端组件。 气氛支持四个组成部分:
大气运行时间:大气层的核心模块。 所有其他模块均基于此模块。 该模块提供了两个用于构建应用程序的简单API: AtmosphereHandler和Meteor 。 AtmosphereHandler是实现的简单接口,而Meteor API是可以在基于Servlet的应用程序中检索或注入的类。
大气球衣:对Jersey REST框架的扩展。 该模块公开了一组新的注释,从而公开了Atmosphere的运行时功能。
privacy-gwt:对GWT框架的扩展。
服务器端
在本文中,我将使用大气运行时来演示编写一个简单的异步应用程序有多么简单。 让我们从使用AtmosphereHandler的服务器组件开始。 AtmosphereHandler的定义如下面的清单1所示。
清单1:AtmosphereHandler
public interface AtmosphereHandler {
void onRequest(AtmosphereResource resource) throws IOException;
void onStateChange(AtmosphereResourceEvent event) throws IOException;
void destroy();
}
每当将请求映射到与AtmosphereHandler关联的路径时,都会调用onRequest方法。 通过注释AtmosphereHandler的实现来定义路径。
@AtmosphereHandlerService(path = “/<path>”)
在Atmosphere中, AtmosphereResource表示物理连接。 AtmosphereResource可用于检索有关请求的信息,对响应执行操作,更重要的是,可用于在onRequest执行期间挂起连接。 WebServer必须知道什么时候需要保持连接开放以进行将来的操作(例如,用于WebSocket),以及何时需要升级连接以支持协议,例如http(流,长轮询,jsonp或服务器端事件)。 。
onStateChange方法( 图1 )将在以下情况下由Atmosphere调用:
发生广播操作,需要采取措施。 广播者总是启动广播操作。 可以看作是沟通的渠道。 应用程序可以创建许多通信渠道,并使用BroadcasterFactory类检索它们。 AtmosphereResource始终与一个或多个广播者关联。 我们还可以将广播者视为事件队列,每次广播新事件时,您都可以在其中收听并得到通知。 广播可以从onRequest , onStateChange或服务器端的任何位置进行。
连接已关闭或超时(没有活动发生)。
在视觉上可以看作是:最后,当取消部署或停止Atmosphere时,将调用destroy方法。
复杂? 对于我们来说幸运的是,该框架附带了AtmosphereHandlers ,几乎可以在所有场景中使用它,这使开发人员可以在已处理连接生命周期的同时专注于应用程序逻辑。 让我们使用OnMessage <T> AtmosphereHandler 编写我们的应用程序( 清单2 )。
@AtmosphereHandlerService(
path="/chat",
interceptors = {AtmosphereResourceLifecycleInterceptor.class,
BroadcastOnPostAtmosphereInterceptor.class})
public class ChatRoom extends OnMessage<String> {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public void onMessage(AtmosphereResponse response, String message) throws IOException {
response.getWriter()
.write(mapper.writeValueAsString(mapper.readValue(message, Data.class)));
}
}
这里的主要思想是将连接生命周期尽可能多地委托给Atmosphere的即用组件。 首先,我们使用@AtmosphereHandlerService注释对ChatRoom类进行注释,并定义路径和拦截器。 AtmosphereInterceptor可以看作总是在AtmosphereHandler#onRequest之前和之后调用的过滤器。 AtmosphereInterceptor可用于处理请求/响应,处理生命周期等。例如,暂停和广播( 图2 )。
如上所述,可以使用两个拦截器来首先暂停请求( AtmosphereResourceLifeCycleInterceptor ),然后广播在每个POST上接收的数据( BroadcastOnPostAtmosphereInterceptor )。 太好了,我们只能专注于应用程序的逻辑。
现在,不用编写自己完整的AtmosphereHandler ,我们可以扩展OnMessage <T>处理程序,该处理程序将广播操作委托给onMessage方法(第10行)。 对于我们的聊天应用程序,这仅意味着我们写我们收到的内容(第11行)。 如果我们有50个已连接的用户,则意味着onMessage将被调用50次,以便50个用户获得该消息。
我们在客户端和服务器之间使用JSON。 客户发送:
{"message":"Hello World","author":"John Doe"}
并将服务器发送回连接的浏览器
{"message":"Hello World","author":"John Doe","time":1348578675087}
在第11行上,我们使用Jackson库读取消息并将其写回,并增加了接收消息的时间。 Data类只是一个简单的POJO( 清单3 )。
清单3:数据类
public final static class Data {
private String message;
private String author;
private long time;
public Data() {
this("","");
}
public Data(String author, String message) {
this.author = author;
this.message = message;
this.time = new Date().getTime();
}
public String getMessage() {
return message;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public void setMessage(String message) {
this.message = message;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
}
客户端–atmosphere.js
服务器端就这样。 现在,让我们使用atmote.js编写客户端。 首先,让我们看一下代码( 清单4 )。
清单4:Atmosphere.js客户端代码
$(function () {
"use strict";
var header = $('#header');
var content = $('#content');
var input = $('#input');
var status = $('#status');
var myName = false;
var author = null;
var logged = false;
var socket = $.atmosphere;
var subSocket;
var transport = 'websocket';
// We are now ready to cut the request
var request = { url: document.location.toString() + 'chat',
contentType : "application/json",
trackMessageSize: true,
shared : true,
transport : transport ,
fallbackTransport: 'long-polling'};
request.onOpen = function(response) {
content.html($('<p>', { text: 'Atmosphere connected using ' + response.transport }));
input.removeAttr('disabled').focus();
status.text('Choose name:');
transport = response.transport;
if (response.transport == "local") {
subSocket.pushLocal("Name?");
}
};
request.onTransportFailure = function(errorMsg, request) {
jQuery.atmosphere.info(errorMsg);
if (window.EventSource) {
request.fallbackTransport = "sse";
transport = "see";
}
header.html($('<h3>', { text: 'Atmosphere Chat. Default transport is WebSocket, fallback is ' + request.fallbackTransport }));
};
request.onMessage = function (response) {
// We need to be logged first.
if (!myName) return;
var message = response.responseBody;
try {
var json = jQuery.parseJSON(message);
} catch (e) {
console.log('This doesn't look like a valid JSON: ', message.data);
return;
}
if (!logged) {
logged = true;
status.text(myName + ': ').css('color', 'blue');
input.removeAttr('disabled').focus();
subSocket.pushLocal(myName);
} else {
input.removeAttr('disabled');
var me = json.author == author;
var date = typeof(json.time) == 'string' ? parseInt(json.time) : json.time;
addMessage(json.author, json.message, me ? 'blue' : 'black', new Date(date));
}
};
request.onClose = function(response) {
logged = false;
}
subSocket = socket.subscribe(request);
input.keydown(function(e) {
if (e.keyCode === 13) {
var msg = $(this).val();
if (author == null) {
author = msg;
}
subSocket.push(jQuery.stringifyJSON({ author: author, message: msg }));
$(this).val('');
input.attr('disabled', 'disabled');
if (myName === false) {
myName = msg;
}
}
});
function addMessage(author, message, color, datetime) {
content.append('<p><span style="color:' + color + '">' + author + '</span> @ ' +
+ (datetime.getHours() < 10 ? '0' + datetime.getHours() : datetime.getHours()) + ':'
+ (datetime.getMinutes() < 10 ? '0' + datetime.getMinutes() : datetime.getMinutes())
+ ': ' + message + '</p>');
}
});
清单4中的代码中还有很多其他内容,因此让我们仅描述moonlight.js的重要部分。 首先,我们初始化一个连接(在代码中称为套接字 ):
var socket = $.atmosphere;
下一步是定义一些回调函数。 对于本文,我们仅定义一个子集。 首先,我们定义一个onOpen函数,该函数在基础传输连接到服务器时被调用( 第24行 )。 在那里,我们仅显示用于连接到服务器的传输。 传输在请求对象上指定,定义为:
var request = { url: document.location.toString() + 'chat',
contentType : "application/json",
transport : transport ,
fallbackTransport: 'long-polling'};
在这里,我们要默认使用WebSocket传输,如果浏览器或服务器不支持WebSocket,则回退到长轮询。 在我们的onOpen函数中,我们仅显示了使用了哪种传输方式。
注意:当WebSocket出现故障时,您还可以通过添加onTransportFailure函数来更改传输方式:
request.onTransportFailure = function(errorMsg, request) {
if (window.EventSource) {
request.fallbackTransport = "sse";
transport = "see";
}
在此处出于演示目的,我们将查找EventSource对象(HTML5服务器端事件),如果可用,请切换传输以使用它。 这里的好处是:您不需要使用特殊的API。 使用atmote.js以相同方式处理所有传输。
接下来,我们定义onMessage函数,每次我们从服务器接收数据时都会调用该函数
request.onMessage = function (response) {
…..
}
在这里,我们只显示收到的消息。 要连接并将数据发送到服务器,我们要做的就是调用:
subSocket = socket.subscribe(request);
订阅后,我们就可以接收和发送数据了。 为了发送数据,我们使用从订阅操作返回的subSocket对象。 如果正在使用WebSocket传输,则subSocket将引用WebSocket连接(因为该协议是双向的),对于所有其他传输,每次调用push操作都将打开一个新连接:
subSocket.push(jQuery.stringifyJSON({ author: author, message: msg }));
接下来,让我们添加对一个非常好的Atmosphere功能的支持,该功能可以在打开的窗口/标签之间共享连接。 在Atmosphere中您需要做的就是在执行请求时将共享变量设置为“ true”:
var request = { url: document.location.toString() + 'chat',
contentType : "application/json",
transport : transport ,
shared : true,
fallbackTransport: 'long-polling'};
现在,每次打开新窗口或标签并打开同一页面时,将共享连接。 要在“主”标签/窗口(首先打开的打开)时得到通知,只需实施
request.onLocalMessage = function(message) {
….
}
Tabs / Windows也可以使用以下功能直接进行通信。
subSocket.pushLocal(…)
功能齐全–不仅如此!
就是这样,我们现在有了一个功能齐全的聊天应用程序。 但是当前应用存在两个问题。 第一个与代理/防火墙有关。 有时,代理/防火墙不允许连接长时间保持不活动状态,并且通常代理会自动关闭连接。 对于挂起的连接,这意味着每次关闭连接时客户端都必须重新连接。 一种可能的解决方案是通过在客户端和服务器之间发送一些字节来保持挂起的连接处于活动状态。 对我们来说幸运的是,我们需要做的就是添加HeartbeatInterceptor ,它将透明地保持连接对我们的活动( 清单5 )。
清单5:HeartbeatInterceptor
@AtmosphereHandlerService(
path = "/chat",
interceptors = {AtmosphereResourceLifecycleInterceptor.class,
BroadcastOnPostAtmosphereInterceptor.class,
HeartbeatInterceptor.class})
public class ChatRoom extends OnMessage<String> {
现在, HeartbeatInterceptor将定期向连接写入字节(空白)以使其保持活动状态。 不幸的是,仍然有一些代理可以在一段时间(活动与否)之后关闭连接,否则可能会出现网络问题,并且浏览器将不得不重新连接。
在重新连接过程中,总是会发生广播操作,并且由于连接正在进行连接,因此浏览器可能永远无法获得广播。 在这种情况下,这意味着浏览器错过了一条消息(或丢失了一条消息)。 对于某些应用程序,这可能不是问题,但是对于某些丢失的消息而言,这是一个主要问题。
幸运的是,Atmosphere支持BroadcasterCache的概念。 安装BroadcasterCache将使浏览器永远不会丢失/丢失消息。 当浏览器重新连接时,Atmosphere将始终在缓存中查找并确保在重新连接期间发生的所有消息都被发送回浏览器。 BroadcasterCache API是可插入的,并且Atmosphere附带了现成的实现。 因此,对于我们的聊天应用程序,我们需要做的是:
@AtmosphereHandlerService(
path = "/chat",
broadcasterCache = HeaderBroadcasterCache.class,
interceptors = {AtmosphereResourceLifecycleInterceptor.class,
BroadcastOnPostAtmosphereInterceptor.class,
HeartbeatInterceptor.class})
public class ChatAtmosphereHandler extends OnMessage<String> {
现在保证我们的应用程序不会丢失或丢失任何消息。 我们需要解决的第二个问题是混合消息,具体取决于所使用的WebServer。 浏览器可能会在一小块中接收到两条消息,一条消息会接收一半,等等。这是有问题的,因为假设我们使用JSON对消息进行编码,那么浏览器将无法解码以下形式的消息:
{"message":"Hello World","author":"John Doe","time":1348578675087}{"message":"Cool Man","author":"Foo Bar","time":1348578675087}
要么
{"message":"Hello World","author":"John Doe
要么
{"message":"Hello World","author":"John Doe","time":1348578675087}{"message":"Cool Man","author"
浏览器收到此类消息时,将无法对其进行解码
var json = jQuery.parseJSON(message);
为了解决这个问题,我们需要安装TrackMessageSizeInterceptor ,它将为消息添加一些提示,并且浏览器将能够使用这些提示来确保始终使用有效消息来调用moistry.js onMessage函数( 清单6)。 )。
清单6:
@AtmosphereHandlerService(
path = "/chat",
broadcasterCache = HeaderBroadcasterCache.class,
interceptors = {AtmosphereResourceLifecycleInterceptor.class,
BroadcastOnPostAtmosphereInterceptor.class,
TrackMessageSizeInterceptor.class,
HeartbeatInterceptor.class})
public class ChatRoom extends OnMessage<String> {
在客户端,我们要做的就是在请求对象上设置trackMessageLength 。
到云!
现在,我们准备将应用程序部署到云中……还没有。 我们需要添加的下一个功能是在云中部署消息时如何在服务器之间分配消息。 我们需要解决的问题如图3所示 。
图3:云中的服务器
在这种情况下,当在Tomcat Server 1上执行广播操作时,Tomcat Server 2将永远不会收到消息。 对于我们的应用程序,这意味着某些用户将看不到其他消息,这显然是一个主要问题。 我们不仅需要聊天,还需要部署到云中的任何应用程序都需要解决该问题。
对我们来说幸运的是,Atmosphere支持“启用云”或“启用集群”的Broadcaster,可用于在服务器实例之间传播消息。 Atmosphere当前本地支持Redis PubSub,Hazelcast,JGroups,JMS,XMPP等知名技术(例如,使用Gmail服务器)。 对于本文,我们使用Redis PubSub( 图4 )。
图4:Redis PubSub
Redis PubSub允许我们连接到Redis实例并订阅一些主题。 对于我们的应用程序,我们要做的就是创建一个“聊天”主题并将所有服务器订阅到该主题。 接下来,我们只需要告诉我们的应用程序使用RedisBroadcaster而不是普通的Broadcaster。 与清单7一样简单。
清单7:
@AtmosphereHandlerService(
path = "/chat",
broadcasterCache = HeaderBroadcasterCache.class,
broadcaster = RedisBroadcaster.class,
interceptors = {AtmosphereResourceLifecycleInterceptor.class,
BroadcastOnPostAtmosphereInterceptor.class,
TrackMessageSizeInterceptor.class,
HeartbeatInterceptor.class})
public class ChatRoom extends OnMessage<String> {
通过仅添加RedisBroadcaster,我们就启用了服务器之间的消息共享,使我们的聊天应用程序在一行中具有“云感知”能力。 在客户端,我们无需更改任何内容。 我们现在有一个功能齐全的应用程序:
透明地支持所有现有的WebServer
透明地支持所有现有浏览器
启用云/集群
我们的应用程序将首先协商在客户端和服务器之间使用的最佳传输方式。 例如,假设我们使用Jetty 8进行部署,则将使用以下传输方式
Chrome 21:WebSockets
Internet Explorer 9:长轮询
FireFox 15:服务器端事件
Safari / iOS 6:WebSockets
Internet Explorer 10:WebSockets
Android 2.3:长轮询
FireFox 3.5:长轮询
所有这些透明地允许开发人员专注于应用程序,而不是传输/可移植性问题。
结论与注意事项
WebSocket和服务器端事件是正在兴起的技术,它们在企业中的采用正在加速。 跳入之前要考虑的一些事项:
- API是否可移植,例如,它是否可以在所有知名的WebServer上运行?
- 该框架是否已提供传输回退机制? 例如,Internet Explorer 7/8/9都不支持WebSocket和服务器端事件,对于我们来说不幸的是,这些浏览器仍在广泛使用。
- 框架云是否已启用,更重要的是会扩展吗?
- 编写应用程序容易吗,框架是否建立良好?
显然,“大气层框架”是对这四个真正重要问题的回应。 还有疑问吗? 好吧,请转到《 华尔街日报》 ,打开页面并查找Wordnik的徽标。 每天有超过6000万个请求,而所有这些都由Atmosphere Framework提供支持! 从今天开始,请访问我们的网站 !
作者简介:
Jeanfrançois在软件工程领域工作了18年。 他学习纯数学,并在加拿大研究中心工作,使用C ++进行数学建模,直到有人向他介绍一种叫做Java的新语言。 他从未停止使用它。 在编写第一个NIO框架 Grizzly 之前,Jeanfrançois在Sun Microsystems工作了近10年, Jeanfrançois还开发了 Grizzly Comet Framework ,这是实现异步Web应用程序的早期方法。 然后 , 他启动了 Atmosphere Framework ,该 框架 带来了跨Servlet容器的可移植性,并允许创建WebSocket和Comet应用程序。 可以在Twitter上关注他,网址为 http://twitter.com/jfarcand
本文发表于10月的《 JAX杂志:大气1.0》中。 有关该问题和先前的后一页, 请单击此处 。
Flickr图片由El Brown提供
翻译自: https://jaxenter.com/tutorial-atmosphere-1-0-websocket-portability-on-the-jvm-105517.html
atmosphere js