前言
把该框架用于网页聊天。效果很好,推荐给大家
原文
介绍
Atmosphere是一个Java/ JavaScript框架,它允许使用Groovy,Scala和Java。Atmosphere带有JavaScript组件,支持所有现代浏览器以及主要的基于Java的Web服务器。框架的目的是允许开发者编写的应用程序,并让该框架发现客户端和服务器之间的最佳通信信道。
例如,开发人员可以编写将使用WebSocket协议用于其该协议的浏览器和服务器或者使用http当遇到不支持WebSocket协议的应用程序。例如,Atmosphere应用将正常使用Internet Explorer6,7,8,9使用HTTP,并与Internet Explorer10使用时将使用WebSocket协议。
我们构建一个简单的聊天应用程序。假设我们的聊天应用只支持单一的聊天室,使逻辑更简单。首先,让我们写服务器端组件。Atmosphere原生支持三个组成部分:
- atmosphere-runtime:核心模块。所有其他模块建立在这个之上。该模块暴露构建应用两个简单的API:AtmosphereHandler和Meteor。该AtmosphereHandler是一个简单的接口工具,而Meteor API是可以检索或基于Servlet的应用注入。
- atmosphere-jersey: 一个扩展 Jersey REST框架。该模块公开了一组新的注解,从而暴露atmosphere的运行时功能。
服务端
在这篇文章中,我将使用atmosphere中运行时证明它是多么简单写一个简单的异步应用程序。让我们先从使用AtmosphereHandler的服务器组件。该AtmosphereHandler的定义如清单1所示。
Listing 1: AtmosphereHandler
public interface AtmosphereHandler {
void onRequest(AtmosphereResource resource) throws IOException;
void onStateChange(AtmosphereResourceEvent event) throws IOException;
void destroy();
}
该onRequest方法被调用每一个请求被映射到与AtmosphereHandler关联的路径的时间。路径由注释AtmosphereHandler的实施方案所定义。
@AtmosphereHandlerService(path = "/{path}")
在Atmosphere中,AtmosphereResource代表的物理连接。一个AtmosphereResource可以用于检索有关请求的信息,对响应执行的动作,以及更重要的是可用于保持onRequest执行期间的连接。一个WebServer需要被告知,当连接需要继续开放,为未来的行动(如的WebSockets),并且,当需要升级到支持该协议,用于HTTP(流,长轮询,JSONP或服务器端事件)保持连接开放为未来的行动。
onStateChange 被调用的情况:
发生广播操作和需要采取的动作。Broadcaster总是启动一个广播操作。它可以被看作是通信信道。应用程序可以创建多个沟通渠道,并使用BroadcasterFactory类检索。一个AtmosphereResource总是与一个或几个广播相关联。我们还可以看到广播作为一个事件队列,在那里你可以聆听并得到通知每一个新的事件被广播的时间。广播可以从onRequest,onStateChange或者在服务器侧的任何地方发生。该连接已关闭或超时(它没有发生活动)。
最后运行的方法–destroy method, 当Atmosphere 是未展开或停止调用。 复杂?幸运的是,该框架附带AtmosphereHandlers可以在几乎所有情况下,它允许开发人员专注于应用程序逻辑,同时它已经处理连接的生命周期中使用。让我们使用的onMessage
Listing 2: OnMessage
@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.write(mapper.writeValueAsString(mapper.readValue(message, Data.class)));
}
}
这里的主要思想是尽可能多地连接生命周期的委托,以Atmosphere’s 的准备使用的组件。首先,我们标注的@AtmosphereHandlerService标注聊天室类,并定义路径和拦截器。AtmosphereInterceptors可以被看作是始终被前后AtmosphereHandler#onRequest后调用过滤器。AtmosphereInterceptor是用于操纵请求/响应,处理生命周期,等等。例如,暂停和广播(图2)是有用的。
如上所述,二拦截器可以用于第一,暂停请求(AtmosphereResourceLifeCycleInterceptor),然后广播上的每个(POST BroadcastOnPostAtmosphereInterceptor)接收的数据。太好了,我们可以只专注于应用程序的逻辑。现在,而不是写我们自己的完整AtmosphereHandler,我们可以扩展的onMessage处理程序,它代表广播操作onMessage方法。对于我们的聊天应用,它只是意味着我们写什么,我们收到。如果我们有50连接的用户来说,这意味着在onMessage将被称为50次这样的50个用户获取消息。我们使用的客户端和服务器之间的JSON。客户端发送的:
{"message":"Hello World","author":"John Doe"}
服务发给浏览器:
{"message":"Hello World","author":"John Doe","time":1348578675087}
我们用Jackson 库读取msg,并将其写回,以得到接收msg的时间增加。数据类只是一个简单的POJO(清单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
Listing 4: Atmosphere.js Client Code
$(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",
trackMessageLength : 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中的代码多余的,所以让我们只描述atmosphere.js重要组成部分。首先,我们初始化连接
var socket = $.atmosphere;
下一步骤是定义一些功能的回调。在这篇文章中,我们只定义一个子集。首先,我们定义当底层传输连接到服务器被调用一个的OnOpen功能。那里我们只是显示用于连接到服务器的传输。传输被请求对象,其被定义为在指定的:
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。所有传输的处理使用atmosphere.js方式相同。
接下来我们定义的onMessage功能,这将是我们每次从服务器接收数据时调用:
request.onMessage = function (response) {
.....
}
在这里,我们只显示所接收的消息。连接和发送数据到服务器,我们需要做的就是调用:
subSocket = socket.subscribe(request);
订阅后,我们已经准备好让接收和发送数据。要发送的数据,我们使用从订阅操作返回的subSocket对象。如果WebSocket的传输是在使用中,subSocket将引用WebSocket连接(因为协议是双向),其中对于所有其他传输,一个新的连接将在每次按压操作被调用时打开:·
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'};
现在每次一个新的窗口或标签被打开同一个页面打开后,连接将被共享。得到通知时,“master”标签/窗口(即在开首开),只需实现
request.onLocalMessage = function(message) {
....
}
分页/视窗也可以直接通过使用以下功能进行通信。
subSocket.pushLocal(...)
完整的功能
就这样,我们现在有一个全功能的聊天应用程序。但有两个问题。第一个是与代理/防火墙。代理/防火墙不允许连接到保持长时间非活动状态,并且通常连接得到由代理自动关闭。对于挂起的连接,这意味着客户将不得不重新每一个连接被关闭的时间。一种可能的解决办法是通过发送在客户端和服务器之间的一些字节,以保持悬浮连接活性。幸运的是,我们需要做的就是添加HeartbeatInterceptor(清单5)。
Listing 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 = UUIDBroadcasterCache.class,
interceptors = { AtmosphereResourceLifecycleInterceptor.class,
BroadcastOnPostAtmosphereInterceptor.class,
HeartbeatInterceptor.class
})
public class ChatAtmosphereHandler extends OnMessage<String> {
我们的应用程序现在已保证不会错过或失去了消息。我们需要解决的第二个问题是混杂的消息,根据所使用的网络服务器。该浏览器可能收到两条短信在一个或者一个半的消息块。假设我们使用JSON编码我们的信息,浏览器将无法解码下面形式的消息:
{"message":"Hello World","author":"John Doe","time":1348578675087}{"message":"Cool Man","author":"FooBar","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,将一些提示添加到邮件,浏览器将能够使用这些提示,以确保atmosphere.js的onMessage函数总是用一个有效的调用消息(清单6)。
@AtmosphereHandlerService( path = "/chat",
broadcasterCache = UUIDBroadcasterCache.class,
interceptors = { AtmosphereResourceLifecycleInterceptor.class,
BroadcastOnPostAtmosphereInterceptor.class,
TrackMessageSizeInterceptor.class,
HeartbeatInterceptor.class
})
public class ChatRoom extends OnMessage<String> {
为了解决这个问题,我们需要安装TrackMessageSizeInterceptor,将一些提示添加到消息,浏览器将能够使用这些提示,以确保atmosphere.js的onMessage函数总是用一个有效的调用消息(清单6)。
var request = { url: document.location.toString() + 'chat',
contentType : "application/json",
logLevel : 'debug',
shared : true,
transport : transport ,
trackMessageLength : true,
fallbackTransport: 'long-polling'};
To The Cloud!
我们现在已经准备好部署我们的应用程序到云……嗯,还没有。接下来的功能,我们需要补充的是如何在云中部署时消息得到分布在服务器上。我们需要解决的问题在图3中可以看到。
在那种情况下,当广播动作Tomcat服务器1上发生,Tomcat服务器2将永远不会得到的消息。对于我们的应用程序,这意味着一些用户将无法看到其他消息,这显然是一个重大问题。不仅是聊天,还包括云中的任何应用程序,我们需要解决这个问题。幸运的是,Atmosphere 支持“云启用”或可用于传播服务器实例之间的消息“启用群集的”广播。目前,Atmosphere 本身支持(使用Gmail服务器为例)众所周知的技术,如Redis的PubSub的,Hazelcast,JGroups的,JMS,XMPP。在这篇文章中,我们将使用Redis的PubSub的(图4)。
Redis的PubSub的允许我们连接到一个Redis的实例,并订阅了一些话题。对于我们的应用程序,我们需要做的是建立一个“聊天”的主题,我们所有的服务器订阅它。接下来我们只需要告诉我们的应用程序使用的RedisBroadcaster。就这么简单清单7。
@AtmosphereHandlerService(path = "/chat",
broadcasterCache = UUIDBroadcasterCache.class,
broadcaster = RedisBroadcaster.class,
interceptors = { AtmosphereResourceLifecycleInterceptor.class,
BroadcastOnPostAtmosphereInterceptor.class,
TrackMessageSizeInterceptor.class,
HeartbeatInterceptor.class
})
public class ChatRoom extends OnMessage<String> {
只要加上RedisBroadcaster我们刚启用服务器之间的信息共享,使我们的聊天应用“云感知”。在客户端,我们没有改变任何东西。我们现在有一个全功能的应用程序:
- 透明地支持所有现有的Web服务器
- 透明地支持所有现有的浏览器
- 支持云/群集
用@ManagedService
白如何构建Atmosphere 应用和它是如何工作的引擎盖下,你可以通过使用@ManagedService简化你的服务器端类
@ManagedService(path = "/chat")
public class Chat {
private final Logger logger = LoggerFactory.getLogger(Chat.class);
@Ready
public void onReady(final AtmosphereResource r) {
logger.info("Browser {} connected.", r.uuid());
}
@Disconnect
public void onDisconnect(AtmosphereResourceEvent event) {
if (event.isCancelled()) {
logger.info("Browser {} unexpectedly disconnected", event.getResource().uuid());
} else if (event.isClosedByClient()) {
logger.info("Browser {} closed the connection", event.getResource().uuid());
}
}
@org.atmosphere.config.service.Message(encoders = {JacksonEncoder.class}, decoders = {JacksonDecoder.class})
public Message onMessage(Message message) throws IOException {
logger.info("{} just send {}", message.getAuthor(), message.getMessage());
return message;
}
}
该@ManagedService包含上述所有操作,并且不要求实现任何atmosphere的接口。
支持的浏览器以及transports
我们的应用程序会先协商客户端和服务器之间使用的最佳交通工具。例如,假设我们部署Jetty 8,下面的传输将被使用:
1. Chrome 21 : WebSockets
2. Internet Explorer 9 : Long-Polling
3. FireFox 15: Server Side Events
4. Safari/iOS 6: WebSockets
5. Internet Explorer 10: WebSockets
6. Android 2.3: Long-Polling
7. FireFox 3.5 : Long-Polling
结论和注意事项
WebSockets和服务器端事件技术正处于上升阶段,被越来越多人接受。不过有些东西也要了解清楚:
- 是否API便携吗,例如会在知名的WebServer的工作吗?
- 是否已经提供了一个传输回退机制?例如,IE浏览器7/8/9既不支持的WebSockets和服务器端的事件,不幸的是对我们来说,这些浏览器仍在广泛使用。
- 是否启用了云架构,更重要的是,它会扩展?
- 是否容易编写应用,是框架确立?
很明显atmosphere可以解决以上问题