Статьи

Написание переносимых приложений WebSocket для HTML5 с использованием инфраструктуры Atmosphere

Атмосфера Framework теперь поддерживает спецификацию HTML5 WebSocket . Если вы не знаете, что такое WebSocket, я рекомендую вам взглянуть на это введение . Как и в случае Ajax Push / Comet, все основные веб-серверы начинают поддерживать эту спецификацию. И угадайте, что, все веб-серверы делают это по-своему. Звучит знакомо?

Еще в 2006 году Jetty впервые представила API-интерфейс Continuation, за которым следовал мой Grizzly Comet Framework , и в итоге мы увидели собственную реализацию Tomcat AIO, Resin Comet и JBossWeb AIO. Прошло почти четыре года, прежде чем Comet стал стандартизирован с помощью немного более сложного Servlet 3.0 Async API . В настоящее время происходит точно такая же картина, например, Jetty, Grizzly / GlassFish и Resin теперь поддерживают WebSocket, и опять же нет переносимости через WebServer. Следовательно, если вы планируете создать приложение WebSocket, убедитесь, что вы выбрали правильный WebServer, поскольку ваше приложение не будет переносимым, и вам, возможно, придется подождать еще четыре года, прежде чем спецификация сервлета подтянется :-).

Как и в случае с Ajax Push / Comet, среда Atmosphere Framework может спасти вашу жизнь, предоставляя API-интерфейсы, переносимые через WebServer. Первоначально целью Atmosphere Framework было обеспечение переносимости Ajax Push / Comet через WebServer. В настоящее время вы можете написать приложение Comet и развернуть его в своем любимом WebServer и быть уверенным, что приложение будет работать. Новым является то, что Atmosphere Framework также поддерживает переносимость WebSocket через WebServer. Yaaa !! В настоящее время мы поддерживаем Jetty, Grizzly и вскоре Resin и добавим поддержку, как только появится реализация WebSocket. Также есть Netty, которую я бы хотел поддержать. Так что не зацикливайтесь на собственном API WebServer, начните с правильной ноги, используя Atmosphere Framework!Событие более интересное: в настоящее время вы можете развернуть приложение WebSocket вашего Atmosphere на WebServer, который не поддерживает WebSocket, и ваше приложение будет работать как есть, единственной частью, которую нужно будет изменить, будет сторона клиента. Но это

временно, так
как наша будущая клиентская библиотека на основе Atmosphere JQuery будет поддерживать обнаружение WebSocket и Comet.

Сторона сервера

Во-первых, давайте вспомним некоторые концепции атмосферы, поскольку они одинаковы независимо от используемой технологии, например, WebSocket или Comet:

  • Приостановить . Действие приостановки состоит в том, чтобы указать базовому веб-серверу не фиксировать ответ, например, не отправлять обратно в браузер последние байты, которые браузер ожидает, прежде чем считать запрос завершенным.
  • Возобновить : Действие возобновления состоит из завершения ответа, например, отправки ответа путем отправки обратно браузеру последних байтов, которые браузер ожидает, прежде чем считать запрос завершенным.
  • Широковещательная рассылка. Действие широковещательной передачи заключается в создании события и его распределении по одному или нескольким приостановленным ответам. Приостановленный ответ может затем решить отменить событие или отправить его обратно в браузер.
  • Длинный опрос : Длинный опрос состоит в возобновлении приостановленного ответа, как только событие транслируется.
  • Потоковая передача по протоколу Http. Потоковая передача по протоколу Http, также называемая навсегда, состоит из возобновления приостановленного ответа после трансляции нескольких событий.
  • Собственный асинхронный API . Собственный асинхронный API (Comet или WebSocket) означает собственный API, например, если вы пишете приложение с использованием этого API, приложение не будет переносимым через веб-сервер.

В атмосфере одна из основных концепций называется AtmosphereHandler.

AtmosphereHandler может использоваться для приостановки, возобновления и широковещания, а также позволяет использовать обычный набор API-интерфейсов HttpServletRequest и HttpServletResponse. Обратите внимание, что Atmosphere 0.7 также будет поддерживать не Servlet API, как в Netty, Play !, и т. Д.

public interface AtmosphereHandler<F,G>
{
  public void onRequest(AtmosphereResource<F,G> event) throws IOException;

  public void onStateChange(AtmosphereResourceEvent<F,G> event) throws IOException;
}

OnRequest вызывается каждый раз, когда запрос соответствует сопоставлению сервлета AtmosphereServlet. Так что в случае приложения WebSocket вы обычно используете значение / *, которое означает, что все запросы будут отправлены в AtmosphereServlet, который, в свою очередь, вызовет AtmosphereHandler.onRequest (). В Атмосфере Ресурс Атмосферы инкапсулирует все доступные операции. Я настоятельно рекомендую вам быстро взглянуть на Белую книгу Атмосферы для получения дополнительной информации об основных классах фреймворка.

Теперь для WebSocket, я добавил реализацию этого интерфейса, называемую WebSocketAtmosphereHandler, чтобы представить некоторые соглашения о конфигурации. Обратите внимание, что вы можете написать свой собственный, если этот класс не делает то, что вы хотите. По умолчанию WebSocketAtmosphereHandler выполняется просто:

public void upgrade(AtmosphereResource
<HttpServletRequest, HttpServletResponse> r) throws IOException
{
r.suspend();
}

Приложение WebSocket обычно должно только переопределить метод upgrade () и решить, что делать с запросом. По умолчанию этот обработчик будет предполагать, что первый запрос относится к статическому ресурсу, такому как index.html, и перенаправит запрос соответствующему компоненту WebServer. Далее, если браузер поддерживает WebSocket, он отправит еще один запрос с просьбой обновить сервер до протокола Websocket. С Атмосферой все, что вам нужно сделать, это вызвать AtmosphereResource.suspend (). Это тот же API, который вы обычно используете в Comet, чтобы сказать WebServer «приостановить» ответ и не фиксировать его, пока вы не возобновите его. С WebSocket, обновление делает то же самое, но более «формально».

После того, как ответ был приостановлен, независимо от Comet или WebSocket, вы готовы транслировать события этому открытому соединению. Трансляция — это просто механизм уведомления, который отправляет события обратно в браузер, используя приостановленное соединение. Одна из фундаментальных концепций Атмосферных Рамок называется вещателем. Broadcaster может использоваться для трансляции (или возврата назад) асинхронных событий в набор или подмножество приостановленных ответов. Концепция довольно близка к очереди или теме JMS. Broadcaster может содержать ноль или более BroadcastFilter, который можно использовать для преобразования событий до того, как они будут записаны обратно в браузер. Например, любые вредоносные символы могут быть отфильтрованы до их обратной записи, добавив XSSHtmlFilter, как описано ниже.

public void upgrade(AtmosphereResource
         <HttpServletRequest, HttpServletResponse>  r) throws IOException
{
   // Upgrade
   super(r);
   // Escape all malicious chars
   r.getBroadcaster().getBroadcasterConfig()
              .addFilter(new XSSHtmlFilter());
}

Внутренне Broadcaster использует ExecutorService для выполнения вышеуказанной цепочки вызовов. Это означает, что вызов Broadcaster.broadcast (..) не будет блокироваться, если вы не используете возвращенный Future API, и будет использовать набор выделенных потоков для выполнения трансляции. Таким образом, вы выдвигаете события асинхронно. По умолчанию каждый раз, когда браузер выдает WebSocket.onmessage, что означает отправку сообщения, это сообщение будет транслироваться всем обновленным соединениям. Иными словами, все, отправленное браузером, будет отражено обратно с использованием приостановленных / обновленных подключений.

Последнее слово о Broadcaster: по умолчанию вещатель будет транслировать все объекты AtmosphereResource, для которых был приостановлен ответ, или обновить WebSocket, например, был вызван AtmosphereResource.suspend (). Это поведение настраивается, и вы можете настроить его, вызвав Broadcaster.setScope ():

  • ЗАПРОС: трансляция событий только в AtmosphereResource, связанный с текущим запросом.
  • APPLICATION: транслировать события на все ресурсы AtmosphereResource, созданные для текущего веб-приложения.
  • VM: транслировать события на весь AtmosphereResource, созданный внутри текущей виртуальной машины.

По умолчанию используется приложение. Следовательно, внутри метода обновления вы можете определить свой собственный Broadcaster и его область действия в зависимости от того, что делает ваше приложение. Например, вы можете захотеть иметь одну очередь на приостановленное / обновленное соединение:

public void upgrade(AtmosphereResource
         <HttpServletRequest, HttpServletResponse>  r) throws IOException
{
   // Upgrade
   super(r);
   // Escape all malicious chars
   r.setBroascaster(BroadcasterFactory.getDefault()
       .get(DefaultBroadcaster.class,"MyEventQueue");
}

В случае выше, события будут транслироваться на одно соединение браузера. Вы всегда можете добавить еще один, просто выполнив во время обновления:

public void upgrade(AtmosphereResource
         <HttpServletRequest, HttpServletResponse>  r) throws IOException
{
   // Upgrade
   super(r);
   // Escape all malicious chars
   r.setBroascaster(BroadcasterFactory.getDefault()
       .lookup(DefaultBroadcaster.class,"MyEventQueue");
}

Известное приложение чата

Теперь давайте напишем наше первое приложение WebSocket, известный чат! Во-первых, давайте определим наш web.xml как:

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:j2ee="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
        http://java.sun.com/xml/ns/j2ee/web-app_2.5.xsd">

    <description>Atmosphere Chat</description>
    <display-name>Atmosphere Chat</display-name>
    <servlet>
        <description>AtmosphereServlet</description>
        <servlet-name>AtmosphereServlet</servlet-name>
        <servlet-class>org.atmosphere.cpr.AtmosphereServlet</servlet-class>
        <load-on-startup>0</load-on-startup>
        <init-param>
            <param-name>org.atmosphere.websocket.WebSocketAtmosphereHandler</param-name>
            <param-value>true</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>AtmosphereServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

Now what we really need next is the server side AtmosphereHandler. But since by default the WebSocketAtmosphereHandler broadcast events to all suspended connections, then for a chat (single room) we don’t have to do anything, e.g we just need to deploy the .war and that’s it. Everything a Browser will send will be broadcasted to all suspended/upgraded connections.

The client side

For the client side, and to demonstrate how portable an Atmosphere application is, let’s just shamelessly copy the Jetty’s Chat index.html without making any changes:

  <script type='text/javascript'>

        if (!window.WebSocket)
            alert("WebSocket not supported by this browser");

        function $() {
            return document.getElementById(arguments[0]);
        }
        function $F() {
            return document.getElementById(arguments[0]).value;
        }

        function getKeyCode(ev) {
            if (window.event) return window.event.keyCode;
            return ev.keyCode;
        }

        var room = {
            join: function(name) {
                this._username = name;
                var location = document.location.toString()
                          .replace('http:', 'ws:');
                this._ws = new WebSocket(location);
                this._ws.onopen = this._onopen;
                this._ws.onmessage = this._onmessage;
                this._ws.onclose = this._onclose;
            },

            _onopen: function() {
                $('join').className = 'hidden';
                $('joined').className = '';
                $('phrase').focus();
                room._send(room._username, 'has joined!');
            },

            _send: function(user, message) {
                user = user.replace(':', '_');
                if (this._ws)
                    this._ws.send(user + ':' + message);
            },

            chat: function(text) {
                if (text != null && text.length &;gt 0)
                    room._send(room._username, text);
            },

            _onmessage: function(m) {
                if (m.data) {
                    var c = m.data.indexOf(':');
                    var from = m.data.substring(0, c)
                      .replace('&;lt', '<').replace('&;gt', '>');
                    var text = m.data.substring(c + 1)
                      .replace('&;lt', '<').replace('&;gt', '>');

                    var chat = $('chat');
                    var spanFrom = document.createElement('span');
                    spanFrom.className = 'from';
                    spanFrom.innerHTML = from + ': ';
                    var spanText = document.createElement('span');
                    spanText.className = 'text';
                    spanText.innerHTML = text;
                    var lineBreak = document.createElement('br');
                    chat.appendChild(spanFrom);
                    chat.appendChild(spanText);
                    chat.appendChild(lineBreak);
                    chat.scrollTop = chat.scrollHeight - chat.clientHeight;
                }
            },

            _onclose: function(m) {
                this._ws = null;
                $('join').className = '';
                $('joined').className = 'hidden';
                $('username').focus();
                $('chat').innerHTML = '';
            }
        };

I’ve put in bold the important piece, which is WebSocket.onopen, WebSocket.onmessage and WebSocket.onclose.Another example can be taken from the Grizzly WebSocket sample:

var app = {
    url: document.location.toString().replace('http:', 'ws:');,
    initialize: function() {
        if ("WebSocket" in window) {
            $('login-name').focus();
            app.listen();
        } else {
            $('missing-sockets').style.display = 'inherit';
            $('login-name').style.display = 'none';
            $('login-button').style.display = 'none';
            $('display').style.display = 'none';
        }
    },
    listen: function() {
        $('websockets-frame').src = app.url + '?' + count;
        count ++;
    },
    login: function() {
        name = $F('login-name');
        if (! name.length > 0) {
            $('system-message').style.color = 'red';
            $('login-name').focus();
            return;
        }
        $('system-message').style.color = '#2d2b3d';
        $('system-message').innerHTML = name + ':';

        $('login-button').disabled = true;
        $('login-form').style.display = 'none';
        $('message-form').style.display = '';

        websocket = new WebSocket(app.url);
        websocket.onopen = function() {
            // Web Socket is connected. You can send data by send() method
            websocket.send('login:' + name);
        };
        websocket.onmessage = function (evt) {
            eval(evt.data);
            $('message').disabled = false;
            $('post-button').disabled = false;
            $('message').focus();
            $('message').value = '';
        };
        websocket.onclose = function() {
            var p = document.createElement('p');
            p.innerHTML = name + ': has left the chat';

            $('display').appendChild(p);

            new Fx.Scroll('display').down();
        };
    },

As with the server side, the client side is also fairly simple. You can take a closer look the entire application’s code from here.

What’s Next!

As with Comet, we will add support native WebSocket support when they will be available. Resin is the next one, and then based on what people wants, we may try Netty. We are also working on a JQuery library that will auto detect if the browser and the server support WebSocket, and if one or both aren’t, fall to use a Comet Technique (hint: we are looking for JQuery guru for help!)

More interesting is soon we will be able to write REST application that runs on top of Atmosphere’s WebSocket. Soon (in my next upcoming blog) you should be able to define REST-Jersey-WebSocket application by doing:

@GET
@Path("/")
public WebSocketUpgrade<String> upgrade(@PathParam("message") String message)
{
       return r = new WebSocketUpgrade.WebSocketUpgradeBuilder()
                .entity(message)
                .scope(Suspend.SCOPE.REQUEST)
                .resumeOnBroadcast(true)
                .period(30, TimeUnit.SECONDS)
                .build();
}

@Produces("application/xml")
@OnBroadcast
public Broadcastable publishWithXML(@FormParam("message") String message)
{
        return new Broadcastable(new JAXBBean(message));
}

The above code will maybe supported in Atmosphere 0.6 GA, but will for sure make its way in 0.7. I will also work on adding WebSocket support to Akka. Looks promising!

For any questions or to download Atmosphere, go to our main site and use our Nabble forum (no subscription needed) or follow the team or myself and tweet your questions there! You can also checkout the code on Github. Or download our latest presentation to get an overview of what the framework is.


Originally posted at http://jfarcand.wordpress.com/