Hálózatkezelés Jávában


Tartalomjegyzék:
Szállítási szintû összeköttetés kezelése
Általános szerver és felhasználása
TCPListener osztály
Az absztrakt TCPServer osztály
A kiszolgáló-gyár
Visszhang

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.

Szállítási szintû összeköttetés kezelése

A TCP/IP hálózat szállítási rétege kétfajta, az. ún. összeköttetés alapú és összeköttetés-mentes szolgáltatást ismer. A összeköttetés alapú szolgáltatást megvalósító TCP (Transfer Control Protocol) protokoll tulajdonképpen a kommunikáló programok között egy kétirányú csõvezetéket valósít meg, amelyen az információ Byte-ok formájában áramlik. A kapcsolatorientált szolgáltatást megbízhatónak is nevezik, mert a TCP biztosítja a Byte-folyam torzításmentes, sorrendhelyes átvitelét, kiküszöbölve az elveszett, megsérült, megduplázódott, késve érkezett információ-csomagok hatását. Ha pedig semmi nem segít, akkor legalább a küldõ, de néha a vevõ is értesül arról, hogy valami helyrehozhatatlan hiba történt.

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:

  1. Az egyes kapukhoz tartozó kommunikációs csatornákat socket-eknek nevezzük. Kapcsolatorientált kommunikációnál kétfajta socket használatos: a szerver oldalon egy kiépülésre váró kapcsolatot az ún. ServerSocket osztály egy példánya valósítja meg:
  2.  

    ServerSocket server = new ServerSocket(port);
  3. A kliens/szerver rendszerekben a szerver passzív szerepet játszik, arra várakozik, hogy egy kliens megszólítsa. Ha sikeresen kiépül a kapcsolat, a szerver visszatér az accept hívásból a kiépült kapcsolatot reprezentáló Socket példányból.
  4.  

    Socket socket = server.accept();
  5. A socket-bõl két, az input - kliensbõl szerver felé irányuló - és az output - szerverbõl kliens felé tartó - adatfolyam kapható meg.
  6.  

    InputStream input = socket.getInputStream();
    OutputStream input = socket.getOutputStream();
  7. A szerver törzse az így megszerzett input adatfolyamot olvassa, a vett információt értelmezi és esetleg az output folyamon válaszol.
  8.  

  9. Az adatcsere végén a szervernek vagy a kliensnek le kell zárnia a kapcsolatot jelentõ socket-et. Ehhez elõször illik az adatfolyamokat lezárni.
  10.  

    input.close();
    output.close();
    socket.close();
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:

 

Socket socket = new Socket (host, port);

Innen a lépések megegyeznek a szerver oldal 3-5-ös lépéseivel.

Általános szerver és felhasználása

A példaprogramunk egy általános szerver programot valósít meg. Általános abban az értelemben, hogy a szerver tevékenységét egy absztrakt osztály valósítja meg, a konkrét szervert megvalósító, leszármazott osztályban egy konstruktort és egy módszert kell csak megírnunk. A szerverünk párhuzamos mûködésû, azaz a hozzá tartozó kapun folyamatosan várja a klienseket, egy kiépülõ kapcsolat kiszolgálására saját szálat indít el. Így egyidejûleg több klienssel is kommunikálhat.

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.

TCPListener osztály

A kapcsolat figyelését és a párhuzamos kiszolgáló szálakat a TCPListener osztály (TCPListener.java) végzi. 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. 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. 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. 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. A shutdown módszer feladata a figyelõ szál és az élõ kiszolgálók teljes leállítása.

Az absztrakt TCPServer osztály

A TCPServer osztály (TCPServer.java) feladata az egyes kliensektõl érkezõ kapcsolatok kezeléséhez szükséges adminisztráció lebonyolítása. 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.

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. 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. A szerver lezárása egyszerû, mind a nyitott adatfolyamokat, mind a socket-et lezárjuk. 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.

A kiszolgáló-gyár

Ez legérdekesebb ötlet az egész programban. Az elv ismerõs, az objektumorientált programozásban újabban nagyon divatos módszertan, az ún. tervezési minták (design pattern) egyik ismert tagja a "gyár". Akkor használjuk, amikor azonos objektumokat kell legyártani, de a program fordításakor még nem tudjuk ezek pontos típusát. A megoldás: definiálunk egy általános "gyár" osztályt, amely adott - esetünkben a createServer eljárása - segítségével általános objektum-példányokat - itt TCPServer típusúakat - gyárthatunk. A program futásánál aztán majd az általános gyár helyett egy konkrétat használunk, amelyik az általános eredménytípusnak megfelelõ, abból leszármaztatott típusú objektumokat állít elõ.

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).

Visszhang

Az általános szerverünk felhasználására egy olyan példát mutatok, amely egyszerûen soronként visszaküld mindent a kliensnek (EchoServer.java). A szerver leáll, ha "exit" üzenetet vesz. 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. A konstruktor egyszerûen lehívja a szülõ konstruktorát. 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. 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. 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. 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.
Kiss István

  updated: 97/05/01, http://www.eunet.hu/infopen/cikkek/java/network.html 1