A Jáva hálózati könyvtára (java.net) jelenleg a TCP/IP protokollcsaládra épül - ez a szorosan vett Internet hálózat alapja is -, bár ennek részleteit csaknem teljesen elfedi a programozó elõl, így elvileg nem kizárt, hogy más alapprotokollokat használó könyvtár-implementációk is megjelenjenek. Ez a cikk korlátozott terjedelme miatt mindössze a szállítási szintû protokollok közül a TCP kapcsolatorientált protokoll használatával foglalkozik, de érdemes megemlíteni, hogy a java.net könyvtár a datagrammokra épülõ UDP protokollt is támogatja. Ezeken túl jelenleg az alkalmazói szintû protokollok közül csak a Web alapját jelentõ HTTP (Hypertext Transfer Protocol) protokoll közvetlen használatát támogatja, de lehetõséget ad a programozóknak, hogy egyéb kezelõ eljárásokat (protocol handler) is beilleszthessen a rendszerbe. A programkákhoz tartozó néhány speciális hálózattal kapcsolatos tevékenység is megtalálható vagy a könyvtárakban, vagy magában a virtuális gépben.
Az alkalmazások egy részének nincs szüksége ekkora megbízhatóságra, ezek beérik az UDP (User Datagram Protocol) protokoll összeköttetés-mentes szolgáltatásával. Ebben az esetben a könyvtárak és a hálózati architektúra mindössze annyit ígér, hogy egy - nem túl nagy - csomagba összepakolt Byte-okat egyben elküldi a címzett felé, és mindent megtesz azért, hogy az odataláljon, az elõforduló hibák kiküszöbölése érdekében viszont különösképpen nem fáradozik.
Mivel mindkét esetben konkrét programok kommunikálnak, ezek egymásra találásához a számítógéphez tartozó egyedi hálózati címet - az ún. IP címet - ki kell egészítenünk a gépeken futó programok megkülönböztetésére szolgáló címmel. A TCP/IP hálózati rendszerben nem a programokat, hanem a kommunikációra szolgáló kapcsolódási pontokat, az ún. kapukat (port) azonosítjuk. Az elsõ 1024 kapu cím használatát általában korlátozzák, csak rendszergazda jogosultságú programok olvashatják, a többi a "mezei" felhasználók rendelkezésére áll. A kapuk egy része, az ún. jól ismert kapuk (well-known ports) minden számítógépen szabványos funkciókat betöltõ programokhoz tartoznak.
Egy tipikus szerver mûködésének lépései:
Eztán minden indulhat elölrõl, pontosabban a 2. lépéstõl, ahol a szerver új kérésre vár. Amennyiben a szerverünket szeretnénk egyidejûleg több kérés kiszolgálására is felkészíteni, akkor a 3-5 lépéseket egy-egy külön szállal (Thread) érdemes végrehajtatni, miközben az eredeti szál a 2-es lépésben várakozik.
A kliens oldali program még ennél is egyszerûbb. Elõször felvesszük a kapcsolatot az adott Internet címmel (vagy névvel) rendelkezõ számítógép megfelelõ kapuján várakozó kiszolgáló programmal:
Innen a lépések megegyeznek a szerver oldal 3-5-ös lépéseivel.
Megjegyzés: terjedelmi korlátok miatt a programot kissé megkurtítottam. A hálózati hibák részletesebb kezelése és a szerver kulturált leállítása szenvedte a legtöbb csorbát.
import java.net.*; import java.io.*; import java.util.*;
Az osztály minden egyes példánya megvalósít egy TCP szervert. Ehhez párhuzamos szálat indítunk el, hogy ne kelljen a konstruktorban várakozni mindaddig, amíg a szerver le nem áll.
public class TCPListener implements Runnable
{
private ServerSocket server;
private int port;
private TCPServerFactory factory;
private Thread listener;
A konstruktor megkapja annak a kapunak a címét, ahol a szerver a kapcsolatokat várja. A második paraméter egy speciális objektum, amely képes arra, hogy TCPServer típusú objektumokat állítson elõ (ezért nevezik gyárnak, factory-nak). Ezek az objektumok a figyelõ szállal párhuzamosan bonyolítják majd az egyes kliensekkel a kapcsolatot. A konstruktor a paraméterek egyedváltozókba tárolása után elindítja a kapcsolatra figyelõ szálat.
public TCPListener(int port, TCPServerFactory factory)
{
this.port = port;
this.factory = factory;
listener = new Thread(this);
listener.start();
}
public void run ()
{ try {
A hálózati program elsõ lépései az általános modellt követik. A szerver kapu létrehozása után a kiszolgáló végtelen ciklusban accept-tal egy kliens által kezdeményezett kapcsolatra vár. Ha ez megjött, a kiszolgálógyárral elkészíttetünk egy újabb kiszolgáló objektumot.
server = new ServerSocket(port);
while (true) {
Socket socket = server.accept();
TCPServer server = factory.createServer(socket);
}
A hálózati kapcsolat kialakítás közben elõforduló összes hibát együttesen kezeljük - ebben az esetben elhanyagoljuk -, a kiszolgáló leáll.
} catch (IOException e) {}
shutdown();
}
A shutdown módszer feladata a figyelõ szál és az élõ kiszolgálók teljes leállítása.
public void shutdown()
{
if (listener != null) listener.stop();
listener = null;
}
}
import java.net.*;
import java.io.*;
abstract public class TCPServer implements Runnable
{
private Socket socket = null;
private Thread life = null;
protected DataInputStream in = null;
protected PrintStream out = null;
protected TCPServer (Socket s) throws IOException
{
socket = s;
Az osztály konstruktora egyedváltozókban tárolja a paramétereit, majd megszerzi a kiépült kapcsolatot jellemzõ ki- és bemeneti adatfolyamot.
Érdemes megfigyelni, hogy a get Stream eljárásokból visszakapott egyszerû adatfolyamokat a kényelmesebb kezelésük érdekében különbözõ szûrõkön vezetjük át. A Data és Print osztályok a beépített Jáva adattípusok közvetlen be/kivitelét valósítják meg.
in = new DataInputStream(socket.getInputStream()));
out = new PrintStream(socket.getOutputStream()),true);
Az új szálból elindulása elõtt démont csinálunk, azaz olyan szálat amelyet a szerver leállásánál a Jáva virtuális gép magától leállít.
life = new Thread(this);
life.setDaemon(true);
life.start();
}
A kiszolgáló tevékenysége a run eljárásban zajlik. Ez nem más, mint egy általános handleSession módszer meghívása. Az esetleges hibákat a program elkapja, de nem kezeli, hiszen nem is tudna velük mit kezdeni.
public void run()
{ try {
handleSession();
} catch(IOException e) {};
shutdown();
}
A szerver lezárása egyszerû, mind a nyitott adatfolyamokat, mind a socket-et lezárjuk.
public void shutdown()
{ try {
if (in != null) in.close();
if (out != null) out.close();
if (socket != null) socket.close();
} catch (IOException e) { }
}
Szeretnénk a szerverünket általánosan megírni, amely kezeli lehetõleg az egyes kiszolgáló szálak adminisztrációjának minden részletét, a programozónak csak a kapcsolat konkrét lebonyolításával kell foglalkoznia. Erre szolgál az itt következõ handleSession absztrakt módszer. Ezért a TCPServer osztályunk is absztrakt, kell hogy legyen, de az ebbõl leszármaztatott osztályok majd definiálhatják a szükséges módszer törzsét.
A módszer belsejében használhatjuk a nyitott in és out adatfolyamokat. A módszer lenyomatának tanúsága szerint a belsejében az átviteli hibák nem kell foglalkoznunk, ezt majd a felsõ szint (az elõbbi run módszer) kezeli majd.
abstract public void handleSession() throws IOException; }
Jávában az ilyen általános osztályokat interfészek segítségével is megadhatjuk. Esetünkben ez különösen kézenfekvõ megoldás, mert a módszer belsejét amúgy sem tudnánk itt megírni (TCPServerFactory.java).
import java.net.*;
interface TCPServerFactory
{
TCPServer createServer(Socket socket);
}
import java.io.*; import java.net.*;
A szerverünket az általános TCPServer osztályból származtatjuk le. Csak egy konstruktort illetve az absztrakt handleSession módszert kell megírnunk. Hasonló könnyedséggel írhatunk bonyolultabb kiszolgálókat is.
class EchoServer extends TCPServer
{
A konstruktor egyszerûen lehívja a szülõ konstruktorát.
EchoServer (Socket socket) throws IOException
{
super(socket);
}
A szerver szálak tényleges tevékenységét itt kell megadnunk. Mivel a módszer belsejében már hozzáférhetünk a 2 megnyitott adatfolyamhoz, egyszerûen csak az egyikrõl olvasunk egy sort, majd kiírjuk a másikra mindaddig, amíg - kis és nagybetûket tetszõlegesen tartalmazó - "EXIT" szöveg nem érkezik. Az egyedül arra érdemes figyelni, hogy a kapcsolat elvesztésével elõfordul, hogy szöveg helyett null-t olvasunk.
public void handleSession() throws IOException
{ String s;
do {
s = in.readLine();
if (s != null) out.println(s);
} while (!(s == null || s.toUpperCase().equals("EXIT")));
}
Szerverünk kipróbálásához elindítunk egy többszálú szerver-csonkot az 5555-ös kapun, megadva neki egy olyan "gyárat", amely termeli majd a kiszolgáló objektumainkat.
public static void main (String[] args)
{
new TCPListener(5555, new EchoServerFactory());
}
}
A gyár definiálása hasonlóan egyszerû, hiszen csak egyetlen módszert kell megadnunk. A termelõ eljárás egyszerûen lehívja a szerver osztály konstruktorát. Ha ez esetleg hibát okoz, akkor a termelõ null-lal tér majd vissza.
class EchoServerFactory implements TCPServerFactory
{
public TCPServer createServer(Socket socket)
{ try {
return new EchoServer(socket);
} catch (IOException e) { return null; }
}
}
A szerver kipróbálásához magyarázat nélkül közlöm egy egyszerû kliens program (EchoClient.java) forrását. A program követi a cikk elején az általános kliens mûködésnél leírtakat.
import java.net.*;
import java.io.*;
class EchoClient
{
public static void main (String[] args)
{
Socket socket = null;
DataInputStream in = null,
console = null;
PrintStream out = null;
String s;
try
{
socket = new Socket(args[0], 5555);
in = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
out = new PrintStream(
new BufferedOutputStream(socket.getOutputStream()), true);
console = new DataInputStream(System.in);
System.out.println("Echo client started, type exit to finish!");
do
{
out.println(console.readLine());
s = in.readLine();
} while (!( s == null || s.toUpperCase().equals("EXIT")));
if (in != null) in.close();
if (out != null) out.close();
if (socket != null) socket.close();
}
catch (IOException e) {};
}
}
Kiss István
updated: 97/05/01, http://www.eunet.hu/infopen/cikkek/java/network.html