• Nem Talált Eredményt

Egy komplex alkalmazás készítése

In document Szoftverfejlesztés II. (Pldal 183-200)

12.1 TANANYAG

A záró feladatunk egy chatalkalmazás készítése. A programnak a következő tulajdonságokkal kell rendelkeznie:

 grafikus felhasználói felület,

 chat típusának megadása, hogy szervert vagy klienst indítunk,

 szerver címének és portjának megadása,

 felhasználónév megadása,

 üzenetek listázása,

 a chat-be bejelentkezett felhasználók nevének megjelenítése,

 státuszsor a rendszerinformációk megjelenítésére.

A programunk egy hálózatos alkalmazás lesz, amely TCP/IP protokoll segít-ségével kommunikál, tehát összeköttetés-alapú kommunikációról van szó. A programban egy chatszoba van, tehát nincs szükség arra, hogy egy-egy felhasz-nálóval külön privát beszélgetéseket folytathassunk. Nézzük a program kezelő-felületét: úgy egyszerűbb leírni, hogy pontosan mi a feladat.

48. ábra: A chat alkalmazás GUI-vázlata

Először is meg kell adni, hogy szervert vagy klienst szeretnénk-e indítani.

Amennyiben szervert indítunk, akkor ott a cím egyértelmű, hiszen a saját gé-pünk címét le tudjuk kérni, csak a portot kell megadni. Ha klienst akarunk indí-tani, vagyis csatlakozni szeretnénk egy szerverhez, akkor ott a címet és a portot is meg kell adni.

A felhasználónév a chat üzeneteinek ablakában lesz majd látható, termé-szetesen figyelni kell, hogy azonos nevű felhasználók ne legyenek a szobában.

Az adatok megadása után elindítjuk a chatet, majd aktív lesz az üzenet szöveg-mező, ahol írhatjuk az üzeneteinket. Alul látható a státuszsor, ahol meg lesz jelenítve, hogy pl. sikerült-e a csatlakozás a szerverhez, vagy ha nem sikerült, akkor miért stb. A bal oldali oszlopban láthatóak a chatszoba felhasználói. Ez a lista mindig az aktuálisan résztvevő felhasználókat mutatja.

Nézzünk először egy rövid áttekintést, hogy milyen osztályokra van szük-ség, és azoknak milyen adattagjai és metódusai vannak:

49. ábra: A chat alkalmazásunk UML-diagramja

A chat alkalmazás készítésének egyik fontos része, hogy meghatározzuk azt, hogyan továbbítjuk az üzeneteket, milyen részei vannak az üzenetnek:

 üzenet típusa: az üzenet jellegére utal, pl. hogy valódi üzenet-e (message) vagy valamilyen rendszerüzenet (pl. login, logout),

 az üzenet feladója, vagyis a felhasználónév,

 az üzenet szövege.

Az üzenetekért felelős osztály így néz ki:

1 public class Message implements Serializable { 2 private String type=null;

3 private String nickName=null;

4 private String message=null;

5 private static final long serialVersionUI=2L;

6 public Message(String t,String nick,String mess) { osztálynak implementálnia kell a Serializable interfészt. Az interfész nem tartalmaz adattagokat és metódusokat, a Message objektumok szerver- és kliensoldali írásához (ObjectOutputStream.writeObject) és olvasásá-hoz (ObjectInputStream.readObject) van rá szükség. A Message osztályban létre kell hozni egy serialVersionUID változót (a JVM is létre-hozza, de ez az ajánlott), amelynek értéke tetszőleges, viszont innen tudja majd

a readObject és writeObject metódus, hogy ugyanazon osztály objek-tumáról van szó.

Nézzük a Server osztályunkat:

1 public class Server implements Runnable { 2 private ServerSocket ss=null;

3 private Hashtable<String,ConnectionHandler>

users=null;

4 private volatile Server running_id=this;

5 public Server(int port) {

33 }else{

Ezen osztály példányainak tartalmaznia kell egy ServerSocket objek-tumot, amely folyamatosan figyeli (ServerSocket.accept), hogy van-e csatlakozni akaró kliens. Mivel ennek állandóan futnia kell, ezért ezt az osztályt úgy kell megírni, hogy szálként futtathassuk. Gondoljunk arra, hogy számos feladat van, amit még el kell látni a programunknak (pl. a felhasználói felülettel való interakció), ezért nem „ragadhat le” az accept-nél. Tehát implementálni kell a Runnable interfészt. A szervernek még tartalmaznia kell az összes kli-enst kezelő ConnectionHandler példányt is, hogy bármely kliens üzenetét továbbíthassa a többinek. Ez lesz a users változóban, amely egy Hashtable lesz, a kulcs a felhasználónevek azonosítója lesz. Ez azért jó, mert egy adott nevű felhasználóhoz tartozó ConnectionHandler példányt így rögtön el lehet érni. A Server konstruktorban elindítjuk a ServerSocket-et a meg-adott porton, létrehozzuk a hash-táblát és elindítjuk a szálat. A szál folyamato-san figyeli, hogy van-e csatlakozni akaró kliens, ha van, akkor beléptetjük a rendszerbe a loginUser metódussal. A metódusban lekérjük a socket-hez tartozó bemeneti/kimeneti adatfolyamot, és beolvassunk róla egy Message objektumot. Megnézzük, hogy van-e már ilyen nevű felhasználó (29–30. sor).

Ha nincs, akkor felvesszük a felhasználók közé az adott klienst kiszolgáló ConnectionHandler objektumot. A stopp metódus felel a szerver leállí-tásáért a szálkezelésnél tanultaknak megfelelően.

A ConnectionHandler osztály megvalósítása:

1 public final class ConnectionHandler implements Runnable {

2 private ObjectInputStream ois=null;

3 private ObjectOutputStream oos=null;

4 private String nickName=null;

5 private Hashtable<String, ConnectionHandler>

users=null;

6 private volatile ConnectionHandler running_id=this;

7 public ConnectionHandler(String

nick,ObjectInputStream ois,ObjectOutputStream

39 } 40

41 } catch (Exception e) {}

42 }

43 public void sendMessage(Message m) { 44 try {

51 public void refreshUserList() {

52 String userList=getUserString();

59 public String getUserString() {

60 StringBuilder sb=new StringBuilder();

A ConnectionHandler példányok tehát mindig az egyes klienshez tar-toznak, feladatuk a szerveroldalon figyelni, hogy a kliens küld-e üzenetet. Ezen példányoknak is folyamatosan futnia kell, ezért ezt is szálként kell elindítani, vagyis implementáljuk a Runnable interfészt. Egy ConnectionHandler példány tartalmazza a klienshez tartozó bemeneti/kimeneti adatfolyamokat, a felhasználó nevét és a felhasználók hash-táblájának referenciáját. Ez azért kell, hogy egy üzenetet az összes felhasználónak elküldhessünk. A run metódusban a readObject folyamatosan vár egy Message objektum érkezésére (19.

sor). A while ciklussal minden kliensnek elküldjük az üzenetet, kivéve az üze-net küldőjének. Amennyiben az üzeüze-net típusa „logout”, akkor a felhasználó ki akar lépni a chatből, ezért a hash-táblából kivesszük a ConnectionHandler

példányát és leállítjuk a szálat. Amennyiben az üzenet típusa „login” vagy

„logout”, az azt jelenti, hogy egy felhasználó belépett vagy kilépett a chatből.

Ilyenkor minden felhasználó felé el kell küldeni az aktuális felhasználók listáját, hogy lássák bal oldalon, éppen ki van a chatszobában. A refreshUserList metódus feladata, hogy az aktuális listát elküldje minden felhasználónak. A listát a getUserString metódus egy sztringként adja vissza, ahol a felhasz-nálónevek egymástól #-tel vannak elválasztva. Ez a lista egy „user” típusú Message objektumban lesz kiküldve. A klienshez tartozó rész majd tudni fogja, hogy az „user” típusú üzenetben felhasználói lista szerepel.

Nézzük a klienshez tartozó osztályt:

1 public class Client {

2 private ClientHandler handler=null;

3 private Socket socket=null;

4 private Chat chat=null;

5 private ObjectOutputStream oos=null;

6 private ObjectInputStream ois=null;

7 private String nickName=null;

8 boolean connected=true;

9 public Client(String sname,int port,String nick,Chat c) { Message("login", nick, "beléptem a chat-be"));

22 }

23 if (mess.getType().equals("login") &&

mess.getMessage().equals("denied")) {

24 chat.displayMessage(new

Message("login", nick, "létezik már ilyen azonosító, csatlakozás sikertelen"));

25 connected=false;

26 }

27 } catch (Exception ex) {}

28 }

29 public void sendMessage(Message message) { 30 try {

55 public ObjectInputStream getOis(){

56 return ois; feladata az lesz, hogy folyamatosan figyelje, hogy a szerver küld-e neki

üzene-tet. Tartalmazni fogja a socketet, a GUI-ért felelős Chat példány referenciáját, a sockethez tartozó bemenő/kimenő adatfolyamot és a felhasználónevet. A konstruktorban felépítjük a kapcsolatot a szerverrel, és küldünk neki egy „login”

üzenetet. Ha a szerver „agree”-t küldött vissza, akkor sikerült a belépés, ha

„denied”-et, akkor nem. A bye metódus feladata, hogy értesítse a szervert (és a szerveren keresztül az összes felhasználót), hogy kiléptünk a chatből.

A ClientHandler osztály kódja:

1 public class ClientHandler implements Runnable { 2 private Client client;

A ClientHandler példányok folyamatosan futni fognak, tehát szüksé-ges a Runnable interfész. Amennyiben egy üzenetet kap, akkor megnézi,

hogy az „user” típusú-e, ha nem, akkor megjeleníti az üzenetet, ha igen, akkor a GUI felhasználói listáját frissíti.

A Chat osztályunk felel a GUI megvalósításáért:

1 public class Chat {

2 final int FRAME_WIDTH=640,FRAME_HEIGHT=480;

3 JFrame frame=null;

11 DefaultListModel listModel=null;

12 boolean isServer=true;

26 frame.setSize(FRAME_WIDTH,FRAME_HEIGHT);

27 frame.setVisible(true);

34 textfieldAddress.setBounds(120,10,150,20);

47 radioServer=new JRadioButton("Szerver");

48 radioServer.setBounds(120,40,100,20);

49 radioServer.setActionCommand("server");

50

radioServer.addActionListener(radioListener);

51 radioServer.setSelected(true);

52 radioClient=new JRadioButton("Kliens");

53 radioClient.setBounds(230,40,100,20);

54 radioClient.setActionCommand("client");

55

70 buttonStart.setActionCommand("start");

71 buttonStart.addActionListener(new

88 scrollpaneTextarea.setAutoscrolls(true);

89

104 listModel=new DefaultListModel();

112 textfieldAddress.setText(serverAddress);

113 radioServer.doClick();

114 }

115 public void setUIEnabled(boolean b) { 116 textfieldAddress.setEnabled(b);

125 public void displayMessage(Message message) { 126 if (message.getType().equals("message")) {

132 public void refreshUserListGUI(String users) { 133 listModel.clear();

142 class RadioActionListener implements ActionListener {

143 public void actionPerformed(ActionEvent e)

153 class StartButtonActionListener implements ActionListener { mert van már ilyen felhasználó

165 if (!client.isConnected()) {

174

185 class SendButtonActionListener implements ActionListener {

A konstruktorban létrehozzuk a frame-et, erre helyezzük el a GUI elemeit, amelyet a createUI metódus végez. Az eseményeket figyelő objektumokat (listener) belső osztályok segítségével készítjük el. Szükség van egy listenerre a rádiógombokhoz, ugyanis szerver választása esetén a szerver címét tartalmazó szövegmezőnek inaktívnak kell lennie. Továbbá szükséges még a két gombhoz tartozó listener is. Azt is csinálhatnánk, hogy a Chat osztály implementálja az ActionListener interfészt, ebben az esetben az actionPerformed metódusban figyelni kellene, hogy mely objektum váltotta ki az eseményt. Bel-ső osztályok segítségével átláthatóbb lesz a program, továbbá a belBel-ső osztá-lyokból el tudjuk érni a Chat osztály aktuális példányát, így hozzáférhetünk az adattagjaihoz és metódusaihoz is. A StartButtonActionListener actionPerformed metódusa szorul némi magyarázatra. Ez végzi a szer-ver/kliens elindítását/leállítását. A buttonStart gombnak beállított (setActionCommand) action command értékkel tudjuk vizsgálni, hogy most indításra vagy leállításra van-e szükség. Ha az isServer értéke true, akkor szervert kell indítani a megadott porton. Kliens indítására mindig szükség van, még szerver választása esetén is, ugyanis ő is kommunikálhat a kliensekkel (nemcsak a kliensek egymással). Amennyiben a kliens nem tudott csatlakozni a chat-hez, akkor leállítjuk a klienst, egyébként a buttonStart-nak új feliratot

és action commandot adunk meg, majd a GUI-nak az üzenetküldésre vonatkozó részét aktívvá tesszük, a többit pedig inaktívvá.

50. ábra: A chat program működés közben

In document Szoftverfejlesztés II. (Pldal 183-200)