Capita molto spesso di trovarsi di fronte al problema di dover far cooperare differenti programmi tra di loro. In passato lo scambio di informazioni avveniva utilizzando sistemi di comunicazione fra processi abbastanza complessi, basati su architetture client-server di pregevole ma intricata fattura.
In tempi più recenti lo sviluppo del web e l'affermazione di alcune tecnologie hanno portato alla standardizzazione di alcuni processi di comunicazione, facilitando lo sviluppo di semplici e compatti protocolli per lo scambio delle informazioni. Basti pensare ai webservice, strumenti ormai affermatisi nel panorama dello sviluppo web (e non solo ...).
XML-RPC è uno di questi standard, pensato appositamente per fornire un semplice formato per la chiamata di procedure remote. Tramite XML-RPC è possibile richiamare funzioni definite in altri programmi, siti web o librerie, sempre che le parti cooperanti abbiano a disposizione un layer di funzioni capace di convertire le chiamate in pacchetti XML-RPC corretti. Questo standard effettua lo scambio di informazioni basandosi sul protocollo HTTP e sfruttando XML al fine di definire in modo organizzato le informazioni necessarie al corretto funzionamento delle procedure.
In questo articolo ci occuperemo di descrivere la struttura di XML-RPC e nel successivo forniremo un'implementazione di server e client utilizzando PHP4. Consiglio caldamente a chiunque sia intenzionato a proseguire la lettura di essere sicuro di aver compreso correttamente il funzionamento del parser SAX di PHP e la programmazione ad oggetti.
Una richiesta XML-RPC
Come accennato precedentemente, il protocollo XML-RPC si appoggia ad HTTP
per effettuare lo scambio di informazioni tra le parti in gioco. Il
client si connette al server, invia i dati necessari codificandoli
nel formato XML corretto e riceve una risposta XML dal server
reppresentante i dati richiesti o un errore, nel caso qualcosa sia
andato storto.
Viene ora presentata una semplice richiesta XML-RPC dove verrà
richiamata una funzione presente sul server:
POST /path/to/my/server.php HTTP/1.0
User-Agent: myUserAgent
Host: hosto.example.com
Content-Type: text/xml
Content-length: 167
<?xml version="1.0"?>
<methodCall>
<methodName>myFunction</methodName>
<params>
<param>
<value>
<i4>100</i4>
</value>
</param>
</params>
</methodCall>
Questa
chiamata richiede al server il risultato della funzione myFunction
richiamata con un parametro intero di valore 100.
Dalla
riga 1 alla riga 5 abbiamo gli header HTTP necessari per
ottenere una risposta corretta dal server:
- Riga 1: specifica che lo scambio dei dati deve avvenire utilizzando
POST, accendendo all'URI specificato come secondo parametro
(/path/to/my/server.php). Informa il server web che ospita il server
XML-RPC di gestire gli header utilizzando le specifiche HTTP 1.0; - Riga 2: specifica lo user agent utilizzato. È possibile utilizzare una stringa qualsiasi che descriva in qualche modo il client che effettua la richiesta in modo che il server sappia come comportarsi in caso di restrizioni particolari;
- Riga 3: specifica l'host al quale connettersi;
- Riga 4: specifica il tipo di contenuto della chiamato. Questo parametro deve essere sempre text/xml;
- Riga 5: specifica la lunghezza in byte della chiamata.
Gli
header specificati nella richiesta di esempio sono obbligatori e
devono essere corretti, al fine di assicurare che ogni server
interpreti senza errori le informazioni ricevute.
Riportiamo per comodità il codice della richiesta pubblicato alla pagina precedente:
POST /path/to/my/server.php HTTP/1.0
User-Agent: myUserAgent
Host: hosto.example.com
Content-Type: text/xml
Content-length: 167
<?xml version="1.0"?>
<methodCall>
<methodName>myFunction</methodName>
<params>
<param>
<value>
<i4>100</i4>
</value>
</param>
</params>
</methodCall>
Dalla
riga 7 in avanti è specificata la chiamata a funzione
codificata. Il nodo di root, che deve obbligatoriamente chiamarsi
methodCall, deve contenere un nodo figlio chiamato methodName
avente come valore una stringa rappresentante la funzione che si
desidera invocare sul server. In caso la funzione da richiamare
accetti dei parametri, il nodo params si occuperà di
contenereli codificati all'interno di nodi param. La codifica
dei dati XML-RPC permette di condividere tra server e client un buon
numero di tipi di dato differenti, e fornisce quindi un buon livello
di interoperabilità tra diversi linguaggi di programmazione.
Ogni
nodo di tipo param contiene un nodo di tipo value che contiene
uno o più nodi rappresentanti il tipo e contenenti il valore
del dato.
I
nodi figli di value possono rappresentare sia tipi di dato scalari
che tipi di dato gerarchici.
Per
quanto riguarda i tipi di dato scalari, abbiamo i seguenti nodi:
- <i4>
o <int>: rappresenta un intero a 4 bit; - <string>:
rappresenta una stringa; - <base64>:
dato di tipo binario codificato in base 64; - <dateTime.iso8601>:
contiene informazioni su data e tempo, specificate nel formato
rappresentato dal seguente esempio: 20041030T23:57:00; - <boolean>:
dato di tipo booleano, che può essere 1 (true) o 0 (false); - <double>:
numero a virgola mobile a precisione doppia;
In
caso un nodo value non abbia specificato alcun tipo, verrà
interpretato dal server come stringa.
I
tipi di dato gerarchici supportati da XML-RPC sono le strutture
e gli array.
Gli
array sono definiti come segue:
<array>
<data>
<value>
<int>10</int>
</value>
<value>
<string>ciao a tutti</string>
</value>
<value>
<array>
<data>
<value>
<string>array multidimensionale ...</string>
</value>
</data>
</array>
</value>
</data>
</array>
Il
nodo data può contenere un numero qualsiasi di nodi
value aventi come tipo di dato uno qualsiasi di quelli
supportati da XML-RPC. È quindi possibile anche definire array
multidimensionali, seguendo le stesse regole descritte
precedentemente.
Le
strutture sono dei tipi di dato gerarchici che permettono di accedere
ai propri membri specificando delle chiavi di tipo stringa anziché
degli indici numerici: in PHP possiamo intendere una struttura come
un array avente solamente chiavi di tipo stringa.
XML-RPC
supporta la strutture attraverso la seguente sintassi:
<struct>
<member>
<Name>nomeChiave</Name>
<value>
<string>Valore chiave</string>
</value>
</member>
<member>
<Name>chiave2</Name>
<value>
<int>2013</int>
</value>
</member>
</struct>
Come
possiamo notare una struttura è formata da un elenco di
membri, ognuno contenente un nome in cui va specificata una stringa
utilizzata come chiave ed un valore che può essere qualsiasi
tipo supportato da XML-RPC, quindi anche un array o una strutture.
Una risposta in XML-RPC
Il
server che riceve la richiesta HTTP correttamente formattata, si
occupa di decodificarla ed eseguire le operazioni richieste, per poi
restituire al client il risultato dell'operazione eseguita. Una
risposta XML-RPC ha il seguente formato:
HTTP/1.1 200 OK
Connection: close
Content-Length: 140
Content-Type: text/xml
Date: Wen, 14 Jul 2004 22:10:00 GMT
Server: MyServer
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value>
<struct>
<member>
<Name>matricola</Name>
<value>
<int>642194</int>
</value>
</member>
<member>
<Name>nome</Name>
<value>
<string>Gabriele Farina</string>
</value>
</member>
<member>
<Name>email</Name>
<value>
<string>g.farina@html.it</string>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodResponse>
Una
risposta del server deve contenere i corretti header, specificati in
questo esempio dalla linea 1 alla linea 6. Le altre linee contengono
codice XML rappresentante il valore restituito dalla funzione
richiamata sul server. Il codice XML deve contenere un nodo root
chiamato methodResponse, contenente a sua volta un unico
figlio di nome params avente un unico parametro che può
avere un valore specificato seguendo le regole si codifica dei dati
descritte nella sezione precedente.
In
caso di errore, XML-RPC supporta un figlio alternativo per il nodo
methodResponse, chiamato nodo fault. In caso sia presente un
nodo con questo nome, non può essere specificato alcun nodo
params, ed il client riuscirà a capire che qualcosa è
andato storto.
HTTP/1.1 200 OK
Connection: close
Content-Length: 426
Content-Type: text/xml
Date: Wen, 14 Jul 2004 22:10:00 GMT
Server: MyServer
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<Name>faultCode</Name>
<value>
<int>4</int>
</value>
</member>
<member>
<Name>faultString</Name>
<value>
<string>Too many parameters.</string>
</value>
</member>
</struct>
</value>
</fault>
</methodResponse>
La
struttura contenuta all'interno del valore del nodo fault si compone
di due membri obbligatori: uno chiamato faultCode che ha come
valore un intero rappresentante il numero dell'errore, ed uno
chiamato faultString che ha come valore una stringa
informativa. È opera del client comprendere di che tipo di
errore si tratta e comportarsi di conseguenza.
XML-RPC in pratica
Ora ci occuperemo
dell'implementazione di un server e di un client XML-RPC utilizzando
una libreria che si appoggia solamente su PHP, senza utilizzo di
estensioni aggiuntive che spesso non sono installate su macchine di
molti hosters.
La libreria che ho deciso di utilizzare ed analizzare in questo articolo è phpRPC, un'insieme di script repreribili su Sourceforge (http://sourceforge.net/projects/phprpc/): phpRPC si presenta con una buona interfaccia ed un'implementazione molto semplice che permette di comprendere come costruire ed inviare correttamente una chiamata XML-RPC tramite PHP. Consiglio a chiunque sia intenzionato ad approfondire l'argomento, di cercare di seguire il flusso logico di costruzione di una chiamata eseguita tramite phpRPC, in modo da poter rendersi conto di come sia possibile implementare un'eventuale libreria alternativa più adatta ai Propri bisogni.
Dopo
aver scaricato la libreria da Sourceforge o dal sito internet del suo
sviluppatore
(http://www.e-xoops.com/public/modules/news/index.php?storytopic=2),
scompattatela all'interno di una cartella nella root del vostro
webserver chiamata phpRPC. I file che ci interessano sono
quelli contenuti all'interno della cartella include e della cartella
library; nella cartella include sono contenuti i file che definiscono
le classi e le funzioni utilizzabili per implementare un server ed un
client XML-RPC; la cartella library contiene dei file che servono per
definire i metodi esportati dal server (definito all'interno del file
server.php contenuto nella root.)
L'approccio che seguirò in questo articolo sarà quello di analizzare insieme come è stata definita la libreria ed i file di esempio in essa contenuti.
Definizione del server
Il server di esempio, che può essere tranquillamente utilizzato in qualsiasi applicazione necessiti di un server XML-RPC semplice e veloce, è contenuto definito come segue:
<?php
require("./cache/.config.php");
require("./include/rpc_decoder.php");
require("./include/rpc_encoder.php");
class rpc_Server {
var $library;
function rpc_Server() {
global $config;
$this->library = $config["library"];
$this->decode_request();
}
function decode_request() {
global $HTTP_RAW_POST_DATA, $rpc_decoder;
$params = $rpc_decoder->decode($HTTP_RAW_POST_DATA);
$mfile = $this->library.str_replace('.', '/',
$rpc_decoder->methodName).'.php';
if (file_exists($mfile)) {
$rpc_encoder = new rpc_Encoder();
require_once($mfile);
} else
trigger_error('Could not find method: '.
$rpc_decoder->methodName, E_USER_ERROR);
if ($php_errormsg == '')
trigger_error('Please enable track_errors in server php.ini',
E_USER_ERROR);
else
trigger_error($php_errormsg, E_USER_ERROR);
}
}
$rpc_server = new rpc_Server();
?>
Procediamo con l'analisi del codice:
- le prime righe si occupano di includere le librerie necessarie per il corretto funzionamento del server: la libreria rpc_decoder server per trasformare i dati XML-RPC in formato nativo PHP, al fine di semplificare le operazioni di controllo dei dati e di chiamata dei metodi; la libreria rpc_encoder si occupa di definire metodologie per trasformare i dati da formato nativo a formato XML-RPC;
- il costruttore della classe si occupa di inizializzare la proprietà library e di richiamare il metodo decode_request;
- il metodo decode_request effettua la chiamata al metodo richiesto e ne restituisce il risultato o un eventuale errore. Per fare questo decodifica i dati passati via POST alla pagina, include il file contenente la definizione del metodo (uno di quelli contenuti nella cartella library) e restituisce degli errori in caso qualcosa vada storto. L'analisi dei parametri e la codifica dei dati restituiti è lasciata al codice definito nel file incluso.
Struttura della libreria
Il server qui presentato si appoggia, come già accennato precedentemente, ale cllassi definite all'interno dei file rpc_decoder.php e rpc_encoder.php, che andremo ora ad analizzare.
Il primo file contiene la definizione della classe rpc_Decoder che si occupa della decodifica dei dati. Analizziamo i metodi più interessanti singolarmente:
- decode:
si occupa di eseguire il parsing dei dati XML passatigli come
parametro richiamando rispettivamente i metodi tag_open,
tag_close e cdata in caso il parser incontri un tag di
apertura, un tag di chiusura o del testo. A parsing concluso esegue
la funzione eval sul codice PHP bufferizzato all'interno della
proprietà code, e restituisce i parametri così
decodificati; - tag_open:
questo metodo è richiamato ogni volta che SAX trova un
tag di apertura; il metodo bufferizza all'interno della proprietà
code una stringa rappresentante del codice PHP che definisce i dati
trovati all'interno della struttura XML. PhpRPC supporta qualche
tipo di dato aggiuntivo, facilmente comprensibile guardando il
codice sorgente. - tag_close:
questo metodo è richiamato ogni volta che SAX trova un tag di
chiusura, e si occupa di terminare la scrittura del codice
necessario per la creazione del dato corrente aggiungendo al buffer
le stringhe necessarie. Per ogni operazione seguita all'apertura di
un tag, ne corrisponde una alla chiusura. - cdata:
questo metodo è richiamato ogni volta che SAX trova del
testo che non corrisponde a nessuna struttura speciale XML. Anche in
questo caso il testo è analizzato ed aggiunto al buffer
facendone le dovute correzioni e trasformazioni.
Il secondo file, invece contiene la definizione di alcune funzioni di utilità e della class rpc_Encoder, che si occupa della codifica dei dati. Analizziamo insieme i metodi più interessanti:
- i
metodi start_* ed end_* si occupano di inserire
all'interno dell'array rappresentante i parametri correnti delle
stringhe che rappresentano tag XML per identificare un tipo di dato
specifico. Per esempio start_struct si occupa di aggiungere
all'array i tag di apertura <value><struct>, mentre
end_struct si occupa di aggiungere i tag di chiusura
corrispondenti. I metodi sono utilizzati solamente per i tipi di
dato complessi, in modo da permettere la gestione di dati nificati
con una metodologia semplice e compatta. - add_value:
si occupa di aggiungere ai parametri correnti il valore specificato
dal parametro $value, dopo averlo codificato in base al tipo
specificato dal parametro $type. Il terzo parametro del
metodo è utilizzato in caso di chiamata ricorsiva (per la
costruzione di una struct, per esempio) al fine di identificare il
nome del membro corrente.
In base al tipo di dato specificato, il
metodo si occupa di estrarre le informazioni necessarie da $value,
codificarle e salvarle all'interno della lista dei parametri
correnti; anche il processo di codifica di parametri complessi è
molto semplice e si basa sulla chiamata ricorsiva dei metodi add_*
per la costruzione di strutture annidiate. Da notare che phpRPC non
sembra codificare correttamente array a chiave numerica contenenti
array a chiave mista. - get_type:
restituisce il tipo di dato del valore passato come argomento
basandosi sul'utilizzo della funzione gattype di PHP e di altre
funzioni ausiliarie necessarie per poter interpretare correttamente
i dati. - return_call:
di occupa di restituire una stringa rappresentante una chiamata o
una risposta in formato XML-RPC contenente tutte le informazioni
necessarie correttamente formattate.
Costruzione di un client
Dopo aver compreso come phpRPC effettua la codifica e la decodifica delle informazioni e come è definita la classe che ci permette di definire un server XML-RPC, analizziamo il codice utilizzato per definire la classe che ci servirà per poter effettuare in modo trasparente le chiamate ad un server remoto. La definizione della classe è contenuta nel file rpc_client.php. Per completezza il codice è riportato, senza commenti dell'autore, qui di seguito:
class rpc_Client {
var $host;
var $port = 80;
var $url;
var $username;
var $password;
var $sent;
var $recieved;
function rpc_Client($host, $url, $port=80) {
$this->host = $host;
$this->url = $url;
$this->port = $port;
}
function username($user) {
$this->username = $user;
}
function password($pass) {
$this->password = $pass;
}
function send($data) {
global $rpc_decoder;
$header = "POST " . $this->url . " HTTP/1.0n";
$header .= "User-Agent: phpRPCn";
$header .= "Host: " . $this->host . "n";
if ($this->username != "") {
$header .= "Authorization: Basic "
.base64_encode(
$this->username.":".$this->password
)
. "n";
}
$header .= "Content-Type: text/xmln";
$header .= "Content-Length: " . strlen($data) . "nn";
$this->sent = $header.$data;
$fp = fsockopen($this->host, $this->port, $errno, $errstr, 15);
if (!$fp) {
trigger_error("Fsocks error: $errno, $errstr", E_USER_ERROR);
} else {
fputs($fp, $this->sent, strlen($this->sent));
while (!feof($fp)) {
$this->recieved .= fgets($fp, 4096);
}
fclose($fp);
}
$data = explode("<methodResponse>", $this->recieved);
$data = '<methodResponse>'.$data[1];
$result = $rpc_decoder->decode($data);
return $result;
}
}
Il
cuore della classe è il metodo send, che si occupa di generare
la chiamata XML-RPC e di inviarla a server, leggendo la risposta e
restituendola al client. Il metodo accetta un parametro che contiene
una struttura XML rappresentante la chiamata correttamente
codificata, che viene corredata di header ed inviata al server
specificato nella proprietà $host. In caso siano state
settate precedentemente le proprietà $username e
$password, viene aggiunto un header alla chiamata che permette
di autenticarsi sul server remoto.
Come possiamo notare, la classe client non specifica alcun metodo per decidere quale funzione richiamare e con quali parametri. Questi dati dovranno essere correttamente forniti nel momento in cui si deciderà di scrivere uno script che funga da client, come quello seguente:
<?php
if ($query == "") {
$user = "test";
$pass = "test";
$query = "SELECT uid, uname, email, rank FROM prefix(users)";
}
if ($submit) {
$host = "4mps.sunsite.dk";
$url = "/hd-dev/modules/phpRPC/server.php";
$port = 80;
require("../cache/.config.php");
require("../include/rpc_encoder.php");
require("../include/rpc_decoder.php");
require("../include/rpc_client.php");
$rpc_client = new rpc_Client($host, $url, $port);
$rpc_encoder = new rpc_Encoder("xoops.xmlQuery");
$rpc_encoder->add_param("base64", $user);
$rpc_encoder->add_param("base64", $pass);
$rpc_encoder->add_param("base64", $query);
$message = $rpc_encoder->return_call();
$result = $rpc_client->send($message);
echo "<hr>$query<br>Resulted in:<hr>";
print_r($result);
echo "<hr>";
} else {
echo "<hr>This form logs to a distant site, queries the database,
and prints out an array result.<hr>";
}
?>
<form method="post">
<table>
<tr>
<td>User Name:</td>
<td>
<input type="text" name="user" value="<?=$user;?>">
</td>
</tr>
<tr>
<td>Password:</td>
<td>
<input type="text" name="pass" value="<?=$pass;?>">
</td>
</tr>
<tr>
<td>SQL Query:</td>
<td>
<input type="text" name="query" value="<?=$query;?>">
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" name="submit">
</td>
</tr>
</table>
</form>
You can see the source of this file <a href="ftp://ftp.sunsite.dk/projects/4mps/web/htdocs/hd-dev/modules/phpRPC/_samples/xmlquery.php">here</a>.<br><br>
Source <a href="ftp://ftp.sunsite.dk/projects/4mps/web/htdocs/hd-dev/modules/phpRPC/library/xoops/xmlQuery.php">on the server</a> that handles this request.<br> Server access is handled via a simple <a href="ftp://ftp.sunsite.dk/projects/4mps/web/htdocs/hd-dev/modules/phpRPC/include/access.php">include()</a>.
Il file in questione (prelevato dalla cartella _samples della libreria phpRPC) descrive un semplici client che si occupa di effettuare una query SQL ad un server e recuperarne il risultato. Tralasciando l'HTML, possiamo notare che una chiamata XML-RPC tramite phpRPC si può effettuare seguendo questi passi:
- includere
i file necessari; - istanziare
rpc_Client ed rpc_Encoder specificando i parametri
corretti; - codificare
i parametri richiesti tramite il metodo encode dell'istanza di
rpc_Encoder; - prelevare
la chiamata codificata correttamente tramite return_call; - inviare
la chiamata attraverso il metodo send di rpc_Client; - utilizzare
il valore restituito;
Conclusioni
Termino qui la descrizione della struttura della libreria, e vi invito a studiare approfonditamente il workflow utilizzato in modo da comprendere appieno come sviluppare una vostra libreria che supporti XML-RPC oppure come migliorare quelle già esistenti. Come sempre sono a vostra disposizione per eventuali chiarimenti.