ZCD - Dettagli Tecnici

ZCD

La libreria di basso livello.

ZCD usa JsonGlib. Chi usa ZCD non necessita di saperlo. E' stata realizzata una testsuite che verifica di potersi linkare a ZCD senza conoscere JsonGlib.

ZCD viene "inizializzata" con alcune chiamate.

In seguito si può richiedere a ZCD di effettuare una chiamata a metodo remoto invocando questi metodi:

tcp_listen

Il metodo tcp_listen avvia una tasklet che ascolta e gestisce le connessioni TCP provenienti dall'esterno.

cosa riceve

I dati che vengono passati sono:

Il delegato per tcp_listen da usare alla ricezione di una connessione è nella forma di una implementazione dell'interfaccia IZcdTcpDelegate. Il suo metodo:

crea una istanza dell'interfaccia IZcdTcpRequestHandler apposita per gestire una connessione. I metodi di quest'ultima sono:

L'interfaccia IZcdDispatcher è implementata da un oggetto che verrà istanziato alla chiamata di get_dispatcher. I suoi metodi sono:

Il delegato per tcp_listen da usare in caso di errore nella listen o nella accept è nella forma di una implementazione dell'interfaccia IZcdTcpAcceptErrorHandler. I suoi metodi sono:

cosa fa la tasklet che gestisce il socket in ascolto

La tasklet avviata dal metodo tcp_listen apre un socket TCP e si mette in ascolto sulla porta e l'indirizzo specificati. In caso di errore nella listen lo passa al metodo error_handler.

Quando riceve una connessione chiama get_new_handler e passa l'istanza di IZcdTcpRequestHandler ad una nuova tasklet per gestire la connessione. In caso di errore nella accept lo passa al metodo error_handler.

quali messaggi passano attraverso una connessione TCP

Le richieste che arrivano da queste connessioni sono chiamate a metodi remoti, ognuna inclusa in un albero JSON.

Poiché sarebbe complicato (la libreria JsonGlib non lo supporta) leggere uno stream di bytes fino ad ottenere esattamente un valido albero JSON, si conviene che nelle comunicazioni in TCP tra end-points della libreria ZCD i primi 4 byte trasmessi indicano la lunghezza totale in bytes del successivo messaggio. Dopo il messaggio, se la connessione non viene chiusa (dal client) è possibile che venga trasmesso un successivo blocco di 4 byte e un successivo messaggio.

Se il messaggio ricevuto prevede una risposta, questa sarà trasmessa nella stessa connessione, sempre con la convenzione dei 4 byte di lunghezza. Si conviene quindi che, quando un client trasmette un messaggio che prevede una risposta, fino a che la risposta non è stata completamente ricevuta il client non usi la stessa connessione per trasmettere altri messaggi. Può, invece, aprirne immediatamente una nuova.

L'albero JSON di un messaggio di richiesta ha come radice un nodo OBJECT con 3 membri: "method-name", "arguments", "wait-reply". Il membro "method-name" è un nodo VALUE di tipo stringa, il membro "arguments" è un nodo ARRAY in cui ogni elemento è un valido nodo radice, cioè OBJECT o ARRAY . Il membro "wait-reply" è un nodo VALUE di tipo booleano.

L'albero JSON di un messaggio di risposta ha come radice un nodo OBJECT con 1 membro: "response". Il membro "response" è un valido nodo radice, cioè OBJECT o ARRAY .

cosa fa la tasklet che gestisce una connessione

La tasklet legge i 4 bytes che indicano la lunghezza del messaggio e poi legge il numero di bytes indicato. Se la connessione si chiude prima la tasklet termina.

Alla fine verifica di avere letto una stringa contenente un valido albero JSON. Tale JSON la libreria ZCD è in grado di interpretarlo, quindi la tasklet ne estrae le seguenti informazioni:

Se qualcuno di questi requisiti non è soddisfatto la tasklet chiude la connessione e termina.

Di seguito la tasklet:

tcp_client

Per ridurre l'overhead di una connessione TCP, un client può stabilire una connessione TCP con un server ed utilizzarla per diversi messaggi. Questo metodo restituisce un'istanza di TcpClient che rappresenta una connessione con il server indicato.

cosa riceve

Il metodo tcp_client riceve:

L'oggetto TcpClient che viene restituito memorizza queste informazioni. Inoltre esso incapsula un socket con il quale verrà stabilita la connessione. Ma la connessione non viene inizialmente aperta.

TcpClient

Ogni istanza di TcpClient lavora con il socket che incapsula. Tale oggetto può essere acceduto da diverse tasklet che lavorano in contemporanea, ma come abbiamo detto non è possibile usare la stessa connessione contemporaneamente per due operazioni. L'oggetto fornirà al suo utilizzatore questi metodi:

Di norma il client esegue queste operazioni. Per prima cosa crea una istanza di TcpClient con il metodo tcp_client di ZCD e la memorizza per usarla in futuro. Quando vuole effettuare una chiamata non urgente chiama direttamente il metodo enqueue_call sull'istanza che aveva memorizzata. Quando vuole effettuare una chiamata urgente chiama il metodo is_queue_empty e se è vuota chiama immediatamente il metodo enqueue_call ; se invece non è vuota crea una nuova istanza di TcpClient con il metodo tcp_client di ZCD, la sostituisce alla precedente e chiama su di essa il metodo enqueue_call .

Quando una operazione viene avviata, se la connessione non è aperta viene aperta. L'oggetto non può venire distrutto (ci saranno riferimenti ad esso) fino a quando ci sono operazioni accodate o in corso. Quando l'oggetto viene distrutto (rimossi tutti i riferimenti), se la connessione è aperta viene chiusa.

come procede una operazione

Se la connessione non era aperta il TcpClient si occupa di aprirla. Se si verifica un errore viene lanciata una eccezione ZCDError.

Il TcpClient costruisce un albero JSON il cui nodo radice è un OBJECT con i membri:

Dal JSON il TcpClient produce una stringa msg. Se durante la fase di costruzione di msg si verifica un errore la libreria può abortire il programma.

Il TcpClient trasmette la stringa msg alla connessione, preceduta da 4 bytes che ne indicano la lunghezza. Se si verifica un errore viene lanciata una eccezione ZCDError.

Se non si vuole attendere una risposta, il metodo restituisce immediatamente una stringa vuota.

Se bisogna attendere una risposta, il TcpClient la attende dalla stessa connessione, leggendola come sempre: prima 4 bytes e poi il numero di bytes riportati. Se si verifica un errore viene lanciata una eccezione ZCDError.

La stringa ricevuta deve essere un valido albero JSON. Altrimenti viene lanciata una eccezione ZCDError.

Il nodo radice deve essere un OBJECT con il membro response che è un nodo valido come radice. Altrimenti viene lanciata una eccezione ZCDError.

Il TcpClient costruisce un nuovo albero con tale nodo e genera una stringa da esso. Restituisce la stringa.

Se durante queste operazioni (cioè tra l'inizio della trasmissione del messaggio e la fine della ricezione della risposta) si verifica un errore che porta a lanciare una eccezione ZCDError, prima il socket viene scartato e sostituito con un nuovo socket costruito con gli stessi dati del precedente (indirizzo IP e porta TCP) non ancora connesso. Poi l'operazione in corso lancia l'eccezione ZCDError. Le altre operazioni, che eventualmente erano state accodate in precedenza, potranno essere avviate e di conseguenza una nuova connessione verrà tentata.

udp_listen

Il metodo udp_listen avvia una tasklet che ascolta e gestisce le connessioni UDP provenienti dall'esterno. I dati che vengono passati sono:

MOD-RPC

La libreria di livello intermedio può essere realizzata con il tool rpc-design partendo da una descrizione delle interfacce degli oggetti remoti. In questo caso la libreria che si ottiene avrà le caratteristiche che vengono ora descritte.

File di descrizione

La descrizione delle interfacce degli oggetti remoti viene preparata dallo sviluppatore in un formato apposito detto RPC-IDL in un file interfaces.rpcidl . Tale formato prevede la presenza di una classe radice, chiamata root-dispatcher . Questa ha un numero di membri che rappresentano ognuno un oggetto remoto che implementa un particolare modulo dell'applicazione. Ogni oggetto remoto ha un numero di metodi remoti.

Lato server

La libreria fornisce una interfaccia skeleton per la classe radice e una per ogni modulo membro.

La libreria fornisce inoltre una interfaccia per il delegato che essa stessa userà per reperire l'istanza della classe radice. Infine fornisce una interfaccia per il gestore di errori; questo sarà invocato nelle tasklet che stanno in ascolto sui socket in caso di errori che causano la terminazione delle stesse tasklet.

L'applicazione APP che voglia essere in grado di fare da server dovrà fornire una implementazione per ognuna di queste interfacce. L'istanza del delegato e quella del gestore di errori andranno passate alla libreria nella fase di inizializzazione, cioè, come vedremo dopo, quando si avviano le tasklet che stanno in ascolto sui socket.

La libreria, attraverso il delegato, sarà in grado, quando riceve una richiesta, di ottenere le istanze di classi radice e classi modulo al fine di chiamare un metodo remoto.

Facciamo un esempio. Il file interfaces.rpcidl presenta la classe radice "NodeManager node_manager". Ha un membro "InfoManager info_manager". Questo ha il metodo "string get_name() throws AccessDeniedError". La libreria prodotta da rpc-design fornirà queste interfacce:

Nota 1: Tutti i metodi remoti nell'interfaccia skeleton ricevono un argomento aggiuntivo ModRpc.CallerInfo che contiene alcune informazioni sul richiedente. Il contenuto varia a seconda del metodo usato per inviare il messaggio (Tcp, Unicast, Broadcast).

Nota 2: L'interfaccia IRpcDelegate ha un metodo per ogni root-dispatcher. Inizialmente supportiamo un solo root-dispatcher e quindi un solo metodo. Se ci fossero più root-dispatcher per ogni richiesta che si riceve lato server la prima parte del nome del metodo remoto starebbe ad indicare il root-dispatcher da usare; sarebbe compito della libreria MOD-RPC prendere questo prefisso e scegliere il metodo di IRpcDelegate da chiamare.

Quando si inizializza il MOD-RPC per fare da server, fra le altre cose, vengono avviate delle tasklet che stanno in ascolto su particolari messaggi dalla rete. Ci sono 2 tipologie di tasklet per i 3 tipi di messaggi che supporta ZCD.

Ad esempio si può avviare una tasklet che ascolta i messaggi TCP per qualsiasi indirizzo dell'host + una tasklet che ascolta i messaggi UDP sulla interfaccia "eth0" + una tasklet che ascolta i messaggi UDP sulla interfaccia "wlan0", e così via.

Le due tipologie di tasklet descritte sopra producono due diversi tipi di oggetti CallerInfo: la prima per ogni connessione produce una istanza di ModRpc.TcpCallerInfo, la seconda per ogni messaggio ricevuto produce o una istanza di ModRpc.UnicastCallerInfo oppure di ModRpc.BroadcastCallerInfo.

Per ognuna di queste tasklet in ascolto, l'applicazione APP ha inizialmente fornito un delegato dlg che è una istanza di ModRpc.IRpcDelegate e un gestore di errori err che è una istanza di ModRpc.IRpcErrorHandler.

I metodi che avviano queste tasklet sono:

Supponiamo ora che una di queste tasklet ha ricevuto un messaggio, composto dal nome del metodo e dai suoi argomenti. Inoltre ha composto il CallerInfo caller .

Continuando con l'esempio precedente, la libreria MOD-RPC chiama "INodeManagerSkeleton? root = dlg.get_node_manager(caller);". Questo metodo implementato dall'istanza fornita da APP decide in base a questo caller se restituire una certa istanza dell'interfaccia skeleton della classe radice.

Se riceve una istanza della classe radice, la libreria MOD-RPC ora ha il compito di esaminare il nome del metodo, individuare il percorso da fare (es: root.info_manager.get_name), deserializzare gli argomenti, chiamare il metodo, ottenere l'esito (il valore di ritorno o catchare le eccezioni) serializzarlo e restituirlo attraverso la rete al chiamante.

Se durante la deserializzazione degli argomenti si riscontra un problema (ad esempio non sono del tipo previsto dalla signature del metodo, oppure un oggetto è di una classe sconosciuta al programma, oppure un oggetto viene deserializzato ma non contiene tutte le proprietà requisito della sua classe) la libreria MOD-RPC non richiama affatto il metodo, invece serializza e restituisce una eccezione DeserializeError.

Le classi fornite da APP per implementare le interfacce skeleton, in particolare quelle dei singoli moduli, avranno quindi solo il compito di implementare la business logic dei singoli metodi remoti come fossero comuni metodi. Riceveranno i parametri previsti dalla signature, potranno elaborarli e restituire un valore di ritorno del tipo previsto dalla signature oppure lanciare una delle eccezioni previste dalla signature.

Lato client

Per l'applicazione APP che voglia essere in grado di fare da client, invece, la libreria MOD-RPC fornisce una interfaccia stub per la classe radice e una per ogni modulo membro. La libreria fornisce alcune funzioni all'applicazione per ottenere una istanza dello stub radice, una funzione per ogni modalità di invio del messaggio. Utilizzando il file interfaces.rpcidl riportato nell'esempio precedente le interfacce prodotte sono:

Le funzioni sono:

In particolare, la classe che si ottiene con la funzione get_node_manager_tcp_client implementa anche l'interfaccia ITcpClientRootStub. Tale interfaccia offre alcuni metodi che permettono ad APP di agire su particolari aspetti di una connessione TCP che non sono (o non è detto che siano) presenti quando i messaggi sono inviati con le altre modalità.

Alcuni metodi dell'interfaccia ITcpClientRootStub sono:

I metodi remoti che possono essere chiamati su uno stub prevedono tutti due possibili eccezioni oltre alle altre eventualmente descritte nel file interfaces.rpcidl. Una è StubError; può essere lanciata dalla libreria nel nodo locale per segnalare un errore nella fase di trasmissione del messaggio o ricezione della risposta. L'altra è DeserializeError; può essere trasmessa (in forma serializzata) come risposta dalla libreria nel nodo remoto se ha riscontrato problemi nel deserializzare gli argomenti che il nodo locale ha passato; oppure può essere lanciata dalla libreria nel nodo locale per segnalare un problema nel deserializzare il valore di ritorno.

Le classi stub fornite dalla libreria MOD-RPC hanno il compito di serializzare gli argomenti, inviare il messaggio attraverso la rete, ricevere la risposta (valore di ritorno o eccezione), deserializzarla e restituirla.

Serializzazione e deserializzazione di argomenti, valori di ritorno e eccezioni

Come detto nell'analisi funzionale, la libreria MOD-RPC ha il compito di:

La libreria MOD-RPC prodotta con il tool rpc-design si avvale della libreria JsonGlib per generare e leggere stringhe JSON.

Serializzazione di un argomento

La classe stub conosce la signature del metodo, quindi per ogni argomento ha il valore e il tipo atteso. Avendo ricevuto la chiamata come un normale metodo di una classe Vala, ha già fatto un controllo sulla validità degli argomenti.

Per gli argomenti del metodo remoto, ogni albero JSON ha per radice un oggetto con un singolo membro chiamato "argument". Il suo valore è un nodo JSON. Vediamo in dettaglio per ogni possibile situazione cosa contiene tale nodo.

Note sulla deserializzazione di un Object

Per deserializzare un oggetto con il metodo gobject_deserialize fornito dalla libreria JsonGlib occorre conoscere il tipo. Per questo ogni Object viene trasmesso sotto forma di un nodo OBJECT con i membri "typename" e "value".

Il membro "typename" è un nodo VALUE che contiene la stringa typename con cui ottenere il Type. La libreria MOD-RPC esegue "Type type = Type.from_name(typename);".

Il membro "value" è il vero node da passare alla deserializzazione. La libreria MOD-RPC esegue "GLib.Object obj = Json.gobject_deserialize(type, node);". Questa chiamata, potrebbe apparire strano, non prevede alcun tipo di eccezione.

Come detto in precedenza, rimandiamo alla documentazione di JsonGlib per dettagli su come questa deserializzazione avviene. Diciamo solo che la libreria istanzia il tipo indicato e cerca di valorizzare le property che riconosce sia nell'albero JSON sia nell'oggetto istanziato. Ma se ci sono properties nell'oggetto che non sono menzionate nel JSON semplicemente le lascia con il loro valore di default; e se ci sono membri nel JSON che non sono presenti nell'oggetto semplicemente li ignora. Per questo la libreria MOD-RPC fornisce, come misura di ulteriore verifica, una interfaccia ModRpc.Serializable.

Quindi, se il tipo type implementa l'interfaccia ModRpc.Serializable, allora la libreria richiama sull'oggetto appena deserializzato il metodo "bool check_serialization()" di tale interfaccia. In tale metodo l'oggetto verifica che tutte le property necessarie (dette property requisito) siano state correttamente valorizzate. Altrimenti (se ritorna false) la libreria MOD-RPC considera fallita la deserializzazione.

Deserializzazione di un argomento

La classe skeleton riceve la stringa JSON di un argomento dalla libreria ZCD. Essa ha già verificato che questa rappresenti un valido albero JSON. Se questo requisito non è soddisfatto è quindi lecito che la libreria MOD-RPC abortisca il programma.

Ogni argomento deve essere un nodo di tipo OBJECT con un membro chiamato "argument". Altrimenti verrà lanciato un DeserializeError.

La classe skeleton esamina il contenuto del nodo "argument":

Serializzazione del valore di ritorno

La classe skeleton conosce il tipo del valore di ritorno del metodo remoto. Se la chiamata del metodo non produce eccezioni, la classe skeleton si occupa di serializzare il risultato in un albero JSON e generare una stringa da esso.

Per un valore di ritorno del metodo remoto, ogni albero JSON ha per radice un oggetto con un singolo membro chiamato "return-value". Il suo valore è un nodo JSON realizzato in modo analogo a quanto abbiamo visto sulla serializzazione di un argomento. Nel caso di un metodo che restituisce void è un nodo NULL .

Serializzazione delle eccezioni

La classe skeleton conosce i tipi di eccezione previsti dal metodo remoto. Se la chiamata del metodo solleva una eccezione, la classe skeleton la gestisce e si occupa di serializzarla in un albero JSON e generare una stringa da esso.

Per una eccezione, ogni albero JSON ha per radice un oggetto con 3 membri di tipo stringa i cui nomi sono "error-domain", "error-code" e "error-message" e i cui valori sono le rappresentazioni stringa dei relativi valori della struttura di errore.

Deserializzazione del valore di ritorno

La classe stub conosce il tipo del valore di ritorno e i tipi di eccezione previsti dal metodo remoto.

La classe stub riceve la stringa JSON del valore di ritorno (risultato o eccezione) dalla libreria ZCD. Essa ha già verificato che questa rappresenti un valido albero JSON. Se questo requisito non è soddisfatto è quindi lecito che la libreria MOD-RPC abortisca il programma.

La radice deve essere un nodo di tipo OBJECT con un membro chiamato "return-value" oppure con i 3 membri "error-domain", "error-code" e "error-message". Altrimenti verrà lanciato un DeserializeError.

Se è "return-value", il suo valore è un nodo JSON che va interpretato in modo analogo a quanto abbiamo visto sulla deserializzazione di un argomento. Il valore di ritorno deserializzato verrà restituito dal metodo della classe stub.

Se è "error...", la classe stub:

Specifiche di trasmissione e ricezione

Modo TCP

Il metodo per ottenere uno stub "get_node_manager_tcp_client(...)" prevede questi parametri:

Lato server, alla ricezione di una connessione, viene valorizzato un oggetto TcpCallerInfo che contiene:

Modo Unicast

Il metodo per ottenere uno stub "get_node_manager_unicast(...)" prevede questi parametri:

Lato server, alla ricezione di un messaggio, viene valorizzato un oggetto UnicastCallerInfo che contiene:

Modo Broadcast

Il metodo per ottenere uno stub "get_node_manager_broadcast(...)" prevede questi parametri:

Lato server, alla ricezione di un messaggio, viene valorizzato un oggetto BroadcastCallerInfo che contiene:

APP

La applicazione di alto livello.