Hlavní navigace

Síťování v Javě: Chatovací server

Martin Majer 4. 5. 2006

Dnes využijeme znalosti získané v minulých dílech. Vytvoříme další server, tenokrát chat. Hlavními cíli tohoto komplikovanějšího příkladu bude ukázka práce s předem neznámým počtem vláken a vysvětlení hlavního rozdílu mezi java.net API a New I/O.

Chatovací server

Jedná se o vskutku primitivní program. Uživatel se připojí přes telnet a kdykoliv odešle nějakou zprávu, ostatním se zobrazí společně s jeho IP adresou. Pro odhlášení stačí ukončit telnet nebo poslat zprávu /quit.

wanto@karmaj:~/root/java_site> java Telnet localhost 10997
10.93.27.111: Připojen.
10.93.27.111: je tu nekdo?
localhost se hlasi :-D
10.93.27.111: ahoj, tady 10.93.27.111
/quit
Vstupní proud uzavřen. 

Takto vypadá chat z pohledu uživatele. Nyní se pojďme zaměřit na to, jak je server naprogramován.

Server je řešen poměrně těžkopádným způsobem. Při velkém počtu uživatelů může být pomalý, nespolehlivý a zatěžovat systém. Využívá totiž pouze schopností java.net API, což nás nutí vytvořit ho jako vícevláknový. Na začátku běhu aplikace spustíme jediné (hlavní) vlákno, jehož úkolem bude přijímat nové klienty. Každý nový klient dostane vlákno vlastní, které bude čekat na příchozí zprávy a přeposílat je ostatním.

Jistě sami soudíte, že takováto aplikace asi nebude příliš dobrá. Ano, není. Existuje totiž jeden mnohem lepší postup ukrytý v knihovnách New I/O API. Nicméně tento postup je výborným příkladem pro ukázku typické práce s více vlákny a vysvětlení rozdílů mezi java.net API a New I/O.

Hlavní vlákno

Nejdříve vytvoříme serverový soket pomocí konstruktoru ServerSocket(int port). Už nemusíme volat metodu bind(), protože tento konstruktor zároveň soket zaregistruje na všech síťových rozhraních.

Dalším úkolem je obsloužit nově připojené klienty. Soket klienta získaný metodou accept() okamžitě zabalíme do objektu vlákna ClientThread, které si uložíme do seznamu. Spustíme toto vlákno a metodou broadcast() informujeme ostatní o připojení nového uživatele.

while(true) {
    Socket s = server.accept();
    ClientThread newclient = new ClientThread(s);
    clients.add(newclient);
    newclient.start();
    broadcast("Připojen.", newclient);
} 

Úloha hlavního vlákna končí v bloku finally. Ten zajišťuje řádné uzavření serverového soketu a odpojení všech klientů.

for(ClientThread clt: getClients()) clt.close();
clients.clear(); 

Metoda void broadcast(String message, ClientThread from)

Metoda broadcast slouží k odeslání zprávy všem připojeným uživatelům kromě odesílatele. Po sestavení zprávy ve tvaru ‚IP adresa: text‘ projde cyklem všechny klienty a zprávu zapíše přes proud typu PrintStream do jejich výstupních soketů.

String ip = from.socket.getInetAddress().getHostAddress();
message = ip + ": " + message;
for(ClientThread clt: getClients()) {
    if(clt == from) continue;
    clt.out.println(message);
    clt.out.flush();
} 

Klientské vlákno ClientThread

V konstruktoru ClientThread si uložíme odkaz na soket klienta. Dále sestavíme vlastní proudy pro jednodušší komunikaci. Z výstupního proudu vytvoříme PrintWriter popsaný v minulém díle, který nám umožní odesílat řetězce. Od vstupního proudu odvodíme BufferedReader, který dokáže číst příchozí data po řádcích.

out = new PrintStream(socket.getOutputStream());
in = new BufferedReader(new InputStreamReader(socket.getInputStream())); 

Nejdůležítější část kódu vlákna se nachází v metodě run(). Zde probíhá čtení příchozích zpráv a ověřování, jestli zpráva není odhašovací příkaz /quit. Pokud by metoda readLine() vrátila null, také by to znamenalo odpojení klienta.

while(true) {
    String message = in.readLine();
    if(message == null) break;
    if(message.startsWith("/quit")) break;
    else broadcast(message, this);
} 

Po opuštění cyklu příkazem break následuje přechod do finally bloku s voláním metody close(), která uzavře soket a proudy a informuje ostatní o odpojení.

Zdrojový kód

import java.io.*;
import java.net.*;
import java.util.List;


/** Hlavní třída zajišťující provoz chatovacího serveru. */
public class ChatServer {

    /** Port, na kterém server poběží. */
    public static final int PORT = 10997;

    /** Soket chatovacího serveru. */
    private ServerSocket server;
    /** Seznam připojených klientů. */
    private List<ClientThread> clients;

    /** Spustí server. */
    public void run() {
        clients = new java.util.ArrayList<ClientThread>();
        try {
            server = new ServerSocket(PORT); //vytvořit serverový soket naslouchající na 0.0.0.0

            while(true) {
                Socket s = server.accept(); //přijmout klienta
                ClientThread newclient = new ClientThread(s); //vytvořit vlákno
                clients.add(newclient); //přidat klienta do seznamu
                newclient.start(); //spustit vlákno
                broadcast("Připojen.", newclient); //odeslat ostatním zprávu
            }
        }
        catch(IOException e) {
            e.printStackTrace(System.err);
        }
        finally {
            if(server != null) {
                //odpojit všechny klienty
                for(ClientThread clt: getClients()) clt.close();
                clients.clear();

                try { server.close(); }
                catch(IOException e) {}
            }
        }
    }

    /** Pošle všem kromě odesílatele zprávu. */
    public synchronized void broadcast(String message, ClientThread from) {
        String ip = from.socket.getInetAddress().getHostAddress();
        message = ip + ": " + message;
        for(ClientThread clt: getClients()) {
            if(clt == from) continue;
            clt.out.println(message);
            clt.out.flush();
        }
        System.out.println(message);
    }

    /** Vrací seznam klientů.
      * Tuto metodu je nutné používat místo přímého přístupu k proměnné clients -
      * - ten není synchronizovaný. */
    public synchronized List<ClientThread> getClients() {
        return clients;
    }


    /** Vlákno zajišťující čtení zpráv od klienta. */
    private class ClientThread extends Thread {

        /** Klientův soket. */
        Socket socket;
        /** Výstupní proud. */
        PrintStream out;
        /** Vstupní proud. */
        BufferedReader in;

        /** Vytvoří nové klientské vlákno. */
        public ClientThread(Socket socket) {
            this.socket = socket;
            try {
                out = new PrintStream(socket.getOutputStream()); //vytvořit PrintStream
                in = new BufferedReader(new InputStreamReader(socket.getInputStream())); //vytvořit BufferedReader
            }
            catch(IOException e) {
                e.printStackTrace(System.err);
                close();
            }
        }

        public void run() {
            try {
                while(true) {
                    String message = in.readLine();
                    if(message == null) break;
                    if(message.startsWith("/quit")) break;
                    else broadcast(message, this);
                }
            }
            catch(IOException e) {
                System.err.println("Kvuli chybe odpojen klient.");
                e.printStackTrace(System.err);
            }
            finally {
                close(); //odpojit
            }
        }

        /** Odpojí klienta. */
        public void close() {
            broadcast("Odpojen.", this); //odeslat zprávu o odpojení
            getClients().remove(this); //vymazat ze seznamu
            try {
                out.close(); //zavřít výstupní proud
                in.close(); //zavřít vstupní proud
                socket.close(); //zavřít soket
            } catch(IOException e) {}
        }
    }

    public static void main(String[] args) {
        new ChatServer().run();
    }
} 

Jak by aplikace vypadala s New I/O?

New I/O (zkráceně NIO) je druhé síťové API v Javě, které bylo poprvé vydáno ve verzi Javy 1.4. Zabývat se s ním začneme až v příštím díle, ale už dnes si popíšeme nejvýznamnější odlišnost od java.net.

Hlavním rozdíl by byl v používání vláken. Nyní je v naší aplikaci n + 1 vláken (kde n je počet klientů). Každé klientské vlákno je pozastaveno (zablokováno), dokud nejsou k dispozici nová data v jeho vstupním proudu. Tomu se říká blokující přístup. NIO knihovny naopak poskytují neblokující přístup. Selektory z NIO dokáží určit, který ze vstupních proudů (resp. kanálů) je právě aktivní, a okamžitě vrátit příchozí data. Počet vláken se tak může snížit pouze na jedno. Výhody jsou zřejmé – vyšší rychlost, menší zatížení systému, větší stabilita a spolehlivost.

Závěr

Dnes jsme se dostali na konec první části seriálu. Dokončili jsme java.net API a dále se mu již nebudeme věnovat. V příštím díle se začneme zabývat konečně pořádným síťováním – knihovnami New I/O.

Našli jste v článku chybu?

5. 5. 2006 10:13

podlesh (neregistrovaný)
No, zas tak tvrdý bych nebyl. Že je článek pro začátečníky je myslím jasné.

A s tou 1.5 rozhodně nesouhlasím. Sice se ještě často používá 1.3 či 1.4, ale 1.5 už se nasazuje docela dost. Koneckonců JDK 1.5 je už stabilní hodně dlouho. Kdo potřebuje starší verzi (třeba kvůli aplikáči, nebo správci produkčního serveru) o tom ví a pro ostatní neexistuje důvod proč se 1.5 vyhýbat.

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Root.cz: Pinebook: linuxový notebook za 89 dolarů

Pinebook: linuxový notebook za 89 dolarů

Podnikatel.cz: Babiše přesvědčila 89letá podnikatelka?!

Babiše přesvědčila 89letá podnikatelka?!

Vitalia.cz: Říká amoleta - a myslí palačinka

Říká amoleta - a myslí palačinka

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život

DigiZone.cz: Flix TV má set-top box s HEVC

Flix TV má set-top box s HEVC

Měšec.cz: Jak levně odeslat balík přímo z domu?

Jak levně odeslat balík přímo z domu?

DigiZone.cz: Rádio Šlágr má licenci pro digi vysílání

Rádio Šlágr má licenci pro digi vysílání

120na80.cz: Jak oddálit Alzheimera?

Jak oddálit Alzheimera?

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

Podnikatel.cz: Udávání kvůli EET začalo

Udávání kvůli EET začalo

Podnikatel.cz: Zavře krám u #EET Malá pokladna a Teeta?

Zavře krám u #EET Malá pokladna a Teeta?

120na80.cz: Horní cesty dýchací. Zkuste fytofarmaka

Horní cesty dýchací. Zkuste fytofarmaka

Vitalia.cz: Znáte „černý detox“? Ani to nezkoušejte

Znáte „černý detox“? Ani to nezkoušejte

Vitalia.cz: 9 největších mýtů o mase

9 největších mýtů o mase

Vitalia.cz: Co pomáhá dítěti při zácpě?

Co pomáhá dítěti při zácpě?

Měšec.cz: Air Bank zruší TOP3 garanci a zdražuje kurzy

Air Bank zruší TOP3 garanci a zdražuje kurzy

DigiZone.cz: NG natáčí v Praze seriál o Einsteinovi

NG natáčí v Praze seriál o Einsteinovi

Vitalia.cz: Láska na vozíku: Přitažliví jsme pro tzv. pečovatelky

Láska na vozíku: Přitažliví jsme pro tzv. pečovatelky

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?