Hlavní navigace

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

4. 5. 2006
Doba čtení: 5 minut

Sdílet

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.

UX DAy - tip 2

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.

Byl pro vás článek přínosný?