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.channels.ServerSocketChannel.
Kanál otevřeme pomocí metody ServerSocketChannel.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 ServerSocketChannel. 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.charset.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_ACCEPT.
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.selectedKeys() 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í.
Samotný převod na řetězec probíhá pomocí objektu decoder typu java.nio.charset.CharsetDecoder 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ů.