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ů)

  1. Java na serveru: úvod 8.1.2010
  2. Java na webovém serveru: první web 15.1.2010
  3. Java na webovém serveru: práce s databází 29.1.2010
  4. Java na webovém serveru: práce s databází II 12.2.2010
  5. Java na webovém serveru: lokalizace a formátování 19.2.2010
  6. Java na webovém serveru: autorizace a autentizace 26.2.2010
  7. Java na webovém serveru: autorizace a autentizace II 5.3.2010
  8. Java na webovém serveru: porovnání Javy a PHP 10.3.2010
  9. Java na webovém serveru: Vlastní JSP značky a servlety 17.3.2010
  10. Java na webovém serveru: posílání e-mailů a CAPTCHA 24.3.2010
  11. Java na webovém serveru: píšeme REST API 7.4.2010
  12. Java na webovém serveru: SOAP webové služby 14.4.2010
  13. Java na webovém serveru: hlasování a grafy v SVG 28.4.2010
  14. Java na webovém serveru: Komentáře a integrace s Texy 9.6.2010
  15. Java na webovém serveru: AJAX formuláře 23.6.2010
  16. Java na webovém serveru: implementujeme Jabber 30.6.2010

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).

Franta Kučera působí jako Java vývojář na volné noze. Programování je jeho koníčkem už od dětství. Kromě toho má rád Linux, relační SŘBD a XML.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 4

Přehled komentářů

mmmmario Výjimky
amos Smack
František Kučera Re: Smack
amos Re: Smack
Zdroj: https://www.zdrojak.cz/?p=3268