Ćwiczenia 4
Temat zajęć:
Programowanie gniazd na platformie Java.
Literatura:
- R.
Stevens, "Programowanie zastosowań sieciowych w systemie Unix"
- A. Jones, J. Ohlund, "Programowanie sieciowe
Microsoft Windows"
- C. Petzold, "Programowanie Windows"
- M. Gabassi, B. Dupouy, "Przetwarzanie rozproszone
w systemie Unix"
- E. Harold, "Java: programowanie sieciowe"
- R. Stevens, "Biblia TCP/IP" (tom 1 - Protokoły i
tom 2 - Implementacje)
- C. Hunt, "TCP/IP - administracja sieci"
- V. Toth, "Programowanie Windows 98/NT - księga
eksperta"
- Dokumenty RFC wersja on-line
- Dokumentacja
WinSock w MSDN wersja on-line
Uwaga
1: Opis większości klas i metod, z których
będziemy korzystać na zajęciach znajduje się w dokumentacji JDK: http://java.sun.com/javase/6/docs/api/
(pakiety java.net i java.io).
Uwaga 2: W niektórych przypadkach
zabezpieczenia lokalnej maszyny wirtualnej mogą uniemożliwiać
korzystanie z lokalnych portów i/lub wykonywanie połączeń. W
takim przypadku należy zmienić zabezpieczenia maszyny wirtualnej na
czas wykonywania danego programu korzystającego z gniazd. Można to
zrobić za pomocą odpowiedniego pliku policy (zasady
zabezpieczeń). Uruchamiając program należy wówczas podać
dodatkowy parametr przy starcie maszyny wirtualnej:
java -Djava.security.policy=mojplik.policy klasa_do_uruchomienia
Plik
java.policy (pobierz)
umożliwiający dostęp do wszystkich portów powyżej 1024 i
wykonywanie dowolnych połączeń wygląda następująco:
grant {
permission java.net.SocketPermission "*:1024-65535", "accept";
permission java.net.SocketPermission "*:1-65535", "connect";
};
Wstęp
Z
poziomu języka Java i pakietu JDK dużo łatwiej jest korzystać z gniazd
TCP niż UDP (i są one używane częściej), dlatego skupimy się
głównie na nich. Do komunikacji sieciowej w Javie
wykorzystujemy zwykle klasy:
- java.net.Socket
- java.net.InetAddress
- java.net.ServerSocket
- java.net.DatagramSocket
- java.net.DatagramPacket
Dodatkowo
w przypadku TCP korzystamy z java.io.InputStream,
java.io.OutputStream oraz z ich klas potomnych
(dla wygody), np. java.io.DataInputStream i java.io.DataOutputStream
(a także z różnego typu klas buforujących).
Należy zaznaczyć, że praktycznie wszystkie metody klas obsługujących
połączenia sieciowe mogą wyrzucić wyjątki w przypadku
błędów; zwykle jest to IOException lub
jedna z jej klas potomnych. Wyjątki te należy przechytywać (za pomocą
odpowiednich konstrukcji try { ... } catch (...) { ... })
i obsługiwać.
Gniazda UDP
Zajmiemy
się najpierw UDP. Pierwszą czynnością jest konstrukcja obiektu klasy java.net.DatagramSocket.
Do konstruktora tej klasy możemy podać numer portu, co spowoduje
powiązanie tworzonego gniazda z danym numerem portu na lokalnej
maszynie. Zatem możemy postąpić np. tak:
DatagramSocket s = new DatagramSocket(); // dowolny numer lokalnego portu nadany przez system
lub
tak:
DatagramSocket s = new DatagramSocket(4750); // gniazdo UDP powiązane z lokalnym portem 4750
Zwykle
w przypadku aplikacji, która czeka na dane od
klientów korzystać będziemy z tego drugiego konstruktora, a
implementując klienta (gdy nasz lokalny numer portu nie jest dla nas
istotny) - z pierwszego.
Klasa DatagramSocket
posiada dwie podstawowe metody służące do wysyłania i odbierania
datagramów UDP:
- void
send(DatagramPacket p) - wysyła dane zapakowane w datagramie
p
- void
receive(DatagramPacket p) - odbiera datagram i umieszcza
jego zawartość w p
Kluczem
jest tutaj klasa DatagramPacket. Jeśli chcemy
skonstruować pakiet przeznaczony do wysłania, to korzystamy z
konstruktora
DatagramPacket(byte[] buf, int len, InetAddress address, int port)
Czyli
podajemy tablicę bajtów do wysłania, liczbę
bajtów, jaka ma być z tej tablicy przesłana, adres odbiorcy
(obiekt klasy InetAddress - za moment
wrócimy do tej klasy) oraz numer portu odbiorcy. Jeśli tak
skonstruowany datagram przekażemy następnie metodzie send
klasy DatagramSocket, zostanie on wysłany pod
wskazany adres, na podany numer portu, za pomocą protokołu UDP.
Jeśli
interesuje nas odbiór datagramu, to również musimy
najpierw go skonstruować. Używamy do tego zazwyczaj innego
konstruktora (nie podajemy adresu, bo nie wiemy przecież skąd datagram
nadejdzie):
DatagramPacket(byte[] bufor, int maxlen)
Konstruujemy
zatem "pusty" pakiet, zawierający podany bufor
danych oraz maksymalną liczbę bajtów, jaką chcemy odebrać
(oczywiście nie powinna ona przekraczać rozmiaru bufora, ale może być
mniejsza).
Tak skonstruowany pakiet podajemy
następnie metodzie receive, która
czeka na nadejście datagramu i jego zawartość umieszcza w podanym przez
nas obiekcie klasy DatagramPacket.
Klasa
DatagramPacket posiada (między innymi) dwie użyteczne metody:
- InetAddress
getAddress() - podaje adres nadawcy datagramu
- int
getPort() - podaje numer portu nadawcy datagramu
Umożliwiają
one wydobycie adresu zwrotnego i numeru portu nadawcy, tak aby możliwe
było np. odesłanie potwierdzenia czy kontynuowanie wymiany danych
zgodnie z zadanym protokołem.
Klasa InetAddress
reprezentuje adresy IP. Co ciekawe, nigdy nie konstruujemy
obiektów klasy InetAddress
bezpośrednio (klasa ta nie ma publicznego konstruktora).
Zamiast tego, korzystamy ze statycznych metod klasy InetAddress,
które zwracają obiekty tej klasy. Jeśli chcemy np. stworzyć
obiekt reprezentujący adres hosta o nazwie atos,
to korzystamy z konstrukcji:
InetAddress a = InetAddress.getByName("atos");
Jeśli
chcemy użyć adresu IP, np. 150.254.78.2, korzystamy
z tej samej metody:
InetAddress a = InetAddress.getByName("150.254.78.2");
(wiemy
już, że funkcja gethostbyname(...) zwraca dane o
hoście niezależnie od tego, czy podamy jako parametr nazwę domenową,
czy tekstową reprezentację adresu IP - metoda InetAddress.getByName(...)
zachowuje się dokładnie tak samo).
Klasa InetAddress
posiada również metodę getByAddress,
jednak wymaga ona podania tablicy bajtów, zawierającej adres
IP w formacie sieci (czyli 4 bajty w formacie MSB).
Aby
stworzyć obiekt klasy InetAddress reprezentujący
adres naszego lokalnego hosta, używamy konstrukcji
InetAddress myhost = InetAddress.getLocalHost();
Jeśli
mamy już obiekt klasy InetAddress, to możemy
skorzystać z metody String getHostName() aby
poznać kanoniczną nazwę domenową odpowiadającą danemu adresowi.
Podane
informacje wykorzystamy w następującym przykładzie, ilustrującym
sposób wykorzystania gniazd w Javie do komunikacji UDP.
Przykład
1
Serwer UDP czeka na podanym porcie na nadejście datagramu.
Zakładamy, że w datagramie będzie zakodowany łańcuch tekstowy. Serwer
wyświetla otrzymany łańcuch na konsoli, po czym odsyła do nadawcy
datagramu datagram z zakodowanym łańcuchem "Odebrano.".
Klient tworzy datagram z zakodowanym łańcuchem "SIK420",
wysyła go na podany adres (lub nazwę) i podany numer portu, po czym
czeka na datagram od serwera. Po odebraniu datagramu wyświetla zawarty
w nim tekst na konsoli.
Plik c5p1a.java
pobierz
import java.io.*;
import java.net.*;
public class c5p1a {
private DatagramSocket socket; // gniazdo UDP
public c5p1a(int aport) throws IOException {
// utworzenie i powiazanie z portem
socket = new DatagramSocket(aport);
}
void dataExchange() {
byte[] bufor = new byte[256];
// "pusty" pakiet do odbioru danych
DatagramPacket p = new DatagramPacket(bufor, 256);
try {
socket.receive(p); // czekaj na datagram
// napisz kto jest nadawca
System.out.println(
"Od: "+p.getAddress().toString()+
" ("+p.getAddress().getHostName()+")");
// utworz lancuch z tablicy bajtow
String s = new String(p.getData());
// wypisz wiadomosc
System.out.println(s);
// utworz datagram zwrotny
// korzystajac z adresu nadawcy
String response = "Odebrano.";
DatagramPacket p2 = new DatagramPacket(
response.getBytes(), response.length(),
p.getAddress(), p.getPort());
socket.send(p2); // wyslij odpowiedz
socket.close(); // koniec protokolu
// jesli cos poszlo nie tak
// wypisz stan stosu wywolan
} catch (Exception e) {
e.printStackTrace();
}
}
// args[0] - numer portu w wierszu polecen
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(
"Podaj numer portu jako parametr"
);
return;
}
try {
c5p1a server = new c5p1a(
Integer.parseInt(args[0]));
System.out.println("Czekam...");
server.dataExchange();
// ew. wyjatek wyrzucany przez konstruktor
// c5p1a
} catch (Exception e) {
e.printStackTrace();
}
}
}
Plik
c5p1b.java pobierz
import java.io.*;
import java.net.*;
public class c5p1b {
// args[0] - nazwa hosta (lub IP)
// args[1] - numer portu
public static void main(String[] args) {
if (args.length < 2) {
System.out.println(
"Podaj nazwe hosta i numer portu"
);
return;
}
try {
// utworzenie gniazda - lokalny port niewazny
DatagramSocket socket = new DatagramSocket();
// ustalenie adresu na podstawie args[0]
InetAddress addr = InetAddress.getByName(args[0]);
// utworzenie datagramu z wiadomoscia
String s = "SIK 420";
DatagramPacket p = new DatagramPacket(
s.getBytes(), s.length(),
addr, Integer.parseInt(args[1]));
// wyslanie wiadomosci
socket.send(p);
// utworzenie "pustego" datagramu
byte[] bufor = new byte[256];
DatagramPacket p2 = new DatagramPacket(bufor, 256);
// czekaj na wiadomosc zwrotna
socket.receive(p2);
// utworz lancuch z tablicy bajtow
String response = new String(p2.getData());
// wyswietl wiadomosc zwrotna
System.out.println("Serwer powiedzial: "+response);
// koniec protokolu
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Kompilacja:
javac c5p1a.java
javac c5p1b.java
Uruchomienie:
Konsola 1:
java c5p1a numer_portu
Konsola
2:
java c5p1b nazwa_hosta numer_portu
TCP
W
przypadku komunikacji TCP mamy do dyspozycji dużo wygodniejsze
narzędzia. Po pierwsze jednak, rozróżniamy tutaj stronę
nasłuchującą i nawiązującą połączenie. Strona nasłuchująca korzysta z ServerSocket
(nasłuch) i Socket (komunikacja z klientem po
nawiązaniu przez niego połączenia). Klient korzysta tylko z klasy Socket.
Obiekty klasy ServerSocket
tworzymy zwykle następująco:
ServerSocket ssocket = new ServerSocket(3455); // tworzy gniazdo nasłuchujące
// i wiąże je z portem 3455
choć
możliwe jest skorzystanie z konstruktora bezparametrowego i
późniejsze wywołanie metody bind.
Kluczową metodą klasy ServerSocket jest accept,
która czeka na nawiązanie przez połączenia przez inny
program, po czym zwraca obiekt klasy Socket,
którego używamy do wymiany danych z tym jednym konkretnym
klientem. Metoda ta jest zatem odpowiednikiem funkcji accept,
która również zwraca nowo utworzone gniazdo po
odebraniu przychodzącego połączenia.
Strona aktywna
(klient) używa klasy Socket, konstruując jej
obiekty np. tak:
Socket s = new Socket("atos", 3000);
Podanie
nazwy hosta (lub adresu IP lub obiektu klasy InetAddress)
i numeru portu powoduje automatycznie nawiązanie połączenia z wybranym
hostem i numerem portu (czyli automatycznie wywoływane jest connect).
Możliwe jest również wywołanie connect
na wcześniej utworzonym gnieździe, np. tak:
s.connect(new InetSocketAddress("atos",3000));
Istnieje
jeszcze drugi wariant metody connect,
który ma postać
void connect(SocketAddress endpoint, int timeout);
i
który pozwala na ustawienie maksymalnego czasu oczekiwania
(parametr timeout, wyrażony w milisekundach) na
nawiązanie połączenia. W przypadku upłynięcia tego czasu bez poprawnego
nawiązania połączenia, metoda connect wyrzuca SocketTimeoutException.
W
obu przypadkach (i serwera, i klienta), jeśli mamy już poprawny obiekt
klasy Socket, możemy korzystać ze strumieni
wejściowych i wyjściowych. Klasa Socket
udostępnia następujące metody:
- InputStream
getInputStream() - zwraca strumień wejściowy skojarzony z
danym gniazdem
- OutputStream
getOutputStream() - zwraca strumień wyjściowy skojarzony z
danym gniazdem
Ponieważ klasy InputStream
i OutputStream są dość ubogie (pozwalają na
czytanie i pisanie bajtów lub tablic bajtów),
zwykle "nadbudowujemy" na obiektach tych klas jakieś bardziej użyteczne
strumienie. Możemy np. skorzystać z klas DataInputStream
i DataOutputStream, gdzie przekazujemy odp.
strumienie do konstruktorów. Przykład:
DataInputStream dis = new DataInputStream(socket.getInputStream());
Teraz
można już korzystać np. z dis.readLong(), dis.readDouble()
czy dis.readUTF() aby odczytywać ze strumienia
(czyli z gniazda) dane konkretnego typu (tutaj odp. long,
double i String). W
przypadku strumieni wyjściowych sytuacja jest analogiczna - DataOutputStream
posiada metody writeT (gdzie T
jest nazwą jednego z podstawowych typów Javy).
Przykład
2
Prosty przykład ilustrujący podstawowe techniki stosowane w
przypadku gniazd TCP w Javie. Wykorzystane zostały strumienie DataInputStream
i DataOutputStream, a klient przesyła do serwera
kolejno liczbę całkowitą, łańcuch tekstowy i liczbę rzeczywistą.
Plik
c5p2a.java pobierz
import java.io.*;
import java.net.*;
public class c5p2a {
private ServerSocket ssocket; // do nasluchu
private int port; // nr portu
public c5p2a(int aport) throws IOException {
port = aport;
// utworzenie gniazda nasluchu
// i powiazanie go z portem (bind)
ssocket = new ServerSocket(port);
}
public void startListening() {
Socket socket = null; // dla jednego klienta
int ivalue;
String svalue;
double dvalue;
try {
// czekaj na polaczenie
socket = ssocket.accept();
// pobierz strumienie i nadbuduj
// na nich "lepsze" strumienie
DataInputStream dis = new DataInputStream(
socket.getInputStream());
DataOutputStream dos = new DataOutputStream(
socket.getOutputStream());
// czytaj kolejno int, String i double
ivalue = dis.readInt();
svalue = dis.readUTF();
dvalue = dis.readDouble();
// wypisz co odebrano
System.out.println("Odebrano:");
System.out.println(ivalue);
System.out.println(svalue);
System.out.println(dvalue);
// odpisz klientowi
dos.writeUTF("ODEBRANO POPRAWNIE");
// zamknij strumienie
dis.close();
dos.close();
// zamknij gniazdo
socket.close();
// zamknij gniazdo nasluchujace
ssocket.close();
// jesli cos pojdzie zle, wypisz
// stos wywolan
} catch (Exception e) {
e.printStackTrace();
return;
}
}
// args[0] - numer portu
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(
"Podaj numer portu"
);
return;
}
try {
// ustal port
int port = Integer.parseInt(args[0]);
c5p2a server = new c5p2a(port);
System.out.println("Czekam...");
server.startListening();
// moze byc albo wyjatek z konstruktora
// c5p2a albo z Integer.parseInt jesli parametr
// nie jest liczba
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Serwer zakonczyl dzialanie.");
}
}
Plik
c5p2b.java pobierz
import java.io.*;
import java.net.*;
public class c5p2b {
// args[0] - nazwa hosta
// args[1] - numer portu
public static void main(String[] args) {
if (args.length < 2) {
System.out.println(
"Podaj nazwe hosta i port"
);
return;
}
try {
// ustal adres serwera
InetAddress addr = InetAddress.getByName(args[0]);
// ustal port
int port = Integer.parseInt(args[1]);
// utworz gniazdo i od razu podlacz je
// do addr:port
Socket socket = new Socket(addr, port);
// pobierz strumienie i zbuduj na nich
// "lepsze" strumienie
DataOutputStream dos = new DataOutputStream(
socket.getOutputStream());
DataInputStream dis = new DataInputStream(
socket.getInputStream());
// zapisz kolejno int, String i double
dos.writeInt(1000);
dos.writeUTF("Hello World!");
dos.writeDouble(3.14159);
// czytaj odpowiedz
String s = dis.readUTF();
// wypisz odpowiedz
System.out.println("Serwer powiedzial: "+s);
dis.close();
dos.close();
// koniec rozmowy
socket.close();
// moga byc wyjatki dot. gniazd,
// getByName, parseInt i strumieni
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Klient zakonczyl dzialanie");
}
}
Kompilacja:
javac c5p2a.java
javac c5p2b.java
Uruchomienie:
Konsola 1:
java c5p2a numer_portu
Konsola
2:
java c5p2b nazwa_hosta numer_portu
Przesyłanie
obiektów za pośrednictwem gniazd sieciowych
Zajmiemy
się teraz problemem przesyłania obiektów za pośrednictwem
gniazd sieciowych. Najwygodniejszym sposobem jest wykorzystanie
obiektów klas ObjectInputStream i ObjectOutputStream,
które posiadają specjalizowane metody writeObject
i readObject, służące odp. do zapisu
obiektów do strumienia i ich odczytu ze strumienia. Obiekty
wymienionych klas możemy, podobnie jak w przypadku DataInputStream
i DataOutputStream, "nadbudować" na obiektach
klas odp. InputStream i OutputStream,
które to obiekty możemy pobrać bezpośrednio z gniazda (za
pomocą Socket.getInputStream() i Socket.getOutputStream()).
Przykładowo, dla strumienia wyjściowego konstrukcja może wyglądać np.
tak:
ObjectOutputStream oos = new ObjectOutputStream(gniazdo.getOutputStream());
Istnieją
dwie fundamentalne zasady dotyczące przesyłania obiektów za
pomocą strumieni (dotyczą one również przesyłania ich za
pośrednictwem sieci):
- Przesyłane obiekty
muszą być serializowalne, co w praktyce oznacza, że
muszą implementować interfejs java.io.Serializable.
Jeśli projektowana przez nas klasa zawiera wyłącznie pola
typów serializowalnych, to wystarcza jawna deklaracja
implementacji wspomnianego interfejsu w nagłówku klasy. W
przeciwnym wypadku należy zaimplementować metody readObject
i writeObject aby poinstruować maszynę wirtualną
w jaki sposób obiekty naszej klasy zapisywać w strumieniu i
z niego odczytywać.
- Przesyłany jest wyłącznie
stan obiektów i informacja o ich klasie,
nie jest natomiast przesyłany sam bytecode klasy. W praktyce oznacza
to, że jeśli przesyłamy obiekt klasy X, to po
stronie czytającej (gdzie de facto następuje utworzenie nowego obiektu
i odtworzenie jego stanu na podstawie danych ze strumienia) musi być
dostępny bytecode klasy X, czyli plik X.class
musi być dostępny w ścieżce poszukiwań maszyny wirtualnej (jest to
pewien skrót myślowy - bytecode klasy nie musi być
koniecznie czytany z pliku, są inne metody jego uzyskania przez JVM,
jednak w przypadku typowym sprowadza się to do dostępności
odpowiedniego pliku w jednym z katalogów wskazanych w
zmiennej CLASSPATH).
Przykład
3
Rozważmy następujący problem. Chcielibyśmy zaimplementować
uniwersalny serwer obliczeniowy, który wykonywałby
dostarczone przez klientów zadania nie mając "świadomości" o
ich merytorycznej zawartości. Jedyne, co serwer powinien wiedzieć, to
że dostanie od klienta obiekt reprezentujący zadanie i inny obiekt,
reprezentujący parametry tego zadania. Serwer powinien wykonać
określoną metodę otrzymanego obiektu-zadania, która
zwróci wynik będący pewnym obiektem. Tak uzyskany wynik
serwer obliczeniowy powinien odesłać klientowi, który zlecił
wykonanie zadania. Zatem serwer obliczeniowy udostępnia moc
obliczeniową maszyny, na której działa, w celu wykonywania
dowolnych (różnej natury) zadań, tak długo, jak zadania te
są zgodne ze zdefiniowanym interfejsem.
Realizację
projektu zaczniemy od określenia interfejsu Zadanie,
który określał będzie funkcjonalność
obiektów-zadań. Od razu podamy, że rozszerza on java.io.Serializable,
ponieważ z założenia obiekty klas implementujących interfejs Zadanie
będą przesyłane za pośrednictwem sieci (a więc strumieni, a więc muszą
być serializowalne).
Plik Zadanie.java
pobierz
public interface Zadanie
extends java.io.Serializable {
public Object Wykonaj(Object params);
}
Intefejs
definiuje tylko jedną metodę - Wykonaj,
która przyjmuje pewien (zależny od zadania) obiekt jako
parametry i zwraca pewien (również zależny od zadania)
obiekt jako wynik.
Na potrzeby przykładu podamy dwie
implementacje interfejsu Zadanie (dwa istotnie
różne zadania): pierwsze z nich generuje liczbę pseudolosową
i zwraca jako wynik (nie potrzebuje żadnych parametrów),
drugie zaś sortuje podaną tablicę liczb całkowitych i zwraca jako wynik
posortowaną tablicę.
Plik ZLiczbaLosowa.java
pobierz
public class ZLiczbaLosowa
implements Zadanie {
public Object Wykonaj(Object params) {
System.out.println("*** Tu zadanie liczba losowa.");
return new Double(Math.random());
}
}
Plik
ZSortuj.java pobierz
public class ZSortuj
implements Zadanie {
public Object Wykonaj(Object params) {
int[] tablica = (int[]) params;
int i, j;
System.out.println("*** Tu zadanie sortowania.");
for (i=1; i<tablica.length; i++) {
for (j=0; j<tablica.length-1; j++) {
if (tablica[j]>tablica[j+1]) {
int tmp = tablica[j];
tablica[j] = tablica[j+1];
tablica[j+1] = tmp;
}
}
}
return tablica;
}
}
Możemy
teraz przystąpić do implementacji serwera obliczeniowego. Będzie on
działał wg następującego schematu:
- zainicjuj
gniazdo nasłuchujące TCP
- powtarzaj w nieskończoność
- czekaj
na połączenie od klienta
- czytaj ze strumienia
obiekt-zadanie
- czytaj ze strumienia obiekt-parametry
- uruchom
Wykonaj z otrzymanego zadania, podając metodzie otrzymane parametry
- wynik
Wykonaj odeślij (jako obiekt) klientowi
- zakończ
sesję z bieżącym klientem
Jak
widać schemat nie jest bardzo skomplikowany, co daje w efekcie dość
prosty kod serwera przedstawiony poniżej.
Plik SerwerObliczen.java pobierz
import java.net.*;
import java.io.*;
public class SerwerObliczen {
private int port;
private ServerSocket ss;
public SerwerObliczen(int aport) {
super();
port = aport;
ss = null;
}
public void InicjujGniazdo() {
try {
ss = new ServerSocket(port);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
public void WykonujZadania() {
Socket s;
ObjectInputStream ois;
ObjectOutputStream oos;
while (true) {
System.out.println("Czekam na zadanie...");
try {
s = ss.accept();
System.out.println("Polaczenie z "+
s.getInetAddress().getHostName());
System.out.println("Odczytywanie zadania ...");
oos = new ObjectOutputStream(s.getOutputStream());
ois = new ObjectInputStream(s.getInputStream());
Zadanie z = (Zadanie) ois.readObject();
System.out.println("Odebrano zadanie typu "+
z.getClass().getName());
System.out.println(
"Odczytywanie parametrow ...");
Object par = ois.readObject();
System.out.println(
"Parametry odczytane. Wykonuje zadanie ...");
Object wynik = z.Wykonaj(par);
System.out.println(
"Zadanie wykonane. Wysylam wyniki ...");
oos.writeObject(wynik);
System.out.println("Gotowe. Zamykam sesje z "+
s.getInetAddress().getHostName());
ois.close();
oos.close();
s.close();
} catch(Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
public static void main(String[] args) {
int port = 0;
BufferedReader klawiatura;
try {
klawiatura = new BufferedReader(
new InputStreamReader(
System.in
)
);
System.out.print("Podaj numer portu: ");
port = Integer.parseInt(
klawiatura.readLine()
);
} catch(Exception e) {
e.printStackTrace();
System.exit(1);
}
SerwerObliczen serwer = new SerwerObliczen(port);
serwer.InicjujGniazdo();
serwer.WykonujZadania();
}
}
Do
przetestowania naszego uniwersalnego serwera wykorzystamy prostego
klienta, który na życzenie użytkownika zleca serwerowi wykonanie
albo zadania ZLiczbaLosowa, albo zadania ZSortuj.
Jest oczywiste, że program kliencki nie może być w pełni uniwersalny,
ponieważ jego zadaniem jest m.in. przygotowanie parametrów
zadania i prezentacja wyników, zatem siłą rzeczy musi on być
związany z konkretnym rodzajem zadania.
Plik Klient.java pobierz
import java.net.*;
import java.io.*;
public class Klient {
private Socket s;
private InetAddress adresSerwera;
private int portSerwera;
private ZLiczbaLosowa zadanie1;
private ZSortuj zadanie2;
private int[] tablica;
private ObjectInputStream ois;
private ObjectOutputStream oos;
public Klient(InetAddress sa, int p) {
super();
adresSerwera = sa;
portSerwera = p;
tablica = new int[5];
for (int i = 0; i < 5; i++) {
tablica[i] = 5-i;
}
zadanie1 = new ZLiczbaLosowa();
zadanie2 = new ZSortuj();
}
public void OtworzPolaczenie() {
try {
System.out.println("Nawiazuje polaczenie ...");
s = new Socket(adresSerwera, portSerwera);
oos = new ObjectOutputStream(s.getOutputStream());
ois = new ObjectInputStream(s.getInputStream());
System.out.println("Polaczenie nawiazane.");
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
public void ZamknijPolaczenie() {
try {
System.out.println("Zamykam polaczenie ...");
oos.close();
ois.close();
s.close();
System.out.println("Polaczenie zamkniete.");
} catch(Exception e) {
e.printStackTrace();
System.exit(1);
}
}
public Object WykonajZadanieNaSerwerze(
Zadanie z, Object par
) {
Object wynik = null;
try {
oos.writeObject(z);
oos.writeObject(par);
wynik = ois.readObject();
} catch(Exception e) {
e.printStackTrace();
System.exit(1);
}
return wynik;
}
public void Menu() {
while (true) {
System.out.println();
System.out.println();
System.out.println(
"1. Wykonaj na serwerze zadanie 1 (liczba losowa)");
System.out.println(
"2. Wykonaj na serwerze zadanie 2 (sortowanie)");
System.out.println(
"3. Koniec");
try {
BufferedReader klawiatura = new BufferedReader(
new InputStreamReader(
System.in
)
);
System.out.print("Wybor: ");
int opcja = Integer.parseInt(
klawiatura.readLine());
if (opcja == 3) System.exit(0);
if (opcja == 1 || opcja == 2) {
OtworzPolaczenie();
if (opcja == 1) {
Double wynik = (Double)
WykonajZadanieNaSerwerze(
zadanie1,
new Double(0.0)
);
System.out.println(
"Wynik zadania 1: "+wynik.doubleValue());
} else {
int[] wynik = (int[])
WykonajZadanieNaSerwerze(
zadanie2,
tablica
);
System.out.print("Wynik zadania 2: [ ");
for (int i = 0; i < wynik.length; i++) {
System.out.print(wynik[i]+" ");
}
System.out.println("]");
}
ZamknijPolaczenie();
}
} catch (Exception e) {
System.out.println("NIE POWIODLO SIE");
}
}
}
public static void main(String[] args) {
String host;
int port;
try {
BufferedReader klawiatura = new BufferedReader(
new InputStreamReader(
System.in
)
);
System.out.print("Host serwera obliczen: ");
host = klawiatura.readLine();
System.out.print("Numer portu serwera obliczen: ");
port = Integer.parseInt(klawiatura.readLine());
Klient k = new Klient(
InetAddress.getByName(host),
port
);
k.Menu();
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
Kompilacja:
javac *.java
Uruchomienie:
Konsola 1:
java SerwerObliczen
Konsola 2:
java Klient