Hlavní navigace

Síťování v Javě: New I/O server

18. 5. 2006
Doba čtení: 6 minut

Sdílet

Dnes budeme pokračovat v New I/O API. Vysvětlíme si práci se serverovým kanálem, selektory a ukážeme si, jak převádět obsah bufferů na řetězce. Na závěr si vytvoříme primitivní HTTP server, na kterém si prakticky vyzkoušíme postupy popisované v teoretické části.

Třída ServerSocketChannel

SocketChannel z balíku java.nio.channels jsme si popsali v minulém díle. Nyní se podíváme, jak pracovat se serverovým kanálem – třídou java.nio.chan­nels.ServerSoc­ketChannel.

Kanál otevřeme pomocí metody ServerSocketChan­nel.open(). Jeho serverový soket však ještě nikde nenaslouchá. Proto musíme pomocí metody socket() získat ServerSocket, jejž známe z druhého dílu, a zavolat jeho metodu bind().

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("localhost", 80)); 

Dále je vhodné nastavit, zda bude serverový kanál v neblokujícím módu. Výběr režimu mezi blokujícím a neblokujícím má totiž zásadní vliv na další používání kanálu.

ssc.configureBlocking(false); //přepne do neblokujícího režimu 

Přijetí nového již není zajišťováno pomocí accept() ze třídy ServerSocket, nýbrž stejnojmennou metodou z ServerSocket­Channel. Pokud je serverový kanál v blokujícím režimu, bude se accept() chovat stejně jako ta ze ServerSocket. Zastaví běh aktuálního vlákna, dokud se nepřipojí nějaký klient. Vlákno uvede do chodu vrácením odkazu na jeho soket. V neblokujícím módu accept() vrátí odkaz na soket nového klienta, nebo null. Vlákno ale nikdy nepozastaví.

Po ukončení práce uzavřeme serverový soket kanálu i kanál samotný.

ssc.socket().close();
ssc.close(); 

Selektory

Nyní se dostáváme k jedné z obtížnějších částí New I/O – k selektorům. Zjednodušeně řečeno, selektor je množina několika kanálů, u které můžeme sledovat, který prvek je aktivní. Vzpomeňte si na chat ze třetího dílu. Pro každého připojeného klienta jsme vytvořili jedno vlákno. To čekalo, až od klienta přijdou nějaká data. Co kdybychom všechny klientské sokety „zabalili“ do selektoru? Měli bychom pouze jedno vlákno, které by čekalo na aktivitu celého selektoru. Pak bychom vybrali pouze kanály, do kterých přišla nějaká data. Této technice se říká multiplexování a je nejvýznaměnším rozšířením oproti klasickému java.io/java.net API.

Pojďme se podívat, jak se se selektory pracuje.

Vytvoření selektoru

Selektor vytvoříme pomocí metody open() třídy Selector z balíku java.nio.channels.

Selector sel = Selector.open(); 

Registrace kanálů

Kanál přepnutý do neblokujícího režimu do selektoru zaregistrujeme pomocí metody register() tohoto kanálu. Tato metoda pochází ze třídy SelectableChannel, takže pokud ji kanál nemá, selektory nepodporuje. Jako první parametr očekává odkaz na příslušný selektor. Druhý parametr je množina operací, které budou selektor zajímat. Můžou to být konstanty OP_ACCEPT, OP_CONNECT, OP_READ, OP_WRITE třídy SelectionKey nebo bitové součty pro jejich kombinace. Třetím, nepovinným parametrem můžeme kanálu zaregistrovanému do selektoru přiřadit libovolný objekt jako přílohu.

SelectionKey key = channel.register(sel, SelectionKey.OP_READ); 

Tento příkaz zaregistruje k selektoru sel kanál channel tak, aby selektor zajímala pouze příchozí data. Zároveň vrátí odkaz na prvek SelectionKey, který umožňuje spravovat připojení k selektoru. Lze pomocí něj kanál ze selektoru odstranit, získat přílohu, připojit jinou přílohu atd.

Object o = key.attachment(); //získáme přílohu
key.attach(new Object());    //připojíme jinou přílohu
key.cancel();                //zrušíme registraci u selektoru 

Výběr aktivních kanálů

K výběru aktivních kanálů slouží tyto tři metody:

  • select() – Zastaví aktuální vlákno, dokud nevykazuje alespoň jeden z registrovaných kanálů nějakou aktivitu.
  • select(long timeout) – Stejné jako předchozí, ovšem blokuje vlákno pouze po dobu timeout milisekund.
  • selectNow() – Okamžitě se vrací bez ohledu na to, kolik kanálů je aktivních.

Metoda select() (resp. selectNow()) vrací celé číslo, které určuje počet aktivních kanálů. Množinu Set<SelectionKey> s aktivními kanály pak vrací metoda selectedKeys().

Kanál získáme z prvku SelectionKey metodou channel(). Za zmínku také stojí metoda selektoru wakeup(), která dokáže probudit vlákno zablokované metodou select().

Uzavření selektoru

Protože selektor je stejně jako např. soket prostředek operačního systému, musíme ho uzavřít metodou close().

HTTP server

Dnes jsem jako praktický příklad zvolil HTTP server. Nečekejte nic zázračného, tento server bez ohledu na obsah požadavku pošle text/plain dokument s obsahem „Simple HTTP Server“. Na příkladu si však ukážeme práci se serverovým kanálem, selektory a jeden postup, jak převést obsah bajtového bufferu na řetězec.

Spuštění serveru

Vytvoříme serverový kanál, přepneme ho do neblokujícího režimu a připojíme jeho soket na všechna síťová rozhraní. Také otevřeme selektor a z třídy java.nio.char­set.Charset získáme dekodér pro ASCII tabulku. Zároveň selektoru zaregistrujeme serverový kanál. Zajímat nás bude přijetí nového klienta čili SelectionKey.OP_AC­CEPT.

ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.socket().bind(new InetSocketAddress("0.0.0.0", PORT));
decoder = Charset.forName("US-ASCII").newDecoder();
buffer = ByteBuffer.allocateDirect(256); 

Hlavní smyčka serveru

Budeme pokračovat cyklem while, který bude trvat, dokud bude serverový kanál otevřený. Zavoláme metodu select() selektoru, která zablokuje hlavní (a jediné) vlákno aplikace. Znovu ho uvede do běhu, jakmile přijdou nějaká data do zaregistrovaných soketů, nebo se k serveru připojí nový klient.

Prvky SelectionKey vrácené metodou selector.selec­tedKeys() projdeme pomocí iterátoru. Pokud bude kanál vrácený metodou channel() prvku SelectionKey serverový kanál, víme, že se připojil nový klient. Jediná aktivita, pro kterou jsme serverový kanál registrovali k selektoru, je totiž OP_ACCEPT.

U kanálu nově připojeného klienta zapneme neblokující režim. Pak ho zaregistrujeme do selektoru. Jako přílohu připojíme nový objekt typu StringBuffer, do kterého budeme ukládat HTTP požadavek.

Jestliže se nejednalo o serverový kanál, můžeme s jistotou říct, že do nějakého soketu přišla část HTTP požadavku. Obsah bufferu převedeme na řetězec a připojíme na konec StringBufferu. V tom se následně pokusíme vyhledat dvojici CRLF znaků, abychom zjistili, jestli dorazil celý HTTP požadavek. Pokud již dorazil, zapíšeme do soketu odpověď a ukončíme spojení.

UX DAy - tip 2

Samotný převod na řetězec probíhá pomocí objektu decoder typu java.nio.char­set.CharsetDe­coder a jeho metody decode(). Ta očekává jako parametr bajtový buffer a vrací znakový buffer, který jednoduše pomocí metody toString() převedeme na řetězec.

Zdrojový kód

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;

/** Hlavní třída jednoduchého HTTP Serveru. */
public class SimpleHTTPServer {

    public static final int PORT = 10997;

    /** Univerzální odpoveď */
    public static final String RESPONSE = "HTTP/1.0 200 OK\r\n"
        + "Connection: closed\r\n"
        + "Content-Type: text/plain\r\n"
        + "Content-length: 20\r\n"
        + "\r\n"
        + "Simple HTTP server.\n";

    private ServerSocketChannel ssc;
    private Selector selector;
    private CharsetDecoder decoder;
    private ByteBuffer buffer;

    public void run() {
        try {
            ssc = ServerSocketChannel.open(); //otevřít serverový kanál
            ssc.configureBlocking(false); //vypnout blokování
            selector = Selector.open(); //otevřít selektor
            ssc.register(selector, SelectionKey.OP_ACCEPT); //přidat kanál do selektoru
            ssc.socket().bind(new InetSocketAddress("0.0.0.0", PORT)); //připojit serverový soket
            decoder = Charset.forName("US-ASCII").newDecoder(); //vytvořit dekóder pro ASCII
            buffer = ByteBuffer.allocateDirect(256); //alokovat bajtový buffer

            while(ssc.isOpen()) {
                selector.select(); //zablokovat vlákno, dokud není selektor aktivní
                Set<SelectionKey> keys = selector.selectedKeys(); //získat vybrané prvky SelectionKey
                Iterator<SelectionKey> i = keys.iterator();
                while(i.hasNext()) { //projít množinu
                    SelectionKey key = i.next();
                    i.remove();
                    Channel chan = key.channel(); //získat kanál
                    if(chan == ssc) { //pokud je to serverový kanál, připojil se nový klient
                        SocketChannel client = ssc.accept(); //přijmout klienta
                        if(client != null) { //pokud se někdo opravdu připojil
                            client.configureBlocking(false); //vypnout blokování
                            client.register(selector, SelectionKey.OP_READ, new StringBuffer()); //a přidat do selektoru
                        }
                    }
                    else { //aktivní je kanál nějakého z klientů
                        SocketChannel client = (SocketChannel)chan;
                        int read = client.read(buffer); //přečíst data
                        if(read == -1) { //ukončeno spojení
                            key.cancel(); //vyřadit ze selektoru
                            client.close(); //zavřít kanál
                            continue;
                        }
                        StringBuffer sb = (StringBuffer)key.attachment(); //získat HTTP požadavek v příloze
                        buffer.flip();
                        sb.append(decoder.decode(buffer).toString()); //připojit k němu obsah bufferu
                        buffer.clear();

                        if(sb.indexOf("\r\n\r\n") != -1) { //přišel celý HTTP požadavek
                            buffer.put(RESPONSE.getBytes()); //vložit odpoveď do bufferu
                            buffer.flip();

                            int written = 0;
                            while(written < RESPONSE.length()) {
                                written += client.write(buffer); //odeslat
                            }
                            buffer.clear();
                            key.cancel(); //vyřadit ze selektoru
                            client.close(); //zavřít kanál
                        }
                    }
                }
            }
        }
        catch(IOException e) {
            e.printStackTrace(System.err);
        }
        finally {
            try {
                if(ssc != null) {
                    ssc.socket().close();
                    ssc.close();
                }
                if(selector != null) selector.close();
            }
            catch(IOException e) {}
        }
    }

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

Závěr

V dnešním díle seriálu jsme si ukázali práci se serverovým kanálem a selektory, čímž jsme probrali další velkou část New I/O API. Příště trochu odbočíme od tématu síťování a ukážeme si, jaké jsou v Javě možnosti vytváření logů.

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