Статьи

Написание переносимого HTML5-приложения WebSocket с использованием Atmosphere Framework

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

Как и в случае с 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 <b>onRequest</b>(AtmosphereResource<F,G> event) throws IOException;

public void <b>onStateChange</b>(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.<b>setBroascaster</b>(BroadcasterFactory.getDefault()
.<b>get</b>(DefaultBroadcaster.class,"MyEventQueue");
}

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

public void upgrade(AtmosphereResource
<HttpServletRequest, HttpServletResponse> r) throws IOException
{
// Upgrade
super(r);
// Escape all malicious chars
r.<b>setBroascaster</b>(BroadcasterFactory.getDefault()
.<b>lookup</b>(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><b>org.atmosphere.websocket.WebSocketAtmosphereHandler</b></param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>AtmosphereServlet</servlet-name>
<b><url-pattern>/*</url-pattern></b>
</servlet-mapping>
</web-app>

Теперь, что нам действительно нужно дальше, это серверная часть AtmosphereHandler. Но поскольку по умолчанию WebSocketAtmosphereHandler транслирует события на все приостановленные соединения, то для чата (отдельной комнаты) нам ничего не нужно делать, например, нам просто нужно развернуть .war и все. Все, что отправляет браузер, будет транслироваться на все приостановленные / обновленные соединения.

Клиентская сторона

Для клиентской части и для демонстрации портативности приложения Atmosphere давайте просто постыдно скопируем чат Jetty index.html, не внося никаких изменений:

    <script type='text/javascript'>

if (!<b>window.WebSocket</b>)
<b>alert("WebSocket not supported by this browser");</b>

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:');
<b> this._ws = new WebSocket(location);
this._ws.onopen = this._onopen;
this._ws.onmessage = this._onmessage;
this._ws.onclose = this._onclose;</b>
},

_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)
<b>this._ws.send(user + ':' + message);</b>
},

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) {
<b>this._ws = null;</b>
$('join').className = '';
$('joined').className = 'hidden';
$('username').focus();
$('chat').innerHTML = '';
}
};

Я выделил жирным шрифтом важную часть, которая является WebSocket.onopen, WebSocket.onmessage и WebSocket.onclose. Другой пример можно взять из примера Grizzly WebSocket:

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 = '';

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

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

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

Как и на стороне сервера, на стороне клиента также довольно просто. Отсюда вы можете более подробно рассмотреть весь код приложения .

Что дальше!

Как и в случае с Comet, мы добавим поддержку нативной поддержки WebSocket, когда они будут доступны. Смола — следующая, и затем, основываясь на том, что хотят люди, мы можем попробовать Netty. Мы также работаем над библиотекой JQuery, которая будет автоматически определять, поддерживает ли браузер и сервер WebSocket, и если один из них или оба не поддерживают метод Comet Technique (подсказка: мы ищем гуру JQuery для помощи!)

Что еще интереснее, скоро мы сможем написать приложение REST, которое работает поверх WebSocket от Atmosphere. Вскоре (в моем следующем блоге) вы сможете определить приложение REST-Jersey-WebSocket, выполнив:

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

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

Вышеприведенный код может быть поддержан в Atmosphere 0.6 GA, но наверняка пробьется в 0.7. Я также буду работать над добавлением поддержки WebSocket в Akka . Выглядит многообещающе!

По любым вопросам или для загрузки Atmosphere перейдите на наш основной сайт и воспользуйтесь нашим форумом Nabble (подписка не требуется) или подписывайтесь на команду или меня и пишите там свои вопросы! Вы также можете проверить код на Github . Или загрузите нашу последнюю презентацию, чтобы получить общее представление о структуре.


Оригинальный блог размещен на http://jfarcand.wordpress.com/