Java na webovém serveru: implementujeme Jabber

Dnes si povíme, jak vytvořit pro naši aplikaci webový chat. A nebude to chat ledajaký, použijeme oblíbený protokol XMPP (Jabber) a napojíme se na existující server. Díky tomu si spolu budou moci povídat jak náhodní kolemjdoucí, kteří přišli na web, tak i uživatelé klasických IM klientů.
Seriál: Java na webovém serveru (16 dílů)
- Java na serveru: úvod 8. 1. 2010
- Java na webovém serveru: první web 15. 1. 2010
- Java na webovém serveru: práce s databází 29. 1. 2010
- Java na webovém serveru: práce s databází II 12. 2. 2010
- Java na webovém serveru: lokalizace a formátování 19. 2. 2010
- Java na webovém serveru: autorizace a autentizace 26. 2. 2010
- Java na webovém serveru: autorizace a autentizace II 5. 3. 2010
- Java na webovém serveru: porovnání Javy a PHP 10. 3. 2010
- Java na webovém serveru: Vlastní JSP značky a servlety 17. 3. 2010
- Java na webovém serveru: posílání e-mailů a CAPTCHA 24. 3. 2010
- Java na webovém serveru: píšeme REST API 7. 4. 2010
- Java na webovém serveru: SOAP webové služby 14. 4. 2010
- Java na webovém serveru: hlasování a grafy v SVG 28. 4. 2010
- Java na webovém serveru: Komentáře a integrace s Texy 9. 6. 2010
- Java na webovém serveru: AJAX formuláře 23. 6. 2010
- Java na webovém serveru: implementujeme Jabber 30. 6. 2010
Nálepky:
Pro komunikaci XMPP protokolem použijeme knihovnu Smack, která nám přináší javovské API a odstíní nás od nízkoúrovňové komunikace s Jabber serverem.
Jako obvykle si nejprve si aktualizujeme zdrojové kódy aplikace pomocí Mercurialu:
$ hg pull $ hg up "16. díl"
Případně je můžete stáhnout jako bzip2 archiv přes web.
Základní práce se Smack knihovnou
Smack je klientská XMPP knihovna od autorů Jabber serveru Openfire. Pomocí následujícího kódu se připojíme k serveru a autentizujeme:
ConnectionConfiguration nastaveni = new ConnectionConfiguration("doména-server"); spojeni = new XMPPConnection(nastaveni); spojeni.connect(); spojeni.login("jméno", "heslo", "zdroj");
Připojení a autentizace jsou oddělené do dvou kroků, protože některé servery umožňují anonymní přístup a některé operace lze provádět už před přihlášením (např. založení nového účtu).
Práce s knihovnou je poměrně jednoduchá a intuitivní. Pomocí následujícího kódu vstoupíme do místnosti a odešleme do ní zprávu:
MultiUserChat muc = new MultiUserChat(spojeni, "název_místnosti"); muc.sendMessage("ahoj");
Příjem zpráv je realizován pomocí posluchačů (listener), které zaregistrujeme, a kteří pak zpracovávají události. Jedná se o stejný princip, jakým se obsluhují události např. ve Swingu (GUI). Posluchače zaregistrujeme pomocí volání metody muc.addMessageListener()
a musí implementovat metodu processPacket()
z rozhraní PacketListener
:
public void processPacket(Packet packet) { if (packet instanceof Message) { Message m = (Message) packet; String od = StringUtils.parseResource(m.getFrom()); String text = m.getBody(); /** uděláme něco se zprávou… */ } }
EJB komponenta
Možná teď přemýšlíte, jak skloubit dohromady navazování spojení s chatovacím serverem a bezestavový HTTP protokol, který používáme na webu. Budeme se přihlašovat k Jabber serveru s každým HTTP požadavkem a po jeho vyřízení spojení zahazovat? Ne, tohle naštěstí není potřeba, Java nabízí řešení v podobě EJB komponent, které „žijí“ na serveru po celou dobu běhu aplikace a mohou tak držet jedno trvalé XMPP spojení. V rámci jednotlivých HTTP požadavků se pak k této komponentě připojíme a využijeme jejích služeb. Toto téma jsme už nakousli v díle srovnávajícím PHP a Javu. Dnes se dostaneme k praktické ukázce.
Základem naší komponenty je třída cz.frantovo.nekurak.ejb.ChatEJB
@Singleton @Startup public class ChatEJB implements ChatRemote { private static final Logger log = Logger.getLogger(ChatRemote.class.getSimpleName()); private Nastaveni nastaveni; private Collection<Spojeni> spojeni = new ArrayList<Spojeni>(); @Override public void posliZpravu(String mistnost, String prezdivka, String zprava) throws NekurakVyjimka { MistnostPripojena mp = najdiMistnost(mistnost); if (mp == null) { throw new NekurakVyjimka("Místnost s tímto názvem neexistuje", null); } else { try { mp.posliZpravu(new ZpravaChatu(prezdivka, zprava)); } catch (Exception e) { log.log(Level.SEVERE, "Selhalo odesílání zprávy", e); throw new NekurakVyjimka("Zprávu se nepodařilo odeslat.", e); } } } /** * @param mistnost název místnosti včetně zavináče a serveru * @param poradoveCislo pořadové číslo poslední zprávy, kterou jsme dostali * @return všechny novější zprávy než dané pořadové číslo * @throws NekurakVyjimka */ @Override public Collection<ZpravaChatu> getZpravy(String mistnost, int poradoveCislo) throws NekurakVyjimka { MistnostPripojena mp = najdiMistnost(mistnost); if (mp == null) { throw new NekurakVyjimka("Místnost s tímto názvem neexistuje", null); } else { return mp.getZpravy(poradoveCislo); } } public ChatEJB() throws NekurakVyjimka { /** TODO: vyřešit lépe. */ nastaveni = new SpravceNastaveni().getNastaveni(); } @PreDestroy public void odpoj() { for (Spojeni s : spojeni) { s.odpoj(); } } @PostConstruct public void inicializuj() throws NekurakVyjimka, NamingException { pripojXMPP(); } private void pripojXMPP() throws NekurakVyjimka { try { for (UcetRobota u : nastaveni.getUctyRobota()) { Spojeni s = new Spojeni(u); spojeni.add(s); } } catch (Exception e) { throw new NekurakVyjimka("Chyba při připojování.", e); } } /** * @param nazev Název místnosti, kterou hledáme. * @return nalezená místnost, nebo null, pokud místnost nebyla nalezena. */ private MistnostPripojena najdiMistnost(String nazev) { for (Spojeni s : spojeni) { for (MistnostPripojena mp : s.getMistnosti()) { if (mp.porovnejNazev(nazev)) { return mp; } } } return null; }
Důležité jsou zde použité anotace. @Singleton
říká, že EJB komponenta bude v systému jen jedna, což v našem případě znamená, že z našeho serveru povede jen jedno XMPP spojení na Jabber server, bez ohledu na to, kolik klientů náš server bude obsluhovat. Pomocí anotace @Startup
říkáme, že se komponenta má vytvořit hned po nasazení aplikace (deploy). Jinak by se totiž vytvořila až ve chvíli, kdy by ji poprvé někdo potřeboval. Anotací @PreDestroy
pak označíme metodu, která se postará o korektní ukončení navázaného XMPP spojení – zavolá se např. při vypínání aplikačního serveru nebo deaktivaci aplikace.
Na straně klienta
Nad EJB vrstvou máme webové rozhraní (viz chat.jsp
), které zpřístupňuje funkcionalitu komponenty webovému prohlížeči. V něm pomocí AJAXu odesíláme zprávy do chatovací místnosti a periodicky kontrolujeme zda přišly nějaké nové zprávy. Tento kód naleznete v souboru chat.js
.
Závěr
V dnešním díle jsme se naučili pracovat s XMPP protokolem v Javě, což se nám může hodit i jinde než na webu – např. při tvorbě IM klienta nebo XMPP služby. Webovou část našeho chatu bychom později mohli přepsat tak, aby nevyžadovala periodickou kontrolu nových zpráv, např. s použitím moderní technologie Webových socketů. K tomu je ale potřeba podstatnější zásah do naší komponenty – je třeba ji upravit, aby posílala nové zprávy všem přihlášeným klientům a ne jen pasivně čekala, až se jí někdo zeptá, jaké jsou nové zprávy.
Odkazy
- XMPP – Extensible Messaging and Presence Protocol.
- Smack – Knihovna pro práci s XMPP (Jabberem).
- Smack – dokumentace ke knihovně.
- New Features in EJB 3.1 – Novinky v EJB 3.1 (např. Singletony).
Zdravím, výjimky bych vyhazoval trochu jinak např když místnost neexistuje: NekurakNoSuchRoom(„Místnost z názvem ‚“+mistnost+„‘ neexistuje.“); apod. Tedy trochu konkrétněji pojmenovat třídy výjimek a v textu zprávy výjimky napsat i „čeho“ se týká jak je v Javě zvykem. Ne tedy obecně ArrayOufOfBounds, ale taky připojit informaci pro snadné opravení chyby jak to dělá Java: index:8, size:5.
Smack kniznica je dobra tak nanajvys do desktopovych klientov pretoze zataz a poolovanie spojeni absolutne nezvlada, rovnako pokia sa zacne prihlasovat viacero klientov tak ukoncuje spojenia ako na beziacom pase a lietaju tam EOException : no more data avalaible – expected end tag. S tou kniznicou by som na chatovom servri moc neexperimentoval. Nam nezostavalo nic ine len si napisat vlastny adapter na XMPP…
Tady ten Smack drží jen jedno XMPP spojení, které se sdílí mezi všemu webovými uživateli.
BTW: ten adaptér je JMS adaptér?
pouziva to podobny princip ako JMS ale implementaciaje vlastna. Proti JMS rozhodlo niekolko veci, ktore boli specificke pre ten projekt.