Torna alla pagina di Ingegneria dei Processi Aziendali
:: Panoramica sul REST ::
1. Cos'è il REST?REST è un termine introdotto da Roy Fielding per descrivere uno stile architetturale per applicazioni web, ed è l'acronimo di Representational State Transfer. Per capire cosa significa facciamo un passo indietro. |
|
Il REST è un'alternativa leggera ai meccanismi come le RPC e i servizi Web che usano SOAP, WSDL, ecc. Questo non significa che non sia potente, e infatti non c'è nulla che si possa fare in SOAP che non sia possibile realizzare con un'architettura REST.
Va detto infine che il REST non è uno standard, ma ne usa molti: HTTP, URI, formati standard dei file (xml, html, jpg, ...), tipi MIME (text/xml, text/html, image/jpg, ...), ...
Il REST definisce un insieme di principi architetturali per realizzare i servizi web, che spesso sono dei veri e propri vincoli di progettazione:
Non sono invece vincoli di progettazione la piattaforma su cui vengono eseguiti i servizi (non importa se il server è Unix e il client è Mac, o viceversa), né il tipo di linguaggio usato per implementarli.
Nei prossimi sottoparagrafi approfondiremo alcuni dei principi architetturali citati.
Ecco alcune linee guida da seguire nella fase di progettazione delle URI:
www.rai.it/servlet/programma
, che risparmierebbero molto tempo a potenziali hacker per scoprire come bucarmi il servizio web;
redirect
;
redirect
è per indirizzare risorse con contenuti sensibili, come informazioni personali degli utenti. Ad esempio si potrebbe fare in modo che la richiesta della URL www.banca.it/mio-username/saldo
mi ridiriga alla URL col mio username, ad esempio www.banca.it/guido/saldo
(possibilmente solo dopo avermi fatto autenticare).
Una risorsa è un'entità concettuale, e una rappresentazione è la concreta manifestazione della risorsa sull'applicazione del client. Abbiamo detto che ogni risorsa ha un URL associata, ma va chiarito che questa non è una URL fisica, bensì logica.
Per capire meglio consideriamo questa URL di esempio, che restituisce la rappresentazione della risorsa "3230° puntata di Blob":http://www.rai.it/programma/blob/3230
L'identificativo numerico non deve farci credere che esistono (almeno) 3230 file, uno per ogni rappresentazione della risorsa "puntata di Blob": bastano infatti 2-3 file di script per generare automaticamente il contenuto di tutte le rappresentazioni. Dato però che all'utente va nascosta la tecnologia implementativa dietro ogni URL, gli sarà fornito un indirizzo alla risorsa logica e non a quella fisica che la genera.
Ecco dunque perché le URL di un servizio REST sono chiamate URL logiche, perché non implicano l'esistenza di un file fisico associato.
Breve nota sulle istanze delle risorse: hanno vita limitata rispetto 'identificativo numerico: si tratta di una pratica comune per identificare le istanze delle risorse. Queste
Avere un'interfaccia comune tra client e server semplifica e disaccoppia l'architettura, facendo sì che le due componenti possano evolvere in modo indipendente. L'interfaccia standard per i servizi web RESTful è il protocollo HTTP, non a caso quello di trasferimento di un ipertesto. Le operazioni supportate sono le cosiddette funzioni CRUD:
Per essere più completi, ecco una tabella che mostra gli effetti delle richieste HTTP su due diversi tipi di URI: quelle che identificano gli insiemi di risorse e quelle che le identificano singolarmente.
Risorsa | POST | GET | PUT | DELETE |
URI insiemi di risorsewww.rai.it/programmi
| Crea un nuovo membro dell'insieme (la sua URI è assegnata automaticamente, e di solito viene ritornata al client) | Mostra l'elenco delle URI e qualche dettaglio aggiuntivo sui membri dell'insieme | Sostituisce l'intero insieme con un altro | Cancella l'intero insieme |
URI singole risorsewww.rai.it/programmi/blob
| Tratta la risorsa indicata come se fosse un insieme e crea una nuova sotto-risorsa al suo interno | Restituisce una rappresentazione della risorsa indicata, espressa nel tipo MIME più appropriato | Sostituisce la risorsa indicata, e se non esiste la crea | Cancella la risorsa indicata dal suo insieme |
Un altro aspetto importante di una chiamata REST è quella di ritornare il codice di risposta HTTP appropriato. Vediamo i principali:
redirect
, e dice che la risposta alla richiesta può essere trovata in un URL diverso da quello contattato ;
Quando si progetta la modalità di navigazione tra ipertesti per i trasferimenti di stato, la regola d'oro è: MAI lasciare al client il compito di costruire la URL per effettuare azioni sulla risorsa, ma INCLUDERLE nella risposta REST sotto forma di elenco di link.
Ad esempio se il client vuole ottenere l'elenco dei programmi della RAI e successivamente accedere alla rappresentazione di uno di questi:
www.rai.it/programma/[nome_programma]
;
Abbiamo detto che le comunicazioni tra client e server devono essere stateless, ma questo non vale per i servizi web o le loro risorse. Come il significato stesso del nome "REST" ci ricorda, tutto ruota intorno al problema di concordare uno stato in un sistema distribuito!
Dal punto di vista dei client lo stato è catturato dalla posizione corrente nell'ipertesto, cioè l'interfaccia attraverso cui possono interagire con le risorse. Il server può influenzare le transizioni di stato del client inviando diverse rappresentazioni nelle risposte alle GET, o inviando diversi collegamenti ipertestuali da seguire.
Dal punto di vista dei server è lo stato della risorsa che cattura lo stato attuale del servizio. In questa prospettiva il client non è solo colui che ha accesso alla risorsa (nelle sue diverse rappresentazioni), ma anche colui che ne può manipolare lo stato usando l'interfaccia CRUD.
Nello schema sotto illustreremo meglio quanto detto finora:
risorsa
, e si sposta nello stato "000";
STATO DEL CLIENT ### ### STATO DELLA RISORSA ### ### | | ... | GET /risorsa | ------------------ ... |----------------------->| |/risorsa | |<-----------------------| |----------------| | | <a href="1" /> | |<a href="1" /> | | | <a href="2" /> | |<a href="1" /> | | | | | 000 | | | | 000 | | | | | | | | / \ | GET /1 | |/1 | / \ |----------------------->| |----------------| |<-----------------------| |<xml> | 111 222 | 200 OK | | | 111 222 | <xml> | |----------------| | | | |
La funzione POST può essere usata sia in lettura che in scrittura, quindi va trattata con attenzione perché potrebbe cambiare lo stato della risorsa con effetti indesiderati. Ad esempio, se vogliamo incrementare il saldo di un conto corrente di 200€ utilizzando una POST e qualcosa va storto nella comunicazione (ad esempio il server va giù), ripetere l'operazione potrebbe depositare 400€ invece della somma corretta.
Le altre funzioni CRUD sono definite invece idempotenti, perché la loro ripetizione non ha effetti collaterali sulla risorsa:
Deposita(200€);
in:s = GetSaldo() // si usa una GET (operazione SAFE) s = s + 200€ // operazione eseguita in locale SetSaldo(s) // si usa una PUT (operazione idempotente)
Si ottiene perciò uno schema di questo tipo:
/saldo OOO 000 OOO 000 | | | GET /saldo | |----------------------->| |<-----------------------| | 200 OK | | s: 26 | | | | | | PUT /saldo | | s: 26 | |----------------------->| |<-----------------------| | 200 OK | | s: 27 |
Il servizio potrebbe essere ulteriormente raffinato, ad esempio tenendo conto delle modifiche concorrenti allo stato della risorsa da parte di due client diversi. Queste potrebbero generare infatti problemi come questo:
/saldo OOO 000 888 OOO 000 888 | | | | GET /saldo | | |----------------------->| | |<-----------------------| PUT /saldo | | 200 OK | s: 26 | | s: 26 |<-----------------------| | |----------------------->| | | 200 OK | | PUT /saldo | s: 27 | | s: 26 | | |----------------------->| | |<-----------------------| | | 409 Conflict | |
Il messaggio di errore 409 informa che c'è stato un conflitto dovuto all'inconsistenza della risorsa, che era stata infatti chiamata con un valore diverso da quello corrente.
Nei sottoparagrafi seguenti vedremo altri esempi di soluzione dei problemi di utilizzo di operazioni UNSAFE.
Abbiamo creato un servizio web che tra le altre cose permette all'utente di ordinare un oggetto e farselo recapitare a casa. Ciò che vogliamo è che nessun ordine venga perso, e che nessun ordine venga processato due volte (o spediremmo due oggetti uguali). In breve:
La soluzione è fare in modo che ogni richiesta d'ordine sia unica. Come? Ad esempio, se stiamo facendo compilare l'ordine attraverso una form html potremo inviare un parametro nascosto (insieme a quelli inseriti dall'utente) che faccia creare lato server un "ticket" dell'ordine. In questo modo se il server riceve una richiesta d'ordine con un ticket già noto potrà facilmente scartarla in quanto duplicato. Vediamo lo schema:
CLIENT SERVER OOO 000 OOO 000 | | | POST | |------------------------------>| (insieme "ticket") |<------------------------------| | 200 OK | | ticket | | | | | | POST | | ordine+ticket | |------------------------------>| (insieme "ordini aperti") |<------------------------------| | 201 Created | | Location: URI ordini aperti |
Alcune caratteristiche che deve avere il ticket:
Proviamo a trovare una soluzione alternativa a quella descritta nel paragrafo precedente.
Se prima abbiamo "incorporato" il ticket nei dati inviati tra client e server, ora lo utilizzeremo per creare un ordine pendente, cioè una URL su cui il client potrà ordinare usando una PUT. Una volta ordinato con successo, l'ordine pendente sarà spostato nell'insieme di risorse ordini aperti, a cui il client sarà rediretto con una risposta dal server con codice di stato 303 ("la risposta alla richiesta può essere trovata sotto un'altra URI usando il metodo GET").
Ecco lo schema:
CLIENT SERVER OOO 000 OOO 000 | | | POST | |-------------------------------->| (insieme "ordini pendenti") |<--------------------------------| | 201 Created | | Location: URI ordine pendente | | | | | | PUT | | ordine | |-------------------------------->| (ordine pendente) |<--------------------------------| | 303 See Other | | Location: URI ordine aperto |
Si tratta di una soluzione migliore della precedente, perché usa l'operazione PUT che è meravigliosamente idempotente: potremo quindi continuare a inoltrare la richiesta finché questa non avrà successo.
L'insieme "ordini pendenti" non sarà altro che una nuova risorsa, che avrà dunque bisogno di essere considerata nella fase di design delle URI.
Una possibile metodologia di progettazione di un servizio REST è la seguente:
Progettiamo l'applicazione web "POTA, VOTA!" che permette di creare sondaggi e di votare, e proviamo a seguire i primi quattro passi della metodologia di progettazione descritti sopra.
Le risorse individuate sono:
poll
, che identifica l'insieme dei sondaggi;
vote
, che identifica l'insieme dei voti.
Ogni sondaggio dell'insieme poll
è identificato da un {id}, così come le istanze dell'insieme vote
. L'{id} potrebbe essere benissimo un identificativo numerico.
Ogni istanza di sondaggio contiene la sotto-risorsa voto, che a sua volta contiene i voti effettivi degli utenti. Graficamente potremo avere una struttura di questo tipo:
poll {id1} vote {id4} {id5} {id2} {id3} ...
La strategia con cui vengono assegnati gli {id} dipende dalla funzione CRUD utilizzata per istanziare le risorse. La questione sarà approfondita nei paragrafi successivi.
Le URI delle risorse dovranno incorporare gli {id} delle loro istanze, quindi avremo qualcosa come:
www.potavota.it/poll/01234
, si riferisce al sondaggio 01234;
www.potavota.it/poll/01234/vote/2
, si riferisce al voto 2 del sondaggio 01234.
Per creare un sondaggio dovremo:
/poll /poll/01234 888 -------------------------------> 000 888 <------------------------------- 000 POST /poll Host: www.potavota.it Content-Type: application/xml <?xml version="1.0"> <options>A,B,C</options> 201 Created Location: /poll/01234
Il server assegna al sondaggio appena creato una URL (www.potavota.it/poll/01234
), che in futuro può essere usata come servizio web anche da altri client.
Ripetiamo per l'ennesima volta che il modo in cui il web service genera il sondaggio è completamente trasparente al client: l'unica cosa che deve sapere è che se invia quell'xml con una POST otterrà la creazione di una nuova istanza della risorsa poll
. In virtù di questa trasparenza, i programmatori del servizio sono liberi di modificare l'implementazione delle risorse senza che il client ne risenta.
Per leggere il contenuto di un messaggio o di un voto useremo una GET.
/poll /poll/01234 /poll/01234/vote 000 <------------------------------- 888 000 -------------------------------> 888 GET /poll/01234 HTTP/1.1 Host: www.potavota.it Accept: application/xml 200 OK <options>A,B,C</options> <votes href="/vote">
Il formato del documento che il server deve restituire a una GET non deve essere specificato nell'URI, ma nell'header della GET come valore del parametro Accept:
, in cui va inserita la lista di tipi MIME accettati dal client.
Forzare il formato del documento da ricevere nell'URI (ad esempio con una GET www.potavota.it/poll/01234.html
) non è un'ottima strategia per il client, perché il server potrebbe non essere in grado di rappresentare la risorsa nel formato richiesto e non gli ritornerebbe niente.
Partecipare al sondaggio significa creare una sotto-risorsa di tipo voto all'interno del poll
interessato. Nello schema seguente (come nei successivi) sarà mostrata a sinistra l'operazione e a destra la verifica del suo risultato utilizzando una GET:
/poll /poll/01234 /poll/01234/vote /poll/01234/vote/1 888 --------------------------------> 000 <------------------------------ 888 888 <-------------------------------- 000 ------------------------------> 888 POST /poll/01234/vote GET /poll/01234 Host: www.potavota.it Host: www.potavota.it Content-Type: application/xml Accept: application/xml <?xml version="1.0"> <name>Pippo</name> 200 OK <choice>B</choice> <options>A,B,C</options> <votes> 201 Created <vote id="1"> Location: <name>Pippo</name> /poll/01234/vote/1 <choice>B</choice> </vote> </votes>
Cambiare il voto in un sondaggio significa aggiornare il valore di quello già dato, per cui dovremo utilizzare la chiamata PUT:
/poll /poll/01234 /poll/01234/vote /poll/01234/vote/1 888 --------------------------------> 000 <------------------------------ 888 888 <-------------------------------- 000 ------------------------------> 888 PUT /poll/01234/vote/1 HTTP/1.1 GET /poll/01234 Host: www.potavota.it Host: www.potavota.it Content-Type: application/xml Accept: application/xml <?xml version="1.0"> <name>Pippo</name> 200 OK <choice>C</choice> <options>A,B,C</options> <votes> 200 OK <vote id="1"> <name>Pippo</name> <choice>C</choice> </vote> </votes>
Ora che abbiamo visto sia la POST che la PUT possiamo chiederci: "qual è il metodo corretto per creare una risorsa inizializzando il suo stato??"
La risposta è: dipende. Da cosa? Dal modo in cui vogliamo che gli {id} siano generati:
-> PUT /resource/{id} <- 201 CreatedIl sistema è a prova di fallimenti (la PUT è idempotente), ma dovremo assicurare che l'{id} sia univoco (due client concorrenti potrebbero creare due risorse diverse assegnandogli lo stesso identificativo). La soluzione potrebbe essere l'utilizzo di una GUID;
-> POST /resource <- 301 Moved Permanently Location: /resouce/{id}Il problema di questa soluzione è complementare a quello precedente: la POST è UNSAFE, quindi ripeterla due volte in caso di fallimento della comunicazione potrebbe avere effetti indesiderati.
In realtà c'è una terza via: predisporre una URL del tipo www.potavota.it/poll/poll-new
da richiamare con una GET, che restituirà al client tutte le informazioni necessarie per creare una nuova risorsa con una POST. Queste informazioni potrebbero essere sotto forma di form html, o di prototipo xml, o di altro tipo.
Infine, per cancellare un voto già dato o un intero sondaggio si userà la richiesta DELETE. Nell'esempio che segue mostreremo come avviene la cancellazione del sondaggio con ID 01234
(si noti che la GET successiva restituisce l'avviso che la risorsa non è stata trovata):
/poll /poll/01234 /poll/01234/vote /poll/01234/vote/1 888 ------------------------> 000 <------------------------ 888 888 <------------------------ 000 ------------------------> 888 DELETE /poll/01234 GET /poll/01234 Host: www.potavota.it 200 OK Accept: application/xml 404 Not Found
In un'architettura REST ben progettata i cookie non dovrebbero essere né presenti, né tanto meno necessari. In primo luogo per una coerenza di design: le chiamate devono essere stateless, quindi dovrebbero contenere al loro interno tutte le informazioni necessarie perché il server le processi. Poiché i cookie per definizione sono informazioni esterne alle chiamate, rappresenterebbero un'eccezione allo stile architetturale.
Il secondo motivo riguarda invece la scalabilità del sistema. Supponiamo ad esempio che i cookie contengano anche solo una chiave di sessione che catturi lo stato dell'applicazione. In questo caso il client dovrà trasferire meno informazioni per chiamata, ma il server sarà obbligato a salvarsi da qualche parte i dati contenuti nel cookie: più sono i client e più saranno i dati da salvare. Inoltre il server si dovrà dotare di un qualche meccanismo di garbage collection per ripulire tutte le sessioni diventate ormai inattive, e all'aumentare dei client serviti il carico di lavoro diventerebbe sempre più oneroso.