Treffen mit J(a)son

Web-2.0-Anwendungen mit JSON und JSON-RPC

Arne Blankerts

Web 2.0 ist in – auch wenn kaum einer weiß, wo genau Web 2.0 wirklich anfängt. Abseits des ganzen Marketinghypes bieten AJAX und Co jedoch erstaunlich einleuchtende Vorteile in der Frontend-Entwicklung. Bleibt nur eine Frage: Wie kommuniziert man effektiv mit dem Server?

Wer mit AJAX arbeitet, setzt bisher zumeist auf XML: Nicht umsonst steht das X in “AJAX”, dem asynchronen JavaScript in Verbindung mit XML (englisch: Asynchronous JavaScript And XML), für eben dieses. So praktisch XML für den Austausch von reinen Daten oder HTML-Fragmenten ist, so umständlich ist die Übergabe komplexer Objekte. Auch das im Gegensatz zu seiner Bezeichnung gar nicht so einfache “SOAP” (Simple Object Access Protocol) macht das Leben nur für den Client einfach, der auf ein WSDL zurückgreifen kann. Zu der aufwändigeren Entwicklung der Serverseite gesellt sich zudem das Problem, dass nur wenige Browser nativ mit einem SOAP-basierten Web Service Kontakt aufnehmen können. Für Web-2.0-Anwendungen also eher ungeeignet. Mit JSON, der “JavaScript Object Notation” (was in seiner Kurzform wie der englische Name “Jason” ausgesprochen wird), gibt es jedoch eine flexible und leistungsfähige Form zur Übertragung von Objekten mit nur marginalem Overhead, den alle JavaScript-fähigen Browser direkt unterstützen. Bevor wir uns allerdings weiter mit JSON und dem darauf aufbauenden Protokoll JSON-RPC (Remote Procedure Call) beschäftigen, brauchen wir die für AJAX gewohnte Grundlage: das XMLHttpRequest-Objekt.

Grundlagen

Wie den meisten Lesern wohl bereits bekannt sein dürfte, brauchen wir um ein derartiges Objekt zu erzeugen eine Browser-Weiche, denn für den Internet Explorer müssen wir eine Extrawurst in den Code integrieren. Da Microsoft es vorgezogen hat, das XMLHttpRequest-Objekt vor der Version 7 seines Browsers nur via ActiveX zu implementieren und sich zudem der ActiveX-Bezeichner auch noch von Version zu Version ändert, muss der in Listing 1 verschachtelte try-catch-Block zur Anwendung kommen, will man auch ältere Versionen des IE nicht vom Besuch der Seite abhalten.





//




hallo welt

Mozilla/Firefox, Safari und Opera machen es einem da deutlich einfacher: für alle gilt dieselbe Syntax. Nachdem wir jetzt ein hoffentlich funktionsfähiges Grundgerüst besitzen, wenden wir uns der Übertragung der Daten und Strukturen zu. JSON selbst ist dabei eigentlich nur JavaScript-Quelltext in “komprimierter” Form. Komprimiert deshalb, weil die kürzestmögliche Schreibweise, mit der Objekte, Arrays und andere Variablen in JavaScript syntaktisch notiert werden können, hier Anwendung findet. Listing 2 zeigt ein Beispiel, wie so ein Objekt im JSON-Format aussehen kann. Das nach einem einfachen eval()-Aufruf in JavaScript erzeugte Objekt besitzt das Property result, das seinerseits ein Objekt mit den vier weiteren Properties hostnamestatusinfo und gruppen besitzt. Bei den ersten beiden sowie den Properties des neuerlichen Kind-objekts info handelt es sich um Strings, das Property gruppen definiert ein Array mit zwei Elementen.

{ "result": {
"hostname":"localhost",
"status":"Erfolgreich",
"info": {
"UID":"1",
"USERNAME":"demo",
"PASSWD":"demo"
},
"gruppen": ["1","10"]
}
}

Verwandlung im Backend

Da die meisten Backendsprachen wie z.B. PHP selbst natürlich kein JavaScript verstehen – auch wenn man dies zum Teil über Erweiterungen nachrüsten könnte – bedarf es einer Umwandlung von JSON-Quelltext in native Objekte und Variablen. Neben der Möglichkeit, dies pur in PHP zu lösen, wie es die JSON-Implementierung des Zend-Frameworks und die JSON-Klasse von Michael Migurski im PEAR-Projekt tun (Listing 3 und 4), gibt es im PECL-Repository seit einiger Zeit auch eine C-basierte Implementierung: ext/json setzt dabei auf die aktuelle JSON-Spezifikation auf und ist nach Angaben des Autors bis zu 270-mal schneller als eine in purem PHP geschriebene Lösung (Listing 5). Ab der Version 5.2.0 von PHP wird diese Erweiterung standardmäßig im Lieferumfang enthalten sein.

Wichtig zu beachten ist, dass, auch wenn alle Implementierungen grundsätzlich natürlich das gleiche machen und die Aufruf-Syntax nahezu identisch ist, die Herangehensweise der einzelnen Lösungen beim Dekodieren sich dennoch unterscheidet. Erzeugt Zend standardmäßig ein assoziatives Array, legen die PECL-Erweiterung und die PEAR-Klasse per Default ein Objekt an. UmZend_Json davon zu überzeugen, dass als Rückgabe ein Objekt gewünscht wird, muss man durch Angabe des Wunschtyps (Zend_Json::TYPE_OBJECT) als zweiten Parameter also erst etwas nachhelfen. Einen zweiten Parameter versteht indes auch die PECL-Erweiterung: Übergibt man hier abweichend von der Standardeinstellung ein boolesches true, wird anstelle eines Objekts ein Array mit assoziativen Keys generiert. Die PEAR-Implementierung spart sich solche Flexibilität beim Aufruf und erwartet stattdessen, dass eine entsprechende Einstellung (SERVICES_JSON_LOOSE_TYPE) bei der Initialisierung der Serviceklasse vorgenommen bzw. dem Konstruktor übergeben wird, liefert aber, wie bereits erwähnt, in der Standardeinstellung ein Objekt zurück.

property;

// Neuen Wert setzen..
$phpObjekt->property='Guten Tag!';

// .. und kodieren
$encoded=Zend_Json::encode($phpObjekt);

// JSON-String ausgeben
echo "n$encoded";
// JSON-Quelltext
$json='{"property":"hallo welt"}';

// Objekt erzeugen
$jsonService = new Service_JSON();

// Dekodieren
$phpObjekt=$jsonService->decode($json);

// Dump
echo $phpObjekt->property;

// Neuen Wert setzen..
$phpObjekt->property='Guten Tag!';

// ... und neu kodieren
$encoded=$jsonServer->encode($phpObjekt);

// JSON-String ausgeben
echo "n$encoded";
// JSON-Quelltext
$json='{"property":"hallo welt"}';

// Dekodieren
$phpObjekt=json_decode($json);

// Dump
echo $phpObjekt->property;

// Neuen Wert setzen..
$phpObjekt->property='Guten Tag!';

// ... und neu kodieren
$encoded=json_encode($phpObjekt);

// JSON-String ausgeben
echo "n$encoded";

Im Vollbesitz unserer geistigen Kräfte, sprich, allem was wir zur Verarbeitung an und für sich brauchen, fehlt uns jetzt nur noch eine standardisierte Kommunikation mit dem Server. Natürlich könnten wir hier auch unsere eigene Aufruflogik entwickeln, aber da proprietäre Lösungen zum einen unschön und zum anderen fast immer mit erheblichem Mehraufwand zur Überbrückung von Inkompatibilitäten verbunden sind, sollte von dieser Idee schnellstens Abstand genommen werden. Ganz besonders dann, wenn es doch bereits einen zwar noch recht jungen, dafür aber immerhin existierenden Standard gibt: JSON-RPC. Idealerweise wird über die POST-Methode ein standardisiertes JSON-Objekt an den Server übergeben und von diesem nach Ausführung der gewünschten Funktion zurückgeliefert.

[ header = Seite 2: JSON-RPC-Objekte ]

JSON-RPC-Objekte Der JSON-RPC-Standard legt zurzeit genau zwei Objekte zur Kommunikation fest, jeweils eines zum Aufruf einer Funktion und eines für die Antwort. Die unterhalb vonparams (Aufruf) bzw. result und error (Antwort) definierten Elemente stehen dem Entwickler vollständig zur freien Gestaltung offen. Um standardkonform zu arbeiten, sollten sie jedoch für den Fall, dass sie nicht benötigt werden, explizit auf NULL gesetzt werden. Fehlt bei einem Funktionsaufruf das Property id, so spricht der Standard von einer “Notification”, was einem Funktionsaufruf ohne Rückgabewert in PHP gleichkommt. Da eine HTTP-Anfrage jedoch immer etwas zurückliefern sollte, dürfte das bewusste Auslassen der ID im Webumfeld nur in den seltensten Fällen Sinn machen.

method

Name der aufzurufenden Funktion

params

Array mit Parametern für die Funktion

id

Eindeutige ID zur Zuordnung der späteren Antwort, auf NULL zu setzen, wenn keine Antwort erwartet/gewünscht wird

result

Ergebnis-Objekt oder NULL bei Fehler

error

Fehler-Objekt oder NULL, wenn kein Fehler

id

Die ID der Anfrage (siehe Funktionsaufruf)

Der Aufbau dieser Objekte ist sehr übersichtlich und schlicht gehalten (siehe Kasten “JSON-RPC-Objekte”), beinhaltet aber alles Notwendige, um eine reibungslose Kommunikation sicherzustellen. Ganz im Sinne der Wiederverwendbarkeit von Quellcode zeigt Listing 7a daher eine generische Implementierung eines einfachen JSON-RPC-Servers unter Verwendung der PECL-Extension und in PHP-5-Notation. Natürlich ließe sich ein derartiger Service auch mit älteren PHP-Versionen realisieren, die neuen Funktionen in der aktuellen PHP-Serie machen das Leben jedoch um einiges leichter.

method) || empty($payload->method)) {
throw new Exception('Keine Methode definiert...');
}

// Wir verwenden :: als Trenner zwischen Klasse und Methode...
if (strpos($payload->method,'::')===false) {
throw new Exception('Keine Klasse definiert...');
}

// JSON-Methode in Zielklasse und Methode aufspalten
list($class,$method)=explode('::',$payload->method,2);

// Klasse instanzieren - das Laden übernimmt __autoload()
$obj = new $class();
if (!$obj) {
throw new Exception("Fehler beim Instanziieren der Klasse '$class'");
}

// Kennt unsere Klasse die gewünschte Methode?
if (!is_callable( array($obj,$method))) {
throw new Exception("Die Methode '$method' existiert nicht in der Klasse '$class'");
}

// Methode aufrufen, ggfs. vorhandene Parameter weiterreichen ...
if ( !call_user_func_array(array($obj, $method),$payload->params) ) {
throw new Exception("Die Ausführung der Methode '$method' ist fehlgeschlagen!");
}

// Das Ergebnis aus dem Puffer im result-Property sichern
$response->result=$obj->buffer;

} catch (Exception $e) {

// Irgendwas ist schiefgelaufen ..

// Fehler-Objekt vorbereiten ..
$err=new stdClass;

// Daten aus der Exception übernehmen
$err->message = $e->getMessage();
$err->line = $e->getLine();
$err->file = $e->getFile();
$err->trace = $e->getTraceAsString();

// und das Error-Property setzen
$response->error=$err;
}

// Request-ID durchschleifen
$response->id=$payload->id;

// Gzip-Kompression aktivieren
ob_start("ob_gzhandler");

// Und unsere JSON-Antwort ausgeben
echo json_encode($response);

?>

Da JSON-RPC zurzeit noch keinerlei Vorgabe zur Behandlung von klassenbasierten Aufrufen  vorschreibt, verwenden wir einfach den aus der OOP bekannten Operator :: zur Trennung zwischen Klasse und Methode. Der Server-Code teilt daher den via method-Property übergebenen Funktionsnamen entsprechend auf, lädt – dank _autoload für uns automatisch – die benötigte Klasse nach und instanziiert diese. Ist die gewünschte Methode in der Klasse vorhanden und auch aufrufbar, führen wir den Funktionsaufruf durch, wobei die ggfs. von JSON-RPC gelieferten Parameter durchgereicht werden. Sollte bei diesem ganzen Procedere etwas schief laufen, fangen wir die geworfenen Exceptions in einer JSON-RPC verträglichen Weise ab: Es wird ein Error-Objekt erzeugt und die aus der Exception bekannten Properties und Methoden zur Informationsgewinnung aufgerufen bzw. eingebunden. Auch ohne großen Aufwand lassen sich so schnell und effektiv kleine oder große Web Services aufsetzen. Um das Ganze etwas zu verdeutlichen, werden wir uns das anhand einer kleinen Galerie noch einmal näher anschauen. In einem bewusst einfach gehaltenen HTML-Dokument (Listing 6a) befinden sich neben den für AJAX notwendigen Skripten eigentlich nur noch zwei Objekte, die von Interesse sind: das Bild (id: image) und die Überschrift (id:desc).

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">



<!-- http://json.org stringify script -->








Willkommen.

Willkommen


[ zurück ] [ weiter ]

Um die in JavaScript erzeugten Objekte serialisieren zu können, greifen wir auf die Stringify-Methode der JSON-Klasse zurück, deren Quelltext man kostenfrei von json.org beziehen und verwenden kann. Will der geneigte Besucher unserer kleinen Galerie nun zum nächsten Bild wechseln, erfragen wir uns via JSON-RPC vom Server die für uns relevanten Informationen (Listing 6b), in diesem Fall den Dateinamen und eine kurze Beschreibung des Bildes.

// xmlHttpRequest-Objektinstanz
var xhttp=null;
var Position=-1;

// onLoad-Funktion
function init(){
[ ... siehe Listing 1 ... ]
}

function gallery_callback() {

if (xhttp.readyState==4) {
var res=eval('('+xhttp.responseText+')');
if (res.error) {
alert(res.error.message);
return;
}

document.getElementById('desc').innerHTML=res.result.desc;
document.getElementById('image').alt=res.result.desc;
document.getElementById('image').src=res.result.src;

Position=res.result.position;

}
}

function prevImage() {
return loadImage(Position - 1);
}

function nextImage() {
return loadImage(Position + 1);
}

function loadImage(Ziel) {
if (!xhttp) return false;

// onReadyStateChange Handler
xhttp.onreadystatechange=gallery_callback;

// Request-Objekt zusammenbauen
var request={
method: 'jsonGallery::getImage',
params: [Ziel],
id: new Date().getTime()
};

// JSON enkodieren
var payload=JSON.stringify(request);

// asynchronen GET Request vorbereiten und abschicken
xhttp.open('POST', 'server.php', true);
xhttp.send(payload);

return true;

}

Die Rückantwort des Servers liegt natürlich im JSON-Quelltext vor, sodass wir alles mit einem einfachen eval() in reale JavaScript-Variablen umwandeln und unsere HTML-Elemente aktualisieren können. Doch Vorsicht: Stammen die Daten aus einer nicht vertrauenswürdigen Quelle, sollte anstelle von eval() auf die parse()-Methode der JSON-Klasse zurückgegriffen werden, damit sichergestellt ist, dass keine XSS-Angriffe durchgeführt werden können. Die einzige von außen via JSON-RPC aufrufbare Methode unseres Backend-Moduls (Listing 7b) ist schnell erklärt, es handelt sich um die Funktion getImage: Basierend auf der übergebenen Index-Position wird ein dynamisch erzeugtes Objekt mit den Daten des Bildes sowie der neuen Position zunächst im Puffer zwischengespeichert und später an den Client übertragen. Ist unter der angegebenen Index-Position kein Eintrag vorhanden, teilen wir dies dem Benutzer mit, wenngleich dies erst als Fehlermeldung im Frontend sichtbar wird.

imageList=Array();
$this->imageList[]=Array('file' => 'pic1.png', 'desc' => 'JSON Beispiel Gallerie');
$this->imageList[]=Array('file' => 'pic2.png', 'desc' => 'Das 2te Bild');
$this->imageList[]=Array('file' => 'pic3.png', 'desc' => 'Alle guten Dinge...');

// Puffer vorbereiten
$this->buffer = new stdClass;

}

// Die durch JSON-RPC aufzurufende Methode
public function getImage($pos) {

// $pos validieren
if ($pos = count($this->imageList)) {
throw new Exception("'$pos' ist ungültig");
}

// Ergebnis-Objekt befüllen
$this->buffer->position=$pos;
$this->buffer->src=$this->imageList[$pos]['file'];
$this->buffer->desc=$this->imageList[$pos]['desc'];

return true;
}

} // class

?>

Zukunft

Auch wenn JavaScript natürlich alles andere als neu ist und folglich auch seine syntaktischen Möglichkeiten seit langem bekannt sind, bietet sich JSON als Übertragungsformat neben XML geradezu an. Im Zusammenspiel mit dem noch jungen JSON-RPC wird dies, angetrieben vom Marketinghype um Web 2.0, den etablierten Web-Services-Formaten XML-RPC und SOAP sicher einiges an Marktanteil abnehmen. Die Tatsache, dass es Überlegungen gibt, PHP standardmäßig mit Unterstützung zur En- und Dekodierung von JSON-Quellcode auszuliefern, sowie die rasant zunehmende Zahl an Frameworks mit JSON Support sprechen eine deutliche Sprache. Auch die Anzahl der im Netz verfügbaren Web Services auf JSON-Basis nimmt zu: So hat beispielsweise Google seit einiger Zeit ebenfalls JSON im Programm, wenn auch leider bisher mit einem zum Standard inkompatiblen RPC-Ansatz. Zurzeit befindet sich im Übrigen die Version 1.1 von JSON-RPC in Planung. Wer sich nach diesem Artikel daran beteiligen möchte, ist herzlich eingeladen. Der Workingdraft zur neuen Version enthält schon einige sehr interessante Neuerungen und Verbesserungen, sodass diese Fassung wohl den endgültigen und webweiten Durchbruch dieses Protokolls sicherstellen wird.

Problemfall “Assoziative Arrays” Die Behandlung der in PHP gerne verwendeten assoziativen Arrays stellt ein Problem dar: Aus Sicht von JavaScript gibt es zwischen einem assoziativen Array und einem Objekt kaum einen Unterschied, sodass ein Zugriff auf Objekt-Properties auch über die sonst für Arrays gewohnte Schreibweise (objekt[‚property‘]) möglich ist. Beim Serialisieren in JSON-Quelltext hingegen fällt das System auf die Nase, da zumindest der JSON-Encoder von json.org bei einer Variablen des Typs Array von einem indexbasierten Element ausgeht, folglich array.length auswertet und als Wert 0 zurückgeliefert bekommt. Die “Länge” eines assoziativen Arrays ist in JavaScript schlicht nicht definiert. Auf JavaScript-Seite lässt sich das Ganze leicht auflösen: einfach ein Objekt anstelle des Arrays verwenden und die Welt ist wieder in Ordnung. Doch leider sieht PHP das natürlich anders, Objekte sind halt eben doch keine Arrays. Wer PHP 5.1 einsetzt, kann dies durch das Verwenden der durch die SPL-Erweiterung eingeführten Iteratoren, im Speziellen des Array-Objekts, etwas entschärfen oder dem JSON-Decoder mitteilen, dass anstelle von Objekten immer assoziative Arrays erzeugt werden sollen. Für die Zukunft, gerade wenn man seine Programmierung objektorientiert gestaltet, sollte jedoch mit der von JSON beim Decode-Vorgang verwendeten stdClass gearbeitet und zumindest für die Übergabe von Daten von und nach JavaScript auf assoziative Arrays verzichtet werden.

Geschrieben von
Arne Blankerts
Arne Blankerts löst IT-Probleme lange bevor viele Firmen überhaupt merken, dass sie sie haben. Seine Themenschwerpunkte sind IT-Sicherheit, Performanz und Ausfallsicherheit von Infrastrukturen, denen er sich mit fast magischer Intuition und Lösungen widmet, die unverkennbar seine Handschrift tragen. Weltweit vertrauen Unternehmen auf seine Konzepte und seine Linux-basierten Systemarchitekturen.
Kommentare

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *


+ 2 = neun

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>