Spora przerwa podczas pisania bloga była spowodowana nawałem pracy (sporo projektów do ukończenia), nauki (matura…) i hobby (Day of Riot). W ferie udało mi się znaleźć odrobinę czasu aby ukończyć ten rozgrzebany poradnik w sposób (dla mnie) zadowalający.
Od czego zacząć
W poprzednim poście napisałem jak projektowo będzie wyglądać nasz serwer. Niestety, podczas tworzenia kodu nieco zmienił się także sam projekt, jednak główna idea działania pozostała niezmienna i testy pokazały, że jest to naprawdę solidnie zaprojektowany system (aczkolwiek jestem świadom że dużo jest w nim błędów).
Cały projekt (zalecam pracę w środowisku NetBeans) będzie można pobrać w całości poniżej, ja natomiast wymienię tutaj tylko kluczowe dla działania kawałki kodu.
Fundamenty
Cała implementacja serwera opiera się na jednej fundamentalnej klasie SocketDistributor, która zarządza komunikacją z klientami oraz przechowuje obiekty reprezentujące indywidualne połączenia z nimi.
Aby uruchomić nasłuchiwanie serwera wystarczy utworzyć obiekt tej klasy podając port jako parametr konstruktora (domyślny port to 6660 i w większości przypadków tak może pozostać). Przykładowy kod uruchamiania serwera z na porcie 6789:
SocketDistributor socketDistributor = new SocketDistributor(6789);
Kod ten jest asynchroniczny, zatem zaraz po utworzeniu obiektu serwer działa w tle jako osobny wątek.
Trochę o klasie SocketDistributor. Podczas tworzenia się obiektu klasa ta tworzy w tle wątek SocketDistributorThread, który obsługuje podłączających się klientów wywołując metodę addClient klasy SocketDistributor. Ta kluczowa funkcja wykonuje szereg istotnych operacji:
- Tworzy obiekt klienta, którego reprezentuje omówiona niżej klasa ClientObject. Za proces tworzenia tego obiektu odpowiada statyczna metoda createClientObject klasy ClientFactory
ClientObject clientObject = ClientFactory.createClientObject(client);
- Przypisuje obiektowi klienta metodę nasłuchującą dla przychodzących z klienta komunikatów. Odpowiada za to interfejs RecievedStringHandler który posiada metodę recievedString(String Text, ClientObject sender). W przykładowym kodzie obsłużyłem tę metodę wyświetlając przychodzący tekst na ekranie konsoli razem z ip wysyłającego klienta, jednak użytkownik może zaimplementować własną obsługę tej metody.
clientObject.setRecievedStringHandler(new RecievedStringHandler(){
@Override
public void recievedString(String Text, ClientObject sender) {
System.out.println("Message recieved from client " + sender.getSocketAdress() + " : " + Text);
}
});
- Dodaje obiekt klienta do wewnętrznej tablicy (co pozwala na dalsze zarządzanie klientami).
Takie przygotowanie klasy SocketDistributor pozwala na bardzo łatwą obsługę nawet dużej ilości podłączonych klientów. Klasa ta zawiera do tego takie funkcje jak:
- public ClientObject getClientByIP(String IP) – Zwraca obiekt klienta na podstawie podanego adresu ip. Na obiekcie takim można wykonać np. wysłanie komunikatu do klienta
- public void SendToAll(String Message) – Wysyła komunikat (podany jako parametr) do wszystkich aktualnie podłączonych klientów
- public void CloseAll() – Zakańcza wszystkie aktywne połączenia z klientami
Funkcje te to tylko przykład co można zrobić na liście podłączonych klientów. Każdy może zdefiniować sobie swoje własne metody, bazujące na potrzebach.
A co z klasą ClientObject?
No właśnie, działanie SocketDistributor ogranicza się do zarządzania inną kluczową klasą jaką jest ClientObject. Ma ona za zadanie obsługiwać strumienie przychodzące i wychodzące od/do klienta oraz przechowywać informacje na jego temat. Podobnie jak w klasie SocketDistributor konstruktor klasy ClientObject tworzy w tle wątek. Jednak zadaniem wątku ClientInputThread jest obsługa strumieni przychodzących od klienta. Działa to na bardzo prostej zasadzie nasłuchiwania na przyjście obiektu, odebrania go i wysłania odebranego tekstu do funkcji recievedString interfejsu RecievedStringHandler. Kod tej obsługi wygląda bardzo prosto:
@Override
public void run() {
try {
String stream;
while(true){
stream = in.readObject().toString();
if(stream.equals("CLOSECONNECTION")){
new ClientCloseThread().start(clientObject);
break;
}
clientObject.recievedFromClient(stream);
if(clientObject.isEnd()) break;
}
} catch (IOException ex) {
System.out.println("Wyjątek podczas obsługi strumieni przychodzących: " + ex.toString());
} catch (ClassNotFoundException ex) {
System.out.println("Wyjątek podczas obsługi strumieni przychodzących: " + ex.toString());
}
}
Nieskończona pętla oczekuje na obiekt wysłany przez klienta, następnie rzutuje go na typ String. Jeżeli wysłany został komunikat “CLOSECONNECTION” klient rozpoczyna zamykanie obiektu ClientObject. Tworzy w tym celu osobny wątek, co jest wymagane aby uniknąć zakleszczenia (kod wątku ClientCloseThread jest w pakiecie całości w załączniku). Jeśli komunikat jest inny niż zakańczający połączenie to wątek wysyła obsługę komunikatu przychodzącego z powrotem do obiektu ClientObject wywołując jego funkcję recievedFromClient(String Text). Następnie sprawdza czy klasa SocketDistributor nie ustawiła flagi końca połączenia.
Funkcja recievedFromClient Jedyne co robi to sprawdza czy został zaimplementowany interfejs RecievedStringHandler. Jeśli tak, to wywołuje jego funkcję recievedString.
Klasa ClientObject definiuje także kilka przydatnych funkcji:
- public void closeConnection() – zamyka połączenie z danym klientem
- public void sendToClient(String Text) – Wysyła komunikat (podany jako parametr) do klienta
Tutaj także pozostawiam wiele możliwości dla przyszłych implementacji w zależności od potrzeb programisty.
Podsumujmy
Jak widać system stworzony przeze mnie jest prosty i przejrzysty, a w 100% spełnia swoją funkcję komunikacji obustronnej. Zastosowań takiego systemu jest multum, od komunikatorów internetowych po skomplikowane systemy firmowe. Wszystko zależy od wyobraźni programisty.
Proszę was o wyrozumiałość jako iż jest to mój pierwszy poradnik tego typu. Proszę o zgłaszanie zauważonych błędów merytorycznych (oraz także projektowych – wciąż uczę się programowania zorientowanego obiektowo więc na pewno coś w tym projekcie da się zrobić lepiej :) ). Dzięki wam następny poradnik może być o wiele lepszy!
PS. Ostatnia część poradnika będzie przedstawiać implementacje bardzo prostego klienta do tego systemu, jednak zachęcam do próbowania napisania go samemu – w ramach ćwiczeń i zrozumienia działania tego projektu :)