Con questo articolo iniziamo un breve viaggio nel mondo delle architetture REST. In particolare, prendendo spunto dal modello di maturità di Richardson, vedremo da una prospettiva molto pratica come sia possibile transitare da un sistema RESTless a un‘architettura RESTful.
Introduzione
Questo articolo e quello che seguirà in un uno dei prossimi numeri sono dedicati allo stile architetturale REST. In particolare, in questo primo articolo focalizziamo l’attenzione sulle direttive fondamentali REST e sfruttando il modello di maturità proposto da Leonard Richardson [1] (figura 1) vedremo, da un punto di vista molto pratico, come far evolvere un sistema attraverso i vari livelli del modello di maturità REST. Partendo dallo stadio zero, in cui le direttive REST non sono seguite (The Swamp of Pox), discuteremo via via i vari livelli fino a raggiungere la “gloria del riposo” (Glory of Rest). Per conferire a questa trattazione un aspetto molto operativo, ci avvarremo come esempio di un ipotetico servizio on-line progettato per consentire agli studenti universitari “comodamente” seduti nella famosa poltrona di casa di prenotarsi per le varie sessioni di appello. In particolare, mostreremo come far sì che questo sistema di esempio, inizialmente basato su una serie di messaggi XML, accolga via via le direttive REST che gli permettono di progredire attraverso i vari modelli di maturità.
Rest in breve
Il Web è popolato da articoli e trattazioni relative alle architetture REST che tristemente ripetono le medesime battute. Pertanto, al fine di minimizzare la monotonia, in questo paragrafo ci limiteremo a ricordare brevemente i concetti fondamentali, riducendo al minimo la ripetizione delle storielle ormai usurate. L’acronimo REST, REpresentational State Transfer (“trasferimento dello stato di rappresentazione”) deriva dalla tesi di dottorato di Roy Fielding intitolata “Architectural Styles and the Design of Network-based Software Architectures” (“stili architetturali e progettazione di architetture software basate sul networking”) [2]; concediamoci una battuta ricorrente: per essere una tesi di dottorato, non è poi così difficile da leggere. Fielding è uno dei principali autori di del protocollo HTTP, HypertText Transfer Protocol, (“protocollo di trasferimento degli ipertesti”) versione 1.0 e 1.1.
REST non è un’architettura bensì uno “stile architetturale” formato da vincoli, linee guida e best practice. La formulazione dello stile REST è stata ottenuto dopo un’attenta analisi effettua da Fielding sulle risorse e le tecnologie disponibili per la creazione di applicazioni distribuite. L’assioma di base è che, senza imporre alcun vincolo, i sistemi tendono “naturalmente” a evolvere in maniera entropica, generando le conseguente negative che tutti noi ben conosciamo (sistemi costosi da creare, difficili da mantenere e da far evolvere, etc.).
Con questo assunto in mente, Fielding ha iniziato la sua esplorazione nel dominio degli stili delle architetture distribuite partendo dal limite inferiore da lui definito “spazio nullo” (null space), ossia il “Far West”, rappresentato da organizzazioni dotate di sistemi a basso grado di maturità in cui tutte le risorse tecnologiche sono disponibili, tutti gli stili sono ammessi, senza regole ne’ limiti. Continuando lungo la direttrice evolutiva, Fielding ha poi definito lo stato di totale maturità caratterizzato da sistemi che rispettano le regole da lui definite e che quindi possono essere definiti compatibili con le guideline REST (RESTful). Queste regole sono condensate nei seguenti sei vincoli, di cui i primi cinque sono obbligatori, mentre l’ultimo è facoltativo. Vediamo quindi i 5+1 vincoli cui un sistema deve sottostare per essere definito RESTful.
1. Client-server
Il sistema deve essere di tipo client-server, in cui le parti interagiscono attraverso interfacce uniformi.
2. Stateless
Il sistema deve essere stateless. Non deve essere necessario implementare sul lato server funzionalità atte a mantenere le sessioni utenti. In questo modo ogni nuova richiesta è indipendente da quelle precedenti e quindi può essere gestita da un’istanza qualsiasi del server. Quindi ogni richiesta deve contenere tutte le informazioni necessarie per permettere a ogni istanza del server di gestirla. La ratio di questo vincolo è garantire un elevato grado di scalabilità del server e anche a renderlo più affidabile. Nota: questo vincolo non vieta tuttavia la possibilità ai server di gestire una propria cache al fine di migliorare le prestazioni. L’esempio classico è l’applicazione delle politiche di sicurezza. In particolare, è prassi che il server e il client si scambino un token come attestato di autenticazione dell’utente (l’utente è effettivamente chi aveva dichiarato di essere). Tuttavia, ciò è solo la punta dell’iceberg. Il sistema deve poi usare il profilo dell’utente per autorizzarlo a eseguire una serie di funzioni e accedere ai dati. Al fine di migliorare le prestazioni, è prassi che il server inserisca nella cache i profili degli utenti collegati, con una validità temporale che dipende dalle politiche di sicurezza. Quindi, anche se il server mantiene informazioni sugli utenti, la comunicazione rimane stateless.
3. Cache a livelli diversi
Il sistema deve essere in grado di supportare cache a diversi livelli. I browser commerciali sono in grado di eseguire il caching delle informazioni inviate dai vari server disponibili in Internet. Pertanto le risposte devono, implicitamente o esplicitamente, definire i livelli consentiti di cache al fine di supportare il client nell’ottimizzazione consistente delle performance.
4. Accesso uniforme
I sistemi devono essere accessibili in modo uniforme: ogni risorsa deve avere un indirizzo univoco globale e un punto valido di accesso. Ciò definisce un’interfaccia uniforme che permette un elevato grado di disaccoppiamento tra client e server.
5. Stratificazione multi-tiered
Il sistema deve essere stratificato (multi-tiered). Un client internet normalmente non è in grado di determinare se è connesso direttamente con il server o se è connesso attraverso un agente intermedio, come del resto tipicamente avviene. In effetti, le architetture classiche prevedono la presenza di agenti intermedi (load balancer, proxy dotati di cache, sistemi di sicurezza tipo firewall, e così via).
6. Supporto per codice eseguibile
Questo è un vincolo facoltativo: i sistemi coinvolti devono essere in grado di estendere temporaneamente o di customizzare le funzionalità al lato client attraverso il trasferimento di codice eseguibile. Alcuni esempi sono dati dal trasferimento di componenti eseguibili come le applet Java o di script sorgenti come JavaScript.
Se quest’ultimo vincolo è opzionale, gli altri cinque sono invece obbligatori: se anche uno solo dei primi cinque non è rispettato, allora un servizio non può fregiarsi del riconoscimento di RESTful. Dall’analisi di questi vincoli si comprende che le architetture RESTful sono fortemente basate sulla più grande infrastruttura informatica creata dall’uomo, il Web, e permettono a qualsiasi sistema ipermediale distribuito di possedere importanti proprietà, quali semplicità, scalabilità, portabilità, visibilità e tipicamente elevate performance.
Citando Fielding [3]: “La separazione delle responsabilità dettata dalle linee guida REST semplifica l’implementazione dei componenti, riduce la complessità della semantica dei connettori, migliora l’efficacia per la messa a punto delle prestazioni e aumenta la scalabilità dei componenti server-side. I vincoli dei sistemi a strati permettono ai sistemi intermedi (proxy, gateway e firewall) di poter essere introdotti in convenienti punti della rete senza cambiare le interfacce tra i componenti, permettendo così loro di assistere nella gestione della comunicazione e/o di migliorare le prestazioni grazie a cache condivise su larga scala. REST consente l’elaborazione intermedia forzando i messaggi a essere auto-descrittivi: l’interazione tra le richieste è stateless, i metodi standard e i media type sono usati per indicare la semantica e lo scambio di informazioni, e le risposte esplicitamente indicano come eseguire la cache”.
Per finire, c’e’ da notare che questi vincoli non dettano il tipo di tecnologia da utilizzare: tuttavia vi sono chiare direttive che definiscono come i dati devono essere trasferiti tra i componenti e quali sono i vantaggi nel seguire le linee guida.
Figura 1 – REST maturity model. Uno schema del modello definito da Richardson [4].
REST maturity model
Dopo aver ricapitolato i concetti basi dello stile REST presentiamo il modello di maturità per le architetture REST ideato da Leonard Richardson e documentato (come spesso avviene) da Martin Fowler [1]. Nei paragrafi seguenti vedremo, da una prospettiva molto pratica, quanto esposto fino a questo momento.
Livello 0: The Swamp of POX (letteralmente “la palude del vaiolo”)
“La palude del vaiolo” è un gioco di parole utilizzato per indicare sistemi popolati da una miriade di sevizi che si scambiano messaggi XML. POX è l’acronimo di Plain Old XML, ma in inglese “pox” è un termine che indica diverse gravi malattie, in particolare il vaiolo.
Tipicamente, i sistemi POX sono quelli che si scambiano messaggi in modo sincrono attraverso il protocollo HTTP (HyperText Transport Protocol – Application layer). Secondo Richardson si tratta di una palude in quanto questi servizi così strutturati non seguono le direttive REST, e quindi il sistema nel suo complesso risulta non organizzato razionalmente, difficile da controllare, da far evolvere, e così via. Il livello 0 dunque è caratterizzato da sistemi che utilizzano il protocollo di networking alla base del WWW come protocollo di integrazione, limitandosi però a un utilizzo base (request-response sincrono) che non include le direttive REST come identificazione delle risorse, utilizzo di verbi, etc. Pertanto i sistemi a questo livello sono posti alla base, al livello 0, del modello di maturità.
Come già detto, ci avvarremo di un semplice esempio: un ipotetico sistema di prenotazioni esami universitari. Tale modello, come lecito attendersi, si presta a essere sofisticato a piacimento, tuttavia è opportuno ricordare che l’oggetto di questa trattazione è l’illustrazione dello stile architetturale REST e non il disegno/implementazione del suddetto sistema.
Si consideri il nostro browser con un engine incapsulato nella pagina HTML, Ajax/JavaScript, Flex, Applet, etc. che avvia la comunicazione con la seguente richiesta iniziale. Ecco un esempio di avvio comunicazioni tra un agente client e il server.
. . . <examSessionRequest lecture="ING_SW_ARCH01" professor="LVT" fromDate="01-11-2011" /> . . .
Pertanto viene emessa la richiesta di tutte le sessioni di esame per l’esame di architetture dei sistemi software della facoltà di ingegneria informatica a partire dall’11 novembre 2011. In questo esempio si assume l’utilizzo del protocollo XML per lo scambio dei dati per motivi di convenienza. Tuttavia, la situazione non varia qualora si fosse utilizzato un altro dei formati disponibili: JSON, HTML, coppie (chiave, valore), formato proprietario, e così via.
A fronte di questa richiesta, il server di gestione delle prenotazioni esami dell’università risponde come segue:
HTTP/1.1 200 OK . . . date="04-11-2011" availability = "9" startingTime="10:00" endingTime="13:00" buildingId="SENATE_HOUSE_01" roomId="222" /> date="18-11-2011" availability = "21" startingTime="10:00" endingTime="13:00" buildingId="SENATE_HOUSE_01" roomId="115" />
Il sistema risponde con la presenza di due sessioni di esame, una il 4 novembre e una il 18 novembre. Per la prima sono disponibili 9 posti, mentre per la seconda sono disponibili 21. Inoltre, ogni sessione è corredata dai dati circa il luogo in cui si terrà la sessione, e dai corrispondenti orari.
A questo punto, scelta la sessione d’esame desiderata, è possibile effettuare la richiesta di prenotazione per la prima sessione:
. . . token="XXXXXXXXX" . . . <bookExamSessionRequest studentId="1000232403" examSessionId="110" />
Ed ecco una possibile risposta da parte del sistema con conferma di avvenuta prenotazione:
HTTP/1.1 200 OK . . . token="XXXXXXXXX" . . . <bookExamSessionResponce bookingId="20111022_221035_100" studentId="1000232403" examSessionId="110" />
Quindi l’interazione tra le due componenti è basata su uno scambio sincrono di messaggi XML attraverso il protocollo HTTPS. Qualora si fosse utilizzata una rappresentazione WebService / SOAP, la situazione non sarebbe cambiata di molto: i messaggi XML sarebbero stati arricchiti dagli specifici tag dell’Envelope SOAP. Inoltre, dall’analisi dell’esempio, è possibile notare che quando l’XML è progettato per supportare le performance, questo non differisce poi di molto da JSON.
Livello 1: Resources (“risorse”)
Il livello successivo nel modello di maturità REST consiste nell’organizzare i servizi in di termini risorse. Una risorsa è qualunque cosa accessibile nel web, ossia il cui stato sia trasferibile tra server e client. Alcuni esempi sono:
- un libro venduto on-line,
- la temperatura presente in quel di Roma,
- le previsioni meteo di Londra,
- un item oggetto di un’asta eBay,
- i dati identificativi di un volo aereo,
- una ricetta di cucina,
- un valore di cambio valuta,
- i prezzi di un prodotto finanziario e le informazioni relative allo stesso,
- la spiegazione di un termine tratta da Wikipedia
Il concetto di risorsa è centrale per le architetture REST e una delle direttive base richiede di assegnare a tutte le risorse un identificativo univoco che nel mondo WWW equivale a un URI, Uniform Resource Identfier, (identificatore uniforme di risorse). Quindi tutte le risorse devono essere identificate univocamente a livello globale per mezzo di apposito URI. L’utilizzo di un metodo predefinito e consistente di assegnare identificativi univoci genera tutta una serie di vantaggi. In primo luogo non è necessario ogni volta inventarsi uno schema, giacche’ questo esiste già, è largamente utilizzato su scala mondiale, non presenta problemi, tutti sono in grado di comprenderlo ed è uno standard internazionale. Inoltre, risorse identificate attraverso un URI sono di fatto un link che è possibile condividere con diverse persone (per esempio inviandolo attraverso una e-mail).
Si consideri per esempio la necessità di segnalare un apposito libro messo a disposizione da Amazon, ad esempio esempio
http://www.amazon.it/UML-Hops-Tecnologie-Luca-Vetti-Tagliati/dp/8883780698
oppure la necessità di voler prenotare il medesimo volo aereo con un collega di lavoro, oppure un termine attinto da Wikipedia
http://en.wikipedia.org/wiki/Representational_state_transfer
Insomma è evidente che identificare univocamente le risorse attraverso l’URI, oltre a essere un metodo standard, fornisce tutta una serie di importanti vantaggi. Dall’analisi degli esempi precedenti si può notare che l’indirizzo URI, in prima analisi, è composto da una parte fissa e da una parte variabile che identifica la specifica istanza. Ora, mentre la parte fissa è pressoche’ definita, per la parte variabile il suggerimento consiste nell’utilizzare un codice univoco naturale; qualora ciò non sia possibile, anche un id generato dal sistema (per esempio un sequence number) rappresenta una buona alternativa. Non è un peccato mortale esporre un simile dettaglio tecnico di basso livello addirittura nell’API, fintantoche’ il codice sia poi utilizzabile dai client.
A questo punto dovrebbe essere chiaro su cosa si intenda con il termine risorsa anche se in informatica questo vocabolo è utilizzato con diversi significati. Di certo in questo contesto non si fa riferimento a risorse fisiche come microprocessori, memorie, stampanti, o simili. Molto pragmaticamente le risorse sono le principali astrazioni ossia gli “oggetti” che il servizio intende esporre. Qualora si partisse dal disegno di un diagramma delle classi rappresentante il business oggetto di studio (modello tipicamente detto “Domain Object Model”), allora gli oggetti (ossia le classi) principali presenti in tale modello, verosimilmente, potrebbero diventare risorse in termini REST. Nel caso precedente alcuni esempi di risorsa sono i seguenti.
Professor
http://myUniversity.ac.uk/professors/lvt
Lecture (ossia la materia oggetto di studio, tipicamente chiamata impropriamente “esame”)
http://myUniversity.ac.uk/lecture/ING_SW_ARCH01
Exam Session (sessione esame)
http://myUniversity.ac.uk/examsessions/110
Building (edificio con i locali a disposizione della facoltà)
http://myUniversity.ac.uk/buildings/senate_house_01
Room (stanze a disposizione in ogni edificio)
http://myUniversity.ac.uk/buildings/senate_house_01/rooms
La tabella 1 mostra le coppie “URI – spiegazione” della risposta che si potrebbe ottenere qualora il servizio fosse stato realmente implementato:
Tabella 1 – Esempio di risorse URI e del risultato generato dal sistema di prenotazione sessioni d’esame.
ID espressi in termini di URI
A questo punto per far sì che il sistema utilizzato come esempio in questo articolo soddisfi i requisiti del livello 1 del modello di maturità REST, è sufficiente assicurarsi che gli ID siano sostituiti dagli appositi URI e che il sistema sia in grado di interpretarli. In questo modo si hanno degli identificatori univoci di risorsa leggibili sia da esseri umani, sia interpretabili da un browser. Pertanto si prestano a essere utilizzati efficacemente per integrazioni di sistemi (B2B, business to business).
Da notare che con gli stessi URI è possibile costruire delle interrogazioni più o meno complesse. Qui di seguito sono riportati alcuni esempi, quali l’elenco delle materie previste dalla facoltà di ingegneria del software:
http://myUniversity.ac.uk/lectures?faculty="ING_SW"
oppure l’elenco delle materie previste per il primo anno della facoltà di ingegneria:
http://myUniversity.ac.uk/lectures?faculty="ING_SW"&year="1"
Il listato che segue presenta un esempio di utilizzo degli ID nel contesto delle prenotazioni sessioni di esame. Come si può notare, giacche’ le sessioni di esame sono risorse, queste possono essere indirizzate direttamente.
. . . POST http://myUniversity.ac.uk/examsessions/110 HTTP/1.1 . . . . . .
Un esempio di risposta del server potrebbe essere quella riportata nel listato seguente:
. . . HTTP/1.1 200 OK
A questo punto è utile enfatizzare la potenza degli ID espressi in termini di URI. In particolare questi sono link a risorse (come per esempio, studenti, materie, sessioni di esame) che possono essere fornite da processi, applicazioni, sever sparsi per il mondo: URI esprimono link secondo uno standard globale. In linea teorica tutte le risorse presenti nel web possono essere “linkate”: un libro da Amazon, un item di eBay, etc. come già spiegato sopra.
Da notare che tramite HTTP è possibile trasferire svariati formati: tutti quelli conosciuti dal Web (Content Type), per esempio: una richiesta a Yahoo finance dà luogo a file, una richiesta al sito YouTube restituisce uno streaming che il plug-in Flash sa interpretare per mostrarci un video, e così via. Questo processo si chiama Content Type Negotiation (negoziazione del tipo di contenuto). In particolare, ogni richiesta include nell’header una serie di content type riconosciuti con associato il grado di conoscenza (rating), quindi il tipo della risposta è una scelta del client tra i formati offerti dal server per una specifica richiesta. Per esempio, un client potrebbe specificare nell’header:
Accept-Language: it; q=1.0, en; q=0.5 Accept: text/html; q=1.0, text/*; q=0.8, image/gif; q=0.6, image/jpeg; q=0.6, image/*; q=0.5, */*; q=0.1
Come si può notare, sia la lingua, sia i formati riconosciuti includono un rating.
Livello 2: HTTP verbs (“verbi HTTP”)
Come visto nel paragrafo precedente, gli URI sono molto potenti sia perche’ permettono di individuare una risorsa messa a disposizione da un server ubicato in qualsiasi parte del mondo, sia perche’ i browser sono in grado di effettuare la richiesta e di interpretare in qualche modo la risposta. Questo è possibile grazie all’esistenza di un altro standard: i “verbi” HTTP. Da notare che durante i primissimi anni della programmazione web, le comunicazioni client server si basavano quasi esclusivamente sul metodo HTTP GET, che veniva utilizzato per fare quasi tutto: reperire dati, inviare form da memorizzare, etc. Al GET, in alcuni casi, si preferiva il metodo POST, quando ci si imbatteva in alcune limitazione del metodo GET, come la dimensione limitata per l’invio dati. Le cose, da allora, si sono evolute tanto che il livello 2 richiede di utilizzare i metodi HTTP standard GET, PUT, POST, DELETE, in modo congruente.
Per comodità del lettore che non li conosca perfettamente, la definizione standard dei metodi HTTP è riportata nei paragrafi seguenti [4].
Metodo OPTIONS
Il metodo OPTIONS è una richiesta di informazioni relative alle opzioni di comunicazione disponibili di un “servizio” (richiesta/risposta) esposto da una risorsa URI. Ciò consente al cliente di determinare le opzioni e/o requisiti associati a una risorsa, o le funzionalità di un server in maniera preventiva, ossia senza richiedere un’azione alla risorsa stessa. Le specifiche dettano che il risultato di questa invocazione non debba essere mantenuto in una cache. Se l’URI contiene un asterisco (*), allora la richiesta OPTIONS non è destinata a una risorsa specifica ma a un server. Pertanto richieste “*” sono utili solo per metodi come un “ping” o tipo “no-op” di metodo.
Metodo GET
Il metodo GET serve per recuperare tutte le informazioni, sotto forma di un’entità, relative alla risorsa identificata dal URI presente nella richiesta. Se l’URI richiesto si riferisce a un processo, allora la risposta deve contenere un’entità che contenga a sua volta i risultati dell’esecuzione del suddetto processo invece di restituire le informazioni relative al processo stesso.
La semantica del metodo GET presenta delle sfumature nella variante “GET condizionale”. Questo è caratterizzato dalla presenza nell’intestazione di uno o più di uno dei seguenti campi: If-Modified-Since, If-Unmodified-Since, If-Match, If-None-Match, If-Range. Il GET condizionale si differenzia da quello standard giacche’ richieste l’effettivo trasferimento dell’entità solo qualora le condizioni specificate nei campi condizionali presenti nell’intestazione siano soddisfatti. È evidente che la motivazione del metodo GET condizionale consiste nel ridurre l’occupazione di banda consentendo ai sistemi client di memorizzare enti nella cache e di ricevere gli aggiornamenti solo quando ciò sia veramente necessario (per esempio se la risorsa è stata modificata dopo il timestamp assegnato all’ultima copia ricevuta). Oltre al “GET condizionale” esiste un’altra variante denominata “GET parziale”, identificato dalla presenza di un campo “Range” nell’intestazione. La semantica di un GET parziale richiede che solo una parte dell’entità sia effettivamente trasferita. Anche in questo caso, la motivazione del metodo GET parziale consiste nel ridurre il consumo di banda permettendo ai client di decidere quali parti dell’entità trasferire. Chiaramente ciò ha senso solo in presenza di entità particolarmente estese.
La risposta a una richiesta GET può essere memorizzata nella cache sono se le condizioni di caching HTTP sono soddisfatte.
Metodo HEAD
Il metodo HEAD è quasi del tutto identico a GET, eccetto per il fatto che il server non deve restituire la sezione body nel messaggio di risposta. Le meta-informazioni contenute nello head della risposta di una richiesta HEAD devono essere idemtiche a quelle inviate in risposta a una richiesta GET. Questo metodo può essere utilizzato per ottenere meta-informazioni relative all’entità implicita nella richiesta senza però dover trasferire l’intero corpo dell’entità. Questo metodo è spesso utilizzato per il test della validità, accessibilità e verifica di modifiche recenti dei collegamenti ipertestuali.
La risposta a una richiesta HEAD può essere memorizzata in una cache, nel senso che le informazioni contenute nella risposta possono essere utilizzate per gestire un’entità precedentemente memorizzata nella cache di un client. In particolare, se i nuovi valori dei campi indicano che l’entità originale è cambiata rispetto alla copia nella cache (e lo si può evincere dai campi Content-Length, Content-MD5, E Tag o Last-Modified presenti nel HEAD) allora il client deve dar luogo alle procedure necessarie per gestire il caso di entità non valida (stale).
Metodo POST
Il metodo POST è utilizzato per permettere ai client di inviare una richiesta a un server contenente dati. Gli scenari tipici sono l’uploading dei file, l’invio di form compilate al lato client, l’invio di un commento a un blog, etc. Rispetto al metodo GET, dove solo un URL e le intestazioni possono essere presenti, il metodo POST può anche includere la parte body. Ciò permette di superare il vincolo sulle dimensioni proprio del metodo GET, consentendo a questo metodo di includere una quantità arbitraria di dati di qualsiasi tipo da inviare al sever. Il metodo POST è stato studiato per permettere un metodo uniforme di svolgere le seguenti funzioni: annotazione delle risorse esistenti; invio di un messaggio a una bacheca elettronica, newsgroup, mailing list, e similari; fornitura di un blocco di dati, come per esempio una form debitamente compilata, a un processo di gestione dei dati; invio dei dati da aggiungere a un database.
La funzione effettiva eseguita dal metodo POST è determinata dal server destinatario e tipicamente dipende dal “Request-URI”. L’entità inviata è subordinata allo URI nello stesso modo in cui un file è subordinato alla directory che lo contiene, un articolo è subordinato a un newsgroup a cui è inviato, o un record è subordinato a un database. L’azione eseguita dal metodo POST potrebbe non portare a una risorsa identificabile da un URI. In questo caso, una risposta appropriata consiste nel rispondere con uno stato 200 (OK) o 204 (no content), a seconda se la risposta includa o meno un’entità che descriva il risultato. Quando il risultato consiste in una risorsa creata sul server, la risposta dovrebbe essere 201 (created) e contenere un’entità che descrive lo stato della richiesta relativa alla nuova risorsa, e un Location header.
Le risposte a questo metodo non possono essere memorizzate in una cache, a meno che la risposta non includa un’appropriata Cache-Control o campi di scadenza (Expires) nell’intestazione. Tuttavia, una risposta di tipo “vedi altro” come la 303 (see other) può essere utilizzata per raccomandare al programma utente di recuperare una risorsa mantenibile in una cache.
Metodo PUT
Una richiesta PUT serve per richiedere che l’entità racchiusa nella richiesta stessa sia memorizzata dal server destinatario (server indirizzato dall’URI). Se la richiesta si riferisce a una risorsa già esistente, allora questa deve essere interpretata come una richiesta di modifica, pertanto l’entità inviata rappresenta una versione aggiornata della corrispondente presente sul server. Se l’URI della richiesta non punta a una risorsa esistente, e se l’URI è in grado di essere definito come una nuova risorsa dal client, il server destinatario può creare la nuova risorsa con lo specifico URI. Qualora ciò avvenga, il client deve essere informato attraverso una risposta con stato 201 (created). Se invece una risorsa esistente viene aggiornata, allora la risposta indicante il completamente dovrebbe includere uno dei seguenti codici 200 (OK) o 204 (no content ) “nessun contenuto”. Se la risorsa non può essere creata o modificata, allora il server deve rispondere con un appropriato errore che riflette la natura del problema. Il destinatario dell’entità inoltre non deve ignorare indicazioni che non capisce o che non può implementare, del tipo Content- specificati nell’intestazione, come per esempio Content-Range. In questi casi deve restituire una risposta del tipo 501 (not Implemented) “non implementato”.
Se la richiesta passa attraverso una cache e l’URI incapsulata nella richiesta identifica uno o più entità presenti nella cache, queste dovrebbero essere trattate come dati passati (stale). Le risposte a questo metodo non sono memorizzabili in cache.
La differenza fondamentale tra le richieste POST e PUT risiede nel diverso significato della richiesta URI. L’URI in una richiesta POST identifica la risorsa che gestirà l’entità presente nella richiesta stessa. Tale risorsa potrebbe essere un processo di accettazione dei dati, una porta verso un diverso protocollo, o un’entità separata che accetta annotazioni. Al contrario, l’URI in una richiesta PUT identifica l’entità allegata alla richiesta: il programma utente conosce quale URI è previsto e il server non deve tentare far eseguire la richiesta a qualche altra risorsa. Se il server desidera che la richiesta sia applicata a un URI diverso, deve necessariamente inviare una risposta 301 (moved permanently), “spostato permanentemente”, in modo da consentire al sistema chiamante di intraprendere le proprie procedure per terminare il processo o reindirizzare la richiesta.
Una singola risorsa può essere identificata da differenti URI. Per esempio, un articolo potrebbe avere un URI per identificare “la versione attuale”, diverso dall’URI che identifica una delle precedenti versioni. In questo caso, una richiesta PUT su un URI generale potrebbe causare diversi altri URI definiti da parte del server. La versione HTTP/1.1 non definisce il modo in cui un metodo PUT influenza lo stato del server.
Metodo DELETE
Le richieste DELETE servono per comunicare al server di eliminare la risorsa identificata dall’URI incapsulato nella richiesta stessa. Questo metodo può essere sovrascritto (overridden) da un intervento umano (o da altri mezzi) sul server. Il client non ha garanzia che l’operazione sia stata eseguita, anche se il codice di stato restituito dal server indica che l’azione è stata completata con successo. Tuttavia, il server dovrebbe restituire un codice di successo solo se, al momento in cui viene preparata la risposta, il server intenda effettivamente eliminare la risorsa o spostarla in una posizione non più accessibile. Una risposta di successo dovrebbe restituire il codice 200 (OK) o 202 (accepted), “accettato”, se l’azione non è stata ancora emanata ma comunque accettata. Dovrebbe restituire 204 (no content), “nessun contenuto”, se l’azione è elaborata, ma la risposta non include un’entità.
Se la richiesta passa attraverso una cache e l’URI identifica una o più entità presenti nella cache, queste dovrebbero essere trattate come dati vecchi (stale).
Le risposte a questo metodo non sono memorizzabili in cache.
Metodo TRACE
Il metodo TRACE è utilizzato per richiamare un ciclo di comunicazione remoto client-server-client. Il destinatario finale della richiesta dovrebbe restituire il messaggio ricevuto al client come entity-body di una risposta 200 (OK). Il destinatario finale è il server puntato dall’URI o il primo proxy o gateway che riceve il messaggio in cui il campo Max-Forwards (numero massimo di inoltri) abbia raggiunto il valore a zero nella richiesta (il campo Max-Forwards è utilizzato per evitare cicli infiniti). Richieste TRACE non devono includere entità.
TRACE consente al client di verificare ciò che viene ricevuto all’altro capo della catena per eseguire operazioni di test o di diagnostica. Il valore del campo di intestazione via è particolarmente interessante dal momento che agisce come una traccia della catena creata dalla richiesta. L’utilizzo del campo Max-Forwards consente al cliente di limitare la lunghezza della catena richiesta, utile per verificare una catena di proxy di inoltro messaggi in un ciclo infinito.
Se la richiesta è valida, la risposta dovrebbe contenere l’intero messaggio richiesta nell’entity-body, con un Content-Type = “message/http”.
Le risposte a questo metodo non possono essere memorizzate nella cache.
Metodo CONNECT
Il nome CONNECT è riservato per l’uso da parte di proxy che permettono il tunneling.
Metodi sicuri e idempotenti: siamo ancora al Livello 2
A questo punto è necessario chiarire i termini “sicuro” (safe) e “idempotente” (idempotent). Un metodo è definito sicuro se, a seguito della relativa chiamata, lo stato del server non cambia. Pertanto il metodo GET dovrebbe essere sempre sicuro (per esempio non è possibile alterare lo stato del server richiedendo più volte l’ottenimento dei dati di un professore universitario). La stessa risorsa può essere invocata tramite metodo GET quante volte si vuole senza che ciò alteri lo stato del server. Tuttavia è sempre possibile violare questa regola come per esempio inviando una richiesta GET, specificando una rappresentazione della risorsa con stato modificato. Come ricordato poc’anzi, agli albori della programmazione web era prassi utilizzare il metodoHTTP GET per eseguire qualsiasi operazione: reperire dati, inviare form da memorizzare, etc. Tuttavia dovrebbe essere ormai chiaro che si tratta di una prassi da non seguire più in quanto è basata su un abuso dei “verbi”.
Il metodo PUT non è sicuro per definizione. Anche qui esistono dei casi anomali, come per esempio richiedere una modifica della risorsa fornendo la stessa rappresentazione ricevuta. Tipicamente questo non dovrebbe alterare lo stato del server. Si tratta comunque di un caso speciale e non della regola.
A questo punto è anche chiaro come il metodo HEAD sia sicuro mentre DELETE non sia un metodo safe. Per finire, POST è un metodo un po’ ambiguo. Può essere utilizzato sia per modificare lo stato delle risorse al lato server, nel qual caso ha un comportamento del tutto equivalente al metodo PUT, oppure per eseguire un’operazione, come per esempio l’invio di un messaggio. In questo caso, il sever potrebbe, non cambiare stato.
Ora che abbiamo visto cosa è un metodo safe, vediamo che cosa è un metodo idempotent. Un metodo è definito “idempotente” se non vi è alcuna differenza osservabile fra l’effetto di una singola richiesta eseguita con quel metodo e n sue richieste multiple consecutive effettuate con l’identica richiesta. Quindi, indipendentemente dal numero delle volte che la stessa richiesta con lo stesso metodo è effettuata, il risultato prodotto è sempre lo stesso. Da quanto detto poc’anzi è evidente che i metodi GET e HEAD sono sicuri e idempotenti (sempre nell’esempio precedente, qualora la medesima richiesta di ottenimento dei dati di un professore universitario sia eseguita n volte, questa genera gli stessi risultati, al netto di altri processi indipendenti). Anche PUT e DELETE presentano la proprietà di idempotenza. In effetti, un’invocazione ripetuta di una richiesta PUT o DELETE dovrebbe variare lo stato del server esattamente come se l’invocazione fosse stata eseguita una sola volta. POST ancora una volta crea problemi e infatti il suo comportamento non è sempre idempotente. L’esempio classico è l’inserimento di un commento a un giornale. A meno di tecniche particolari, se una richiesta POST è eseguita n volte, la relativa esecuzione genera n post. Chiaramente esistono dei meccanismi per evitare ciò, tuttavia si tratta di soluzioni non standard. Da quanto scritto dovrebbe essere chiaro l’appellativo di pecora nera assegnata al metodo POST.
La proprietà di idempotenza è molto importante in quanto permette a un sistema client di ripetere la medesima richiesta più volte senza alterare lo stato del server (safe) o senza alterarlo ulteriormente. Si tratta chiaramente di una proprietà molto utile per semplificare l’integrazione tra sistemi (semplifica la semantica dei connettori). Caso tipico è quello di un client che esegue una richiesta a un server senza ricevere risposta in tempo utile per il sopraggiungimento del time-out. In questo scenario, il client, invece di intraprendere complicate procedure per cercare di capire cosa sia successo effettivamente al lato server (richiesta non ricevuta, richiesta ricevuta e processata con risposta non pervenuta in tempo utile, etc.), può tranquillamente emettere nuovamente la medesima richiesta.
Per quanto concerne il nostro esempio del sistema universitario, i listati mostrati in precedenza includono già le direttive di questo livello. Il diagramma UML di figura 2 (simile a quanto riportato da Stefan Tilkov [5]), anche se non perfettamente allineato alle specifiche ufficiali, evidenzia uno dei grandi vantaggi generati dal corretto utilizzo dei metodi HTTP standard: la semantica delle interfacce è ben definita e le interfacce stesse sono uniformi.
Figura 2 – Interfacce REST.
Livello 3: Hypermedia Controls (“controlli ipermediali”)
L’ultimo livello del modello di maturità REST è costituito dai “controlli ipermediali” (Hypermedia Controls). Il termine “ipermedia” (hypermedia) è stato coniato da Ted Nelson e si riferisce alla logica estensione del termine”ipertesto” (hypertext) in cui grafica, audio, video, testo e collegamenti ipertestuali si intrecciano per creare un genere non-lineare di media di informazione.
Per raggiungere questo livello è necessario soddisfare il vincolo noto con l’acronimo, forse non felicissimo di HATEOAS, HyperText As The Engine Of Application State (“ipertesto come motore dello stato delle applicazioni”). Il principio stabilisce che un client interagisce con un’applicazione di rete interamente attraverso ipermedia forniti dinamicamente dal server. Un client REST pertanto non ha bisogno di conoscere preliminarmente come interagire con una particolare applicazione o con un server; tutto quello di cui ha bisogno è una conoscenza generica degli hypermedia. Uno stile diametralmente opposto a questo è costituito, per esempio, dalle architetture orientate ai servizi (SOA), in cui i client e i server interagiscono attraverso un’interfaccia fissa, ben definita e documentata e implementata attraverso un linguaggio di descrizione dell’interfaccia (come WSDL, Schema XML, IDL, etc.).
Il vincolo HATEOAS serve a disaccoppiare client e server in modo da consentire al server di evolvere autonomamente le proprie funzionalità. Al fine di rispettare questo vincolo, il server deve restituire nella risposta anche l’insieme delle possibili azioni richiedibili.
Nell’esempio oggetto di studio, il server potrebbe rispondere alla richiesta del listato con una risposta come quella del listato seguente, compatibile con le direttive del livello 3:
HTTP/1.1 200 OK . . . date="04-11-2011" availability = "9" startingTime="10:00" endingTime="13:00"> uri ="/examsessions/110" /> date="18-11-2011" availability = "21" startingTime="10:00" endingTime="13:00"> uri ="/examsessions/211" />
Questa risposta contiene una sola azione; tuttavia è possibile creare risposte che contengano un insieme ben definito di azioni. Per esempio, nella risposta mostrata nel listato appena visto, si sarebbe potuta aggiungere un’azione di verifica della presenza di una propria prenotazione.
In ogni caso, è evidente che attraverso l’utilizzo di controlli ipermediali è possibile creare un ulteriore livello di indirezione tra client e server caratterizzato dal fatto che il server possa cambiare i propri link a proprio piacimento senza inficiare il funzionamento dei propri client. Tuttavia, non è un disaccoppiamto totale giacche’ deve esistere un accordo implicito tra client e server sul nome delle azion (per esempio “reserve”, “verify”, etc.), e il client e il server devono convenire sui metodi da utilizzare per le future richieste (per esempio mentre “reserve” richiede un’azione POST, per eseguire l’azione verify è sufficiente un GET).
Questo vincolo, infine, permette al server di pubblicare nuove azioni senza creare problemi ai client esistenti.
Conclusione
In questo primo articolo, abbiamo analizzato le linee guida REST attraverso il modello di maturità proposto da Richardson. In particolare, partendo da un esempio pratico, abbiamo visto come sia possibile partire da uno stato costituito da architetture basate su scambio disorganizzato di messaggi XML, fino a giungere alla maturità, ad architetture RESTful che traggono pieno vantaggio dalle esperienze maturate con il web.
Si parte con l’individuare correttamente le risorse. Queste sono concettualmente separate dalla rappresentazione restituita al client. In effetti, un server non restituisce mail il database e nemmeno la risorsa così come è memorizzata nel DB (cioè il record), ma una sua adatta rappresentazione attraverso il formato HTML, XML, JSON, coppie (chiave, valore), etc. Poi si tratta di assegnare a tutte le risorse un URI e di utilizzare i link nelle interazioni richiesta/risposta. Successivamente occorre usare stabilmente i metodi standard HTTP. Una volta che un client riceve la rappresentazione di una risorsa, inclusi i metadata associati, questo dispone di un quantitativo sufficiente di informazione per consentirgli di modificare o richiedere l’eliminazione della risorsa al lato server. Tutte queste richieste devono avvenire per mezzo dei metodi standard HTTP: GET, POST, DELETE, etc. Non bisogna dimenticare di prevedere diverse rappresentazioni per la stessa risorsa, e, infine di progettare comunicazioni stateless.
Nel prossimo articolo discuteremo dei modelli di maturità, del fatto che in questo caso forse potrebbe non avere molto senso definire un’architettura di livello 1 del modello di maturità REST. Presenteremo poi una comparazione con interfacce web service e mostreremo anche una serie di limiti propri dei sistemi RESTful.
Riferimenti
[1] Martin Fowler, “Richardson Maturity Model: steps toward the glory of REST”
http://martinfowler.com/articles/richardsonMaturityModel.html
[2] Roy T. Fielding, “Architectural Styles and the Design of Network-based Software Architectures”, PhD thesis, 2000
[3] Roy T. Fielding – Richard N. Taylor, “Principled Design of the Modern Web Architecture “, University of California, Irvine 1, submitted to ESEC/FSE 99 on the 1st March 1999
http://www.ics.uci.edu/~fielding/pubs/fse99_webarch.pdf
[4] Roy T. Fielding et al., “Hypertext Transfer Protocol – HTTP/1.1”, W3C/MIT, June 1999
http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
[5] Stefan Tilkov, “A Brief Introduction to REST”, InfoQ
http://www.infoq.com/articles/rest-introduction