V prvním díle tohoto seriálu si povíme, jaké balíky Javy slouží k práci se sítí. Podrobněji se zaměříme na třídy java.net.InetAddress, java.net.InetSocketAddress a java.net.Socket. Nakonec si ukážeme, jak vytvořit jednoduchého konzolového telnet klienta. Server vytvoříme v následujících článcích, dále budeme programovat vícevláknové aplikace pro více uživatelů. Ukážeme si, jak používat moderní New I/O (NIO) rozhraní Javy a skončíme u IRC chatovacího serveru.
Nepočítejte s žádným vyčerpávajícím výkladem teorie o Java API a implementaci TCP/IP a UDP protkolů. Pokud se budete chtít o dané třídě či rozhraní dozvědět více, nahlédněte do dokumentace k jejímu API. Seriál bude zaměřen výhradně na praktické příklady; teorii nutnou k jejich pochopení ale samozřejmě vypustit nelze.
Balíky
Pracovat budeme s balíky java.net, java.io a java.nio, tak si je nyní ve stručnosti popíšeme. První dva uvedené jsou součástí Java API už od své první verze. Balík java.net nabízí základní funkčnost pro jednodušší síťové aplikace. Říkám jednodušší, protože pro každý nově vytvořený soket je nutné spustit vlákno navíc. V java.io jsou sice umístěny třídy spíše pro práci se soubory, některé z nich jsou však nesmírně užitečné. A to nejen při síťování.
Mladší java.nio umožňuje práci s novým NIO rozhraní, které bylo začleněno ve verzi 1.4. Jeho hlavní předností oproti java.net je schopnost používat pouze jedno vlákno pro neomezené množství soketů (respektive klientů připojených k serveru), čímž se výrazně zvyšuje výkon aplikací. Za ostatní novinky z java.nio uvedu např. zcela nový přístup k souborům nebo možnost soubory uzamknout.
Třídy
java.net.InetAddress
Tato třída reprezentuje adresu v síti podle IP protokolu. Zde je vypsáno několik jejích metod:
static InetAddress getByAddress(byte[] addr) | Vrací adresu na základě pole bajtů. 4 bajty pro IPv4, 16 bajtů pro IPv6. |
static InetAddress getByName(String host) | Vrací adresu podle hostname. |
static InetAddress getLocalHost() | Vrací adresu pro localhost. |
boolean isReachable(int timeout) | Zjistí, jestli se lze připojit k cíli na zadané adrese. |
java.net.InetSocketAddress
InetSocketAddress je kombinace třídy InetAddress a portu. Tuto třídu budeme využívat k připojování soketů na server.
K vytváření nových instancí budeme nejčastěji volat konstruktory InetSocketAddress(InetAddress addr, int port)
nebo InetSocketAddress(String hostname, int port)
.
java.net.Socket
Sockety jsou jedny z nejdůležitějších částí síťových aplikací, jejich prostřednictvím totiž probíhá veškerá síťová komunikace. V Javě se pro vytvářen soketů používá třída java.net.Socket. Zde uvádím, podobně jako u InetAddress, její nejdůležitější metody:
void connect(SocketAddress endpoint) | Připojí se k serveru. Při neúspěchu vyhodí výjimku SocketTimeoutException . |
InputStream getInputStream() | Vrací vstupní proud soketu, ze kterého čteme příchozí data. |
OutputStream getOutputStream() | Vrací výstupní proud soketu, do kterého data zapisujeme. |
void close() | Uzavře soket. |
Dokumentace
Více se o těchto třech třídách můžete dozvědět v dokumentaci k balíku java.net na adrese http://java.sun.com/j2se/1.5.0/docs/api/java/net/package-summary.html.
Telnet klient
Po únavné teorii ;-) se konečně dostáváme k něčemu mnohem zajímavějšímu – ke slibovanému telnet klientovi. Klient bude běžet ve dvou vláknech. První vlákno bude vyčkávat na vstup z konzole a posílat data soketem na server, druhé bude dělat přesný opak. Čekat na data ze serveru a tisknout.
Vlákno TelnetThread
Všimněte si, že obě vlákna vlastně dělají stejnou věc: čtou z nějakého vstupního proudu a následně zapisují do proudu výstupního. To nám velice ulehčí práci, pro obě vlákna nám totiž stačí pouze jedna společná třída.
Nejdůležitější kód třídy TelnetThread se nachází v metodě run(). Do bufferu o velikosti 64 bytů načítá ze vstupního proudu data, která okamžitě a bez jakékoliv změny pošle na výstup. Ve chvíli, kdy se vstupní proud uzavře, vlákno se samo ukončí.
while(true) {
int nbytes = is.read(b);
if(nbytes == -1) break;
os.write(b, 0, nbytes);
}
Proměnná is odkazuje na vstup, proměnná os na výstup. Bytový buffer jsme pojmenovali jako b. Také si povšimněte podmínky, ve které zjišťujeme, jestli se počet přečtených bytů nbytes rovná hodnotě –1. Jakmile jakýkoliv vstupní proud vrátí tuto hodnotu, znamená to, že byl ukončen. U vstupního proudu soketu je tato návratová hodnota signálem odpojení.
Připojení soketu k serveru
Podle parametrů z konzole sestavíme adresu typu InetSocketAddress. Dále vytvoříme soket.
InetSocketAddress addr = new InetSocketAddress(hostname, port);
Socket socket = new Socket();
Nyní se pokusíme připojit. V případě neúspěchu vyvolá příkaz některou z dceřiných výjimek java.io.IOException.
socket.connect(addr);
Spuštění vláken TelnetThread
Vytvoříme obě dvě vlákna typu TelnetThread a jako parametry jim předáme použité proudy. Vlákno čtoucí z konzole nakonfigurujeme jako démona – nikdy totiž neskončí (vstupní proud konzole se neuzavře). To by ale bránilo ukončení aplikace po odpojení soketu. Na smrt démonových vláken však konec běhu aplikace nečeká, proto ho tak musíme označit. Obě vlákna spustíme, přičemž hlavní vlákno necháme čekat, dokud neskončí TelnetThread, které čte ze soketu.
Thread reading = new TelnetThread(socket.getInputStream(), System.out);
Thread writing = new TelnetThread(System.in, socket.getOutputStream());
writing.setDaemon(true);
reading.start();
writing.start();
reading.join();
Nakonec pro jistotu zavoláme metodu close() soketu.
Celý zdrojový kód
Zde je kompletní a detailně okomentovaný zdrojový kód telnetu:
import java.io.*;
import java.net.*;
/** Hlavní třída programu Telnet. Spouští se příkazem "java Telnet host port". */
public class Telnet {
/** Hlavní metoda. Připojí se k serveru a spustí dvě vlákna TelnetThread. */
public static void main(String[] args) {
//ověřit počet parametrů
if(args.length < 2) {
System.out.println("Použití: java Telnet host port");
return;
}
String hostname = args[0]; //získat hostname
//získat port
int port = 0;
try {
port = Integer.parseInt(args[1]); //převést parametr na číslo
}
catch(NumberFormatException e) {
System.out.println("Neplatný port");
System.exit(-1);
}
//vytvořit adresu a soket
InetSocketAddress addr = new InetSocketAddress(hostname, port);
Socket socket = new Socket();
try {
socket.connect(addr); //pokusit se připojit
//vlákno, které čte ze soketu a tiskne na konzoli
Thread reading = new TelnetThread(socket.getInputStream(), System.out);
//vlákno, které čte z konzole a posílá data do soketu
Thread writing = new TelnetThread(System.in, socket.getOutputStream());
writing.setDaemon(true); //vytvořit démona
//spustit vlákna
reading.start();
writing.start();
reading.join(); //počkat odpojení
socket.close(); //uzavřit soket
}
/* Probuzení hlavního vlákna z čekání. Nemělo by nastat. */
catch(InterruptedException e) {
e.printStackTrace();
}
/* Vypršel čas připojení k serveru. */
catch(SocketTimeoutException e) {
System.out.println("Nelze se připojit k serveru.");
System.exit(-1);
}
/* Neznámý host. */
catch(UnknownHostException e) {
System.out.println("Neznámý host.");
System.exit(-1);
}
/* Jiná IO výjimka. Obvykle NoRouteToHostException nebo ConnectException. */
catch(IOException e) {
System.out.println("IO chyba:");
e.printStackTrace();
System.exit(-1);
}
}
/** Toto vlákno čeká na data ze zadaného vstupního proudu a okamžitě je přeposílá do výstupního proudu. */
static class TelnetThread extends Thread {
/** Vstupní proud. */
private InputStream is;
/** Výstupní proud. */
private OutputStream os;
/** Vytvoří nové vlákno pracující se zadanými proudy. */
public TelnetThread(InputStream is, OutputStream os) {
this.is = is;
this.os = os;
}
/** Hlavní metoda vlákna. */
public void run() {
byte[] b = new byte[64]; //vytvořit buffer
try {
while(true) {
int nbytes = is.read(b); //přečíst bajty
if(nbytes == -1) break; //ověřit konec proudu
os.write(b, 0, nbytes); //zapsat bajty
}
System.out.println("Vstupní proud uzavřen.");
}
catch(IOException e) {
System.out.println("IO chyba:");
e.printStackTrace();
System.exit(-1);
}
}
}
}
Spuštění telnetu
Telnet spustíte pomocí příkazu java Telnet host port:
wanto@karmaj:~/root/java_site> java Telnet www.google.com 80
GET / HTTP/1.1
Host: www.google.com
HTTP/1.1 200 OK
...
Závěr
Dnes jsme si vysvětlili úplné základy programování síťových aplikací v Javě. Popsali jsme si tři důležité třídy a naprogramovali naší první klientskou aplikaci – telnet! Příště si ukážeme, jak vytvořit jednoduchý server – to abychom mohli náš telnet nějak využít :-).