Atmosphere框架--网页聊天

前言

把该框架用于网页聊天。效果很好,推荐给大家
原文

介绍

Atmosphere是一个Java/ JavaScript框架,它允许使用Groovy,Scala和Java。Atmosphere带有JavaScript组件,支持所有现代浏览器以及主要的基于Java的Web服务器。框架的目的是允许开发者编写的应用程序,并让该框架发现客户端和服务器之间的最佳通信信道。

例如,开发人员可以编写将使用WebSocket协议用于其该协议的浏览器和服务器或者使用http当遇到不支持WebSocket协议的应用程序。例如,Atmosphere应用将正常使用Internet Explorer6,7,8,9使用HTTP,并与Internet Explorer10使用时将使用WebSocket协议。

我们构建一个简单的聊天应用程序。假设我们的聊天应用只支持单一的聊天室,使逻辑更简单。首先,让我们写服务器端组件。Atmosphere原生支持三个组成部分:

  1. atmosphere-runtime:核心模块。所有其他模块建立在这个之上。该模块暴露构建应用两个简单的API:AtmosphereHandler和Meteor。该AtmosphereHandler是一个简单的接口工具,而Meteor API是可以检索或基于Servlet的应用注入。
  2. 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我们刚启用服务器之间的信息共享,使我们的聊天应用“云感知”。在客户端,我们没有改变任何东西。我们现在有一个全功能的应用程序:

  1. 透明地支持所有现有的Web服务器
  2. 透明地支持所有现有的浏览器
  3. 支持云/群集

用@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和服务器端事件技术正处于上升阶段,被越来越多人接受。不过有些东西也要了解清楚:

  1. 是否API便携吗,例如会在知名的WebServer的工作吗?
  2. 是否已经提供了一个传输回退机制?例如,IE浏览器7/8/9既不支持的WebSockets和服务器端的事件,不幸的是对我们来说,这些浏览器仍在广泛使用。
  3. 是否启用了云架构,更重要的是,它会扩展?
  4. 是否容易编写应用,是框架确立?

很明显atmosphere可以解决以上问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值