14. září 2007

DWR a Jetty = výkoný AJAX naruby

Nebudu přímo reagovat, ale spíš doplním vlastův blog DWR - AJAX knihovna pro remotování Java objektů o ,z mého hlediska hodně zajímavou ,vlastnost frameworku DWR 2.0, tj. Reverse AJAX.

Reverse AJAX je když dokážeme ze serveru volat clienta, toď velmi jednoduchá definice. A jak této skutečnosti dosáhnout? Existují 3 možné způsoby:

  • polling - asi nejstarší dostupná varianta, klient se v pravidelných intervalech dotazuje serveru, zda není něco nového - nevýhoda je zřejmá, velmi často dotaz běží na server jenom proto, aby se dozvěděl, že nic nového není
  • piggyback - dotaz na server se přiloží k nejbližšímu následujícímí regulérnímu požadavku na server - nevýhodou je, že se o změnách ze serveru můžeme dozvědět s poměrně velkým zpožděním
  • comet - vytvoří se spojení se serverem, toto spojení se neuzavře a server jej využívá k odesílání dat klientovi v okamžiku, kdy má taková data k dispozici - výhodou je, že se klient o změnách dozví okamžitě a toto řešení nevyvolává nežádoucí provoz (blíže např. dagiho příspěvek)
Ovšem přeci jenom jednu nevýhodu tento přístup má. Pokud bychom nevytvářeli aplikace "inteligentně", pak každé spojení klienta se serverem, které se používá k posílání dat ze serveru na klienta, nám použije jedno vlákno na serveru, což bude řešení, které nebude moc škálovatelné.

DWR + Jetty = výkoný reverse AJAX

Nyní se dostaneme k řešení, které framework DWR dovoluje použít, pokud je nasazen v kontejneru Jetty 6.0. Ve verzi 6.0 totiž Jetty obsahuje podporu pro continuations, které umožňují "uspat" obsloužení aktuálního požadavku a vyvolat jej později (za podpory java.nio toto uspání neznamená uspání a blokování obslužného vlákna požadavku).

Nyní se pomalu dostaneme k příkladu chat serveru. V první řadě si představíme třídu, která bude reprezentovat zprávu:
public class ChatMessage {

protected String nick;
protected String message;

public ChatMessage(String nick, String message) {
super();
this.nick = nick;
this.message = message;
}

public String getNick() {
return nick;
}

public String getMessage() {
return message;
}

}
Nic překvapujícího není k vidění. Následuje rozhraní pro příjemce nových zpráv a třída, která bude singleton a bude implementovat vstupní bránu pro všechny došlé zprávy (doručené např. AJAX requestem a nebo obyčejným odesláním formuláře - implementaci takového jednoduchého servletu nechám na vás). Jdu rovnou na ChatListener a ChatGateway:
public void class ChatListener {

public void onMessage(ChatMessage msg);

}

public void class ChatGateway {

private static final ChatGateway instance = new ChatGateway(); //singleton instance

protected List<ChatListener> lsts;

private ChatGateway() {
super();
lsts = new ArrayList<ChatListener>();
}

public void addListener(ChatListener lst) { //přidej posluchače
...
}

public void newMessage(ChatMessage msg) {
List<ChatListener> lsts2 = lsts;
for (ChatListener lst : lsts2) {
lst.onMessage(msg); //každému sděl novou zprávu
}
}

}

V tomto kódu stále ještě není žádné moudro, které by nějak přibližovalo naše řešení a proto pojďme dále k ChatTracker:
import org.directwebremoting.*;

public class CharTracker implements ChatListener {

protected static final String URL = "..."; //URL stránky, kde se chat odehrává

protected ServerContext srvCtx; //to je celé DWR kouzlo

public void ChatTracker() {
super();

//vytvoříme server context
WebContext webCtx = WebContextFactory.get();
srvCtx = ServerContextFactory.get(webCtx.getServletContext());

//gateway o nás musí vědět
ChatGateway.getInstance().addListener(this);
}

public void onMessage(ChatMessage msg) {
//vytvoříme script, který bude odeslán všem klientům
ScriptBuilder sb = new ScriptBuilder();
sb.appendScript("newMsg(").appendData(msg).appendScript(");");

//pošli script všem klientům, kteří jsou na stránce s definovaným URL
for (ScriptSession ss: serCtx.getScriptSessionsByPage(URL)) {
ss.addScript(sb);
}

}

}

V tomto útržku kódu je velmi podstatný ServerContext, což je DWR třída, která pro nás bude prostředníkem pro zjišťování, kteří klienti jsou právě na stránce chatu a má se jim poslat informace o nové zprávě. Každý takový klient je reprezentován instancí třídy ScriptSession, která je použita k posílání událostí (v našem příkladu k posílání JavaScriptového kódu).

Podstatnou drobností je metoda appendData třídy ScriptBuilder, která zakóduje Java objekt do JavaScriptové implementace, přesně v duchu DWR (viz. konfigurace dále).

Pro jednoduchost nepoužiji integraci se SpringFrameworkem a použiji přímou konfiguraci DWR pomocí souboru dwr.xml, jenž bude umístěn v adresáři WEB-INF:

<dwr>
<allow>
<create creator="new" javascript="chatTracker" scope="application">
<param name="class" value="ChatTracker"/>
</create>

<convert converter="bean" match="ChatMessage"/>
</allow>
</dwr>

S přihlédnutím k vlastově příspěvku zde není nic neznámého a představíme si definici DWR servletu ve web.xml:

<servlet>
<servlet-name>dwr-invoker</servlet-name>
<servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
<init-param>
<param-name>activeReverseAjaxEnabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>initApplicationScopeCreatorAtStartup</param-name>
<param-value>true</param-value>
</init-param>
</servlet>

Servlet DwrServlet je v jiné package, než uváděl vlasta, což je dáno použitím verze 2.0. Parametr activeReverseAjaxEnabled povoluje reverse AJAX a initApplicationScopeCreatorAtStartup zařídí vytvoření instance
třídy ChatTracker při startu aplikace, nikolivěk při jejím prvním použití.

A co na straně klienta. Asi už je vám jasné, že to bude opět velmi jednoduché:

window.onload = function() {
dwr.engine.setActiveReverseAjax(true);
}

function newMsg(msg) {
if(msg) {
//zde přidej kód pro zobrazení zprávy
}
}

A je to. Je to úžasně jednoduché, to je přesně podle mého gusta. Jediné co mi dělá vrásky na čele, je implementace pouze pro Jetty 6.0, ale už existuje požadavek na vytvoření podpory pro Tomcat 6.0 (DWR-143), tak snad se v budoucnu dočkáme.

Závěr


Ukázal jsem vám, jak pomocí DWR 2.0 a Jetty 6.0 implementovat posílání událostí ze serveru na klienty na příkladu chatu. DWR vám jednoznačně hodně ulehčí v implementování detailů, čímž vám ušetří nejenom čas, ale i nervy. Navíc můžete velmi jednoduše přepínat mezi pollováním, piggybackem a cometem bez zásahu do vašeho kódu, všechny detaily jsou schované uvnitř DWR.

Odkazy

2 komentáře:

jarop řekl(a)...

Nemalo by tam byť poling namiesto pooling ?

jira řekl(a)...

Mělo by tam být polling ... omlouvám se