Один из комментариев, который я получаю об Atmosphere, заключается в том, почему я должен использовать фреймворк вместо ожидания Servlet 3.0 Async API. Что ж, все просто: намного проще, работает с любым существующим Java WebServer (включая Google App Engine!) И автоматически обнаружит асинхронный API Servlet 3.0, если вы развернете свое приложение на WebServer, который его поддерживает.
Чтобы сделать честное сравнение, давайте напишем привет мир Comet, приложения чата и сравним код на стороне сервера. Без технических деталей давайте просто отбросим весь код сервера. Во-первых, версия Servlet 3.0 (возможно, немного оптимизированная):
package web.servlet.async_request_war; import java.io.IOException; import java.io.PrintWriter; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet(urlPatterns = {"/chat"}, asyncSupported = true) public class AjaxCometServlet extends HttpServlet { private static final Queue<AsyncContext> queue = new ConcurrentLinkedQueue<AsyncContext>(); private static final BlockingQueue<String> messageQueue = new LinkedBlockingQueue<String>(); private static final String BEGIN_SCRIPT_TAG = "<script type='text/javascript'>\n"; private static final String END_SCRIPT_TAG = "</script>\n"; private static final long serialVersionUID = -2919167206889576860L; private Thread notifierThread = null; @Override public void init(ServletConfig config) throws ServletException { Runnable notifierRunnable = new Runnable() { public void run() { boolean done = false; while (!done) { String cMessage = null; try { cMessage = messageQueue.take(); for (AsyncContext ac : queue) { try { PrintWriter acWriter = ac.getResponse().getWriter(); acWriter.println(cMessage); acWriter.flush(); } catch(IOException ex) { System.out.println(ex); queue.remove(ac); } } } catch(InterruptedException iex) { done = true; System.out.println(iex); } } } }; notifierThread = new Thread(notifierRunnable); notifierThread.start(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/html"); res.setHeader("Cache-Control", "private"); res.setHeader("Pragma", "no-cache"); PrintWriter writer = res.getWriter(); // for IE writer.println("<!-- Comet is a programming technique that enables web servers to send data to the client without having any need for the client to request it. -->\ n"); writer.flush(); req.setAsyncTimeout(10 * 60 * 1000); final AsyncContext ac = req.startAsync(); queue.add(ac); req.addAsyncListener(new AsyncListener() { public void onComplete(AsyncEvent event) throws IOException { queue.remove(ac); } public void onTimeout(AsyncEvent event) throws IOException { queue.remove(ac); } }); } @Override @SuppressWarnings("unchecked") protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); res.setHeader("Cache-Control", "private"); res.setHeader("Pragma", "no-cache"); req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); String name = req.getParameter("name"); if ("login".equals(action)) { String cMessage = BEGIN_SCRIPT_TAG + toJsonp("System Message", name + " has joined.") + END_SCRIPT_TAG; notify(cMessage); res.getWriter().println("success"); } else if ("post".equals(action)) { String message = req.getParameter("message"); String cMessage = BEGIN_SCRIPT_TAG + toJsonp(name, message) + END_SCRIPT_TAG; notify(cMessage); res.getWriter().println("success"); } else { res.sendError(422, "Unprocessable Entity"); } } @Override public void destroy() { queue.clear(); notifierThread.interrupt(); } private void notify(String cMessage) throws IOException { try { messageQueue.put(cMessage); } catch(Exception ex) { throw new IOException(ex); } } private String escape(String orig) { StringBuffer buffer = new StringBuffer(orig.length()); for (int i = 0; i < orig.length(); i++) { char c = orig.charAt(i); switch (c) { case '\b': buffer.append("\\b"); break; case '\f': buffer.append("\\f"); break; case '\n': buffer.append("<br />"); break; case '\r': // ignore break; case '\t': buffer.append("\\t"); break; case '\'': buffer.append("\\'"); break; case '\"': buffer.append("\\\""); break; case '\\': buffer.append("\\\\"); break; case '<': buffer.append("<"); break; case '>': buffer.append(">"); break; case '&': buffer.append("&"); break; default: buffer.append(c); } } return buffer.toString(); } private String toJsonp(String name, String message) { return "window.parent.app.update({ name: \"" + escape(name) + "\", message: \"" + escape(message) + "\" });\n"; } }
Хорошо, теперь с Атмосферой , тот же код состоит из:
package org.atmosphere.samples.chat.resources; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MultivaluedMap; import org.atmosphere.annotation.Broadcast; import org.atmosphere.annotation.Schedule; import org.atmosphere.annotation.Suspend; import org.atmosphere.util.XSSHtmlFilter; @Path("/") public class ResourceChat { @Suspend @GET @Produces("text/html;charset=ISO-8859-1") public String suspend() { return ""; } @Broadcast({XSSHtmlFilter.class, JsonpFilter.class}) @Consumes("application/x-www-form-urlencoded") @POST @Produces("text/html;charset=ISO-8859-1") public String publishMessage(MultivaluedMap form) { String action = form.getFirst("action"); String name = form.getFirst("name"); if ("login".equals(action)) { return ("System Message" + "__" + name + " has joined."); } else if ("post".equals(action)) { return name + "__" + form.getFirst("message"); } else { throw new WebApplicationException(422); } } }
Хорошо, так в чем же дело? Что делает Атмосферу такой легкой? Новый Async API Servlet 3.0 предлагает:
- Метод приостановки ответа, HttpServletRequest.startAsync ()
- Метод возобновления ответа: AsyncContext.complete ()
Атмосфера предлагает:
- Аннотация для приостановки: @Suspend
- Аннотация или резюме: @Resume
- Аннотация для трансляции (или отправки) событий на набор приостановленных ответов: @Broadcast
- Аннотация для фильтрации и сериализации передаваемых событий с помощью BroadcasterFilter (XSSHtmlFilter.class, JsonpFilter.class)
- Обеспечьте поддержку всех несовместимых реализаций браузера (например, нет необходимости выводить комментарии, как в примере с сервлетом 3.0 (строка 69)). Атмосфера обойдет все эти проблемы для вас.
В Servlet 3.0 Async API отсутствует часть обмена информацией с приостановленными ответами. В текущем примере чата вам нужно создать собственную ветку / очередь, чтобы транслировать события на ваш набор приостановленных ответов (строки с 32 по 56). Это не имеет большого значения, но вам нужно будет сделать что-то подобное для всех ваших приложений на основе Servlet 3.0 Async … или использовать Framework, который сделает это за вас !.
Все еще не убежден? Итак, вы можете написать свои приложения Atmosphere сегодня, и вам не придется ждать внедрения Servlet.3.0 (хорошо, простой плагин для моего другого проекта: GlassFish v3 поддерживает его довольно хорошо!). Зачем? Атмосфера всегда автоматически определяет лучший асинхронный API при развертывании приложения. Всегда сначала пытайтесь найти 3.0 Async API. В случае неудачи он попытается найти собственный API WebServer, такой как Grizzly Comet (GlassFish), CometProcessor (Tomcat), Continuation (Jetty), HttpEventServlet (JBossWeb), AsyncServlet (WebLogic), Google App Engine (Google). И, наконец, отступит от использования блокирующего потока ввода-вывода для эмуляции поддержки асинхронных событий.
Но вы не хотите использовать Java? Хорошо, попробуйте подключаемый модуль Atmosphere Grails , или Atmosphere в PrimesFaces, если вам нравится JSF, или используйте Scala:
package org.atmosphere.samples.scala.chat import javax.ws.rs.{GET, POST, Path, Produces, WebApplicationException, Consumes} import javax.ws.rs.core.MultivaluedMap import org.atmosphere.annotation.{Broadcast, Suspend} import org.atmosphere.util.XSSHtmlFilter @Path("/chat") class Chat { @Suspend @GET @Produces(Array("text/html;charset=ISO-8859-1")) def suspend() = { "" } @Broadcast(Array(classOf[XSSHtmlFilter],classOf[JsonpFilter])) @Consumes(Array("application/x-www-form-urlencoded")) @POST @Produces(Array("text/html;charset=ISO-8859-1")) def publishMessage(form: MultivaluedMap[String, String]) = { val action = form.getFirst("action") val name = form.getFirst("name") val result: String = if ("login".equals(action)) "System Message" + "__" + name + " has joined." else if ("post".equals(action)) name + "__" + form.getFirst("message") else throw new WebApplicationException(422) result } }
Echec et Mat!
Теперь я могу понять, что у вас уже есть приложение и просто хотите обновить его с помощью функции приостановки / возобновления / трансляции, не переписывая его полностью. Хорошо, давайте просто использовать API атмосферы Meteor :
package org.atmosphere.samples.chat; import java.io.IOException; import java.util.LinkedList; import java.util.List; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.atmosphere.cpr.BroadcastFilter; import org.atmosphere.cpr.Meteor; import org.atmosphere.util.XSSHtmlFilter; public class MeteorChat extends HttpServlet { private final List list; public MeteorChat() { list = new LinkedList(); list.add(new XSSHtmlFilter()); list.add(new JsonpFilter()); } @Override public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { Meteor m = Meteor.build(req, list, null); req.getSession().setAttribute("meteor", m); res.setContentType("text/html;charset=ISO-8859-1"); res.addHeader("Cache-Control", "private"); res.addHeader("Pragma", "no-cache"); m.suspend(-1); m.broadcast(req.getServerName() + "__has suspended a connection from " + req.getRemoteAddr()); } public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException { Meteor m = (Meteor)req.getSession().getAttribute("meteor"); res.setCharacterEncoding("UTF-8"); String action = req.getParameterValues("action")[0]; String name = req.getParameterValues("name")[0]; if ("login".equals(action)) { req.getSession().setAttribute("name", name); m.broadcast("System Message from " + req.getServerName() + "__" + name + " has joined."); res.getWriter().write("success"); res.getWriter().flush(); } else if ("post".equals(action)) { String message = req.getParameterValues("message")[0]; m.broadcast(name + "__" + message); res.getWriter().write("success"); res.getWriter().flush(); } else { res.setStatus(422); res.getWriter().write("success"); res.getWriter().flush(); } } }
Servlet 3.0 Async API — игра окончена! Наконец, я должен признать, что Servlet 3.0 Async API имеет асинхронный диспетчер, который можно использовать для асинхронной пересылки запроса
public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { final AsyncContext ac = req.startAsync(); final String target = req.getParameter("target"); Timer asyncTimer = new Timer("AsyncTimer", true); asyncTimer.schedule( new TimerTask() { @Override public void run() { ac.dispatch(target); } }, 5000); }
В Atmosphere будет работать тот же код, но ваше приложение будет работать только при развертывании на Servlet 3.0 WebServer. Вместо этого вы можете реализовать ту же функциональность, используя API-интерфейс отложенной трансляции Broadcast, и при этом иметь переносное приложение, не ограничивая вас с помощью Servlet 3.0 Async API … Об этом я и расскажу в своем следующем блоге!
По любым вопросам или для загрузки Atmosphere перейдите на наш основной сайт и воспользуйтесь нашим форумом Nabble (подписка не требуется) или подпишитесь на нас в Twitter и разместите там свои вопросы!