Autor Wątek: [Java] Interpretowanie komunikatów sieciowych klient-serwer  (Przeczytany 2074 razy)

Offline Xenox93

  • Użytkownik

# Sierpień 09, 2016, 23:51:43
Witam,

mam problem bardziej z OOP i podejściem od strony implementacji rozwiązania problemu jakim jest interpretowanie komunikatów sieciowych, jakimi mogą być żądania i rezultat wcześniej wspomnianych żądań.

Nie od dziś wiadomo, że wysłanie zapytania do serwera czy istnieje konto użytkownika a czy dana sala jest zarezerwowana danego dnia różnią się niezbędnymi parametrami do wykonania w/w zapytania.

Jako rozwiązanie tego problemu postanowiłem użyć biblioteki GSON( upraszcza do minimum użycie JSON ), aby ustalić pewien wspólny standard wymiany komunikatów.

Standard komunikatów sieciowych:
{
  "service": "login",
  "content": {
    "login": "",
    "password": "",
    "error": ""
  }
}
service- usługa, czego dotyczy komunikat
content- zawartość komunikatu zależna od usługi




NetRequest - klasa abstrakcyjna wszystkich komunikatów( żądań/wyników )
public class NetRequest<T> {
   
    protected String service;
    protected T content;
   
    //==========================================================================
   
    public String getService() {
   
        return service;
    }
    public T getContent() {
   
        return content;
    }
   
    //==========================================================================
   
    @Override
    public String toString() {
       
        return new StringBuilder( "service:" ).append( service ).append( ",content:" ).append( content ).toString();
    }
}

a następnie klasa rozszerzająca powyższą klasę abstrakcyjną

public class LoginNetRequest extends NetRequest<Account> {
   
    public LoginNetRequest( Account account ) {
       
        service = "login";
        content = account;
    }
}

Jeszcze parę klas pośredniczących czyli:
Event - klasa abstrakcyjna wszystkich klas, które sprawdzają czy są w stanie obsłużyć dane żądanie, jeśli nie to wywołują kolejny obiekt
public abstract class Event
{
    private Event event = null;
    protected Client client;
   
    //==========================================================================
   
    public Event( Client client ) {
       
        this.client = client;
    }
   
    //==========================================================================
   
    public final Event add( Event event ) {
       
        this.event = event;
       
        return this.event;
    }
    public final Event get() {
       
        return event;
    }
   
    //==========================================================================
   
    public void forward( final NetRequest command ) throws Exception {
       
        if( event != null )
            event.handle( command );
    }
   
    //==========================================================================
   
    public abstract void handle( final NetRequest command ) throws Exception;
}

LoginEvent - klasa rozszerzająca klasę Event
public class LoginEvent extends Event
{
    public LoginEvent( Client client )
    {
        super( client );
    }
   
    //==========================================================================

    /**
     * @param command
     * @throws Exception
     */
    @Override
    public void handle( NetRequest command ) throws Exception {
       
        if( command.getService().equals( "login" ) ) {
           
            Account account = (Account)( command.getContent() );
            LoginNetRequest request = new LoginNetRequest( account );
           
            // Sprawdzenie w BD czy takie konto istnieje - LoginDBRequest !!!
            if( !account.getLogin().equals( "1" ) || !account.getPassword().equals( "1" ) )
                account.setError( "IncorrectLoginDataException" );
           
            client.sendMsg( request );
        }
        else
            forward( command );
    }
}

Interpreter - na końcu klasa, która trzyma wszystkie obiekty typu Event
public class Interpreter
{
    protected Event event;
   
    //==========================================================================
   
    public Interpreter( Client client ) {
       
        event = new LoginEvent( client );
    }
    public Interpreter( Interpreter interpreter ) {
       
        event = interpreter.getEvent();
    }
   
    //==========================================================================
   
    public Event getEvent() {
       
        return event;
    }
   
    //==========================================================================
   
    public void exec( final NetRequest command ) throws Exception {
        event.handle( command );
    }
}



Jak pewnie każdy już zauważył, użyłem typów generycznych oraz takich wzorców projektowych jak Composite oraz Chain of Responsibility. Były mi potrzebne, gdyż wszystkie dane GSON od razu zapisuje do/odczytuje z klasy Account.

Sami przyznacie, że jest to spore ułatwienie. Pisząc teraz zapytanie o rezerwację sali tak właściwie tworzę drugą klasę, która rozszerza w/w klasę abstrakcyjną ustawiając typ generyczny na RoomReservation. Na dodatek jak chcę obsłużyć komunikat, np. rezerwacji sal, to piszę nową klasę, która rozszerza klasę Event, a obiekt typu nowo utworzonej klasy dodaję do interpretera i to właściwie tyle.

Pewnie zapytacie, skoro działa to w czym tkwi problem. Otóż w samej metodzie, gdzie wiadomość jest pobierana a następnie konwertowana z łańcucha znaków na obiekt typu NetRequest - polimorfizm ^^

Najpierw spójrzcie na samą metodę:
private NetRequest getMsg() throws Exception {
   
    try {
       
        String request = in.readObject().toString();
       
        if( request.isEmpty() )
            throw new BlankCommandException();
           
        System.out.println( request );
       
        return new Gson().fromJson( request, LoginNetRequest.class ); <- chodzi o tę linijkę
       
    } catch( java.io.EOFException e ) {
       
        throw new BlankCommandException();
    }
}
Musi tam być przekazana klasa LoginNetRequest, gdyż jeżeli użyję NetRequest zapytanie nie zakończy się sukcesem. Patrząc na przykład wiadomo czemu, chodzi o tę część protected T content; z klasy NetRequest. Jak wiadomo mogą przyjść różne żądania, np. raz o zarezerwowaną salę a raz o wylogowanie, dlatego klasa NetRequest jest dla mnie zbawienna.




Możliwości jakie mi przychodzą przez kilka dni siedzenia nad problemem jest sporo, ale żadne mnie nie satysfakcjonują, gdyż tworzą nielogiczny kod, np. jeden z najlepszych to chyba ten:

Event
public abstract class Event
{
    protected Client client;
   
    //==========================================================================
   
    public Event( Client client ) {
       
        this.client = client;
    }
   
    //==========================================================================
   
    public abstract void exec( final NetRequest command ) throws Exception;
}

LoginEvent
public class LoginEvent extends Event {
   
    public LoginEvent( Client client )
    {
        super( client );
    }
   
    //==========================================================================

    /**
     * @param command
     * @throws Exception
     */
    @Override
    public void exec( NetRequest command ) throws Exception {
       
        Gson gson = new Gson();
       
        Account account = gson.fromJson( command.getContent(), Account.class );
           
        // Sprawdzenie w BD czy takie konto istnieje - LoginDBRequest !!!
        if( !account.getLogin().equals( "1" ) || !account.getPassword().equals( "1" ) )
            account.setError( "IncorrectLoginDataException" );
       
        client.sendMsg( new NetRequest( command.getService(), gson.toJson( account )  ) );
    }
}

NetRequest
public class NetRequest {
   
    protected String service;
    protected String content;
   
    //==========================================================================
   
    public NetRequest( String service, String content ) {
       
        this.service = service;
        this.content = content;
    }
   
    //==========================================================================
   
    public String getService() {
   
        return service;
    }
    public String getContent() {
   
        return content;
    }
   
    //==========================================================================
   
    @Override
    public String toString() {
       
        return new StringBuilder( "service:" ).append( service ).append( ",content:" ).append( content ).toString();
    }
}

Interpreter
public class Interpreter
{
    private final Map< String, Event > commands = new HashMap<>();
           
    //==========================================================================
           
    public Interpreter( Client client ) {
       
        commands.put( "login", new LoginEvent( client ) );
    }
   
    //==========================================================================
   
    public void exec( NetRequest command ) throws Exception {
       
        if( !commands.containsKey( command.getService() ) )
            return;
       
        commands.get( command.getService() ).exec( command );
    }
}

Prywatna metoda do pobierania komunikatów przez serwer
private NetRequest getMsg() throws Exception {
       
        try {
           
            String request = in.readObject().toString();
       
            if( request.isEmpty() )
                throw new BlankCommandException();

            System.out.println( request );

            JSONObject obj = new JSONObject( request );
       
            String service = obj.getString( "service" );
            String content = obj.getString( "content" );

            return new NetRequest( service, content );
       
        } catch( java.io.EOFException e ) {
           
            throw new BlankCommandException();
        }
    }

Klasa NetRequest zamiast przyjmować typ generyczny będzie ustawiona na sztywno jako String, czyli protected String content;

I dopiero, gdy konkretny obiekt typu Event jest w stanie obsłużyć dane żądanie, mógłby za pomocą GSON tworzyć klasę Account.

Tylko jak każdy z Was zauważył co najmniej dwa razy korzystam z GSON i raz na samym początku z czystego JSON. Jeżeli jest to jedyne rozsądne rozwiązanie to będę musiał iść w tę stronę. Korzyści też całkiem sporo, unikam pisania każdej klasy to każdego możliwego żądania ^^

Jeżeli okaże się że ostateczne rozwiązanie jest chyba najlepsze to tak zrobię. W przeciwnym przypadku przynajmniej czegoś się nauczę od doświadczonych programistów :]

PS. Jak na razie zastosowałem najprostsze rozwiązanie i wszystko działa jak należy. Widzę nawet pewne zalety. Jednakże jeśli ktoś będzie miał lepszy pomysł z chęcią wysłucham :)
« Ostatnia zmiana: Sierpień 10, 2016, 01:47:31 wysłana przez Xenox93 »

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Xion

  • Moderator
    • xion.log

  • +2
# Sierpień 10, 2016, 09:34:08
Cytuj
Jak pewnie każdy już zauważył, użyłem typów generycznych oraz takich wzorców projektowych jak Composite oraz Chain of Responsibility.

Cytuj
PS. Jak na razie zastosowałem najprostsze rozwiązanie i wszystko działa jak należy. Widzę nawet pewne zalety. Jednakże jeśli ktoś będzie miał lepszy pomysł z chęcią wysłucham :)
Mój pomysł jest taki, że nawet ta "uproszczona" wersja jest ze trzy razy przekombinowana. To czego potrzebujesz to kawałek jakiegoś prostego frameworka HTTP żeby zakodować parę REST-owych handlerów typu /login czy co tam potrzebujesz. W środku możesz co najwyżej zdefiniować POJO typu:
class LoginRequest {
   public String login;
   public String password;
}
i jego używać do deserializacji przychodzących requestów z Gson.fromJson.

Offline Xenox93

  • Użytkownik

# Sierpień 10, 2016, 15:24:15
W środku możesz co najwyżej zdefiniować POJO typu:
class LoginRequest {
   public String login;
   public String password;
}
i jego używać do deserializacji przychodzących requestów z Gson.fromJson.
Takie coś właśnie użyłem i mam problem przy deserializacji, bo nie wiem na samym początku jakie dane zawiera, więc nie wiem jaką klasę podać, np.: LoginRequest, RoomReservationRequest itd. itd. Czemu? Wiadomo, że serwer może odebrać każdy rodzaj żądania w danej chwili. Co innego, gdybym miał do czynienia z aplikacją, która każde żądanie wysyła step-by-step, czyli kolejność żądań jest znana z góry ^^

Tak więc sporo tego już jest, no i nie jest to zgodne z zasadą DRY, czyli posiadaniem dwóch dokładnie tych samych danych w dwóch klasach, gdzie druga mogłaby korzystać z obiektu typu tej pierwszej klasy, np. klasa LoginEvent zawiera obiekt typu klasy Account - agregacja. Tak więc wcześniej wymieniona zasada jest jak najbardziej spełniona. Przykład podany przez Ciebie ignoruje tę zasadę oraz zaletę stosowania OOP ;)

Reasumując, takie coś już miałem, choć jak to już nazwałeś, było przekombinowane, przyznaję się.

Co do HTML HTTP to jeszcze nie korzystałem z tego protokołu do wymiany danych poprzez sieć. No i muszę się przyznać, że nie wiem za bardzo jak miałoby to wyglądać, gdyż zawsze kojarzy mi się ze stronami WWW.
Ofc, następny projekt, który będę pisał, aby zrozumieć język Java wraz z resztą technologii będzie opierał się na wymianie danych z użyciem znanych protokołów ^^

Teraz po uproszczeniu już nie korzystam z tak rozbudowanej funkcjonalności. Mam chyba wszystko czego potrzebuję. Tak przynajmniej wydaje mi się, że znalazłem pewien kompromis między lepszym kodem, a tym czego oczekuję na tyle na ile pozwala mi mój poziom wiedzy.
« Ostatnia zmiana: Sierpień 10, 2016, 15:40:33 wysłana przez Xenox93 »

Offline bies

  • Użytkownik

  • +1
# Sierpień 10, 2016, 15:31:23
Takie coś właśnie użyłem i mam problem przy deserializacji, bo nie wiem na samym początku jakie dane zawiera, więc nie wiem jaką klasę podać, np.: LoginRequest, RoomReservationRequest itd. itd.
Dlatego Xion pisał o protokole HTTP (nie HTML). Elementem HTTP jest nagłówek który zawiera np: POST /login lub POST /roomreservation. I to na podstawie tego nagłówka wiesz jaka jest struktura pytania/odpowiedzi. Jeśli nie chcesz używać HTTP (lub innego, ustalonego protokołu -- ale HTTP jest bardzo dobry do takich zastosowań) to musisz zaprogramować swój. Gdzie też będzie jakiś nagłówek oraz dane. Sądząc po tym co piszesz próbujesz oprogramować protokół komunikacji tylko z danymi (na dodatek OOP, WTF!) -- to tak nie działa. Zawsze: nagłówek (który pozwoli zinterpretować dane) oraz dane.

Offline Xenox93

  • Użytkownik

# Sierpień 10, 2016, 15:39:59
Przepraszam, mój błąd, chodziło mi o HTTP.

Sądząc po tym co piszesz próbujesz oprogramować protokół komunikacji tylko z danymi (na dodatek OOP, WTF!) -- to tak nie działa. Zawsze: nagłówek (który pozwoli zinterpretować dane) oraz dane.
W sumie częściowo udało mi się to uzyskać. Tzn, mam jedną wspólną klasę dla wszystkich oraz pewnego rodzaju parser, który rozróżnia żądanie i odpowiednio interpretuje dane. Gdyby tak nie było, to mój kod wyglądałby mniej więcej tak, że jedna klasa, która ma n instrukcji warunkowych wraz z wykonaniem pewnych operacji ;)

Sądząc po tym co napisałeś, stwierdzam, że podszedłem do sprawy wymyślając koło na nowo. Bo moje założenie jest identyczne z protokołem HTTP, tzn. też mam najpierw usługę( w HTTP jest to nagłówek ) a później dane.
Jednym słowem zamiast korzystać z biblioteki do parsowania HTTP, korzystam z JSON,a robią coś podobnego.
Własnie o taką zaletę OOP mi chodzi - polimorfizm i nie tylko, pozwala na utworzenie pewnego modułu( interfejs/klasa abstrakcyjna ), którego działanie mogę zmienić pisząc nową klasę rozszerzającą w/w moduł.

Dzięki za pomoc. Coś mi się wydaje, że będę musiał poczytać jeszcze o zastosowaniu w/w protokołu.
« Ostatnia zmiana: Sierpień 10, 2016, 15:44:17 wysłana przez Xenox93 »

Offline matheavyk

  • Użytkownik
    • rabagames.com

  • +1
# Sierpień 13, 2016, 03:06:24
Czytałem Twój post 3 dni temu; trochę już go zapomniałem, ale może operator podobny do "is" albo "as" w C# Ci się przyda?
Tutaj opis operatora "is" w C#: https://msdn.microsoft.com/en-us/library/scekt9xw.aspx
Tutaj jak w javie zrobić sobie "as": http://stackoverflow.com/questions/148828/how-to-emulate-c-sharp-as-operator-in-java

Offline Avaj

  • Użytkownik

  • +1
# Sierpień 13, 2016, 19:25:51
Może jeśli już używać JSONa to korzystać z istniejących standardów, do których są tony bibliotek? http://www.jsonrpc.org/specification

Offline Xenox93

  • Użytkownik

  • +1
# Sierpień 14, 2016, 16:55:14
@matheavyk: dzięki za poszerzenie horyzontów. Nawet nie wiedziałem, że taki operator istnieje w C# oraz, że można go zaimplementować/emulować w Java. Niestety, jedyne zastosowanie jakie w nim widzę, to program działający lokalnie, tzn. bez wymiany komunikatów przez sieć. Również jest podobny do operatora w języku Java - instanceof, który może nie jest obiektowy, ale pozwala na to samo co operator "is"/"as".

@Avaj: Hmmm, jest to dosyć podobne do tego co już zaimplementowałem i działa. Biblioteka, która jest jakby wrapperem biblioteki JSON a którą używam - GSON. Nie ma sensu, zmieniać jej, gdyż na obecną chwilę spełnia swoją rolę oraz nie wymaga pisania n razy deserializacji dla każdego nowego typu złożonego( klasy ). Co ma miejsce w przypadku JSON, gdzie każdą zmienną i jej wartość musimy pobrać w różny sposób, niż od razu za pomocą jednego polecenia wypełnić obiekt danymi.

BTW, problem już został rozwiązany zaraz po dodaniu problemu na forum. Jednakże myślałem, że jest jakiś bardziej elegantszy sposób( smart ), który mógłbym zaimplementować.

Problem był taki, że program nie mógł wiedzieć z góry, z jakim rodzajem wiadomości będzie miał do czynienia, a tym samym jak odpowiednio pobrać dane, a następnie wypełnić nimi dany obiekt.
Wydaje mi się że chciałem (nieświadomie) rozwiązać problem natury teorii obliczalności xD Gdyż mój interpreter tak jak automat "niedeterministyczny" mógł przyjąć dowolny stan/typ wiadomości, czy to dane logowania czy dane rezerwacji sali w dowolnym momencie, które należy odpowiednio zinterpretować. Z interpretacją nie miałem problemu, ale uprzednio musiałem wiedzieć jaka to wiadomość( nagłówek/service ) i tu był mój problem. Chciałem w magiczny sposób, żeby biblioteka/GSON/JSON pobrał ten nagłówek i sam wybrał jak interpretować dane, mając do dyspozycji różne klasy. W sumie udało mi się to, ale w trochę mniej zoptymalizowany sposób ^^
Problem był w tym, że nie da się przewidzieć struktury( chyba najodpowiedniejszy wyraz ) danych zawartych w wiadomości, niezależnie czy to HTTP, JSON (RPC) czy jakiś inny protokół.

Jednakże w informatyce ważniejsze jest rozwiązanie problemu niż utopijna perfekcja prezentowanego kodu, który sam wie, kiedy i jak ma się wykonać - panaceum ;)

Reasumując, dowiedziałem się że powinienem wykorzystać najprostszy protokół HTTP/HTTPS, niż wymyślać swój własny lub teraz zastępować wieloma innymi, które robią dokładnie to samo i prezentują ten sam poziom pod względem jakości kodu. Co do reszty to raczej jest w porządku, bo jest uniwersalnie napisana i w większości przypadków wykorzystywana w ten sam sposób.
« Ostatnia zmiana: Sierpień 14, 2016, 16:57:26 wysłana przez Xenox93 »