Implementierung des Apache Jserv-Protokolls in PHP

Java Servlets integrieren
Kommentare

Wer beim Lesen der Überschrift einen innerlichen Widerspruch verspürt, tut dies nicht zu Unrecht. PHP und Java Servlets sind für viele Entwickler unvereinbar. In der Tat werden zwar in vielen Projekten beide Technologien verwendet, jedoch kann hier von einer Verknüpfung von PHP mit Java Servlets keine Rede sein. Der Grund: ein einfaches Kommunikationsproblem. Wie man PHP die Kommunikation mit einem Servlet-Container wie Tomcat 3.3 lehrt und sich dabei eine Welt voller neuer Möglichkeiten eröffnet, soll dieser Artikel zeigen.

Sie verwalten den Inhalt der Website, stellen die Online-Community bereit oder führen die vom Vertreter angepriesenen, nur mit diesem Java-Produkt realisierbaren Tätigkeiten aus: Java Servlets. Für teures Geld angeschafft oder vom nun in der Karibik verweilenden Java-Entwickler erschaffen, machen sie sich starr auf dem Webserver breit. Für den PHP-Entwickler, welcher über der Bytecode-Datei vorerst nur die Nase rümpft, kaum von Interesse. Doch spätestens wenn sich die Silhouette des Vorgesetzen hinter dem Monitor abzeichnet und bei der Teamkonferenz die Wörter Anpassung und PHP erschreckend oft fallen, wird er vor einem Berg von Problemen stehen. Die vom Servlet ausgelieferten Seiten sollen angepasst werden, am besten noch etwas dynamisch. An anderer Stelle sollen die Seiten in ein bestehendes, in PHP verfasstes Tool integriert werden – und das natürlich mit PHP.

<img src="http://cdn.sandsmedia.com/ps/onlineartikel/pspic/picture_file//52/import3e34fc5fba080.tif<
Abb.1: Zusammenspiel von Webserver und Servlet-Container

Ein besonderes Augenmerk soll nun auf die verwendeten Kommunikationsprotokolle zwischen den einzelnen Stationen gerichtet werden: Browser und Apache kommunizieren über HTTP/1.1. Mod_jk und Servlet-Container beherrschen mehrere Protokolle. In vielen Konfigurationen kommt das Apache Jserv Protocol (AJP) zum Einsatz. Tomcat 3.3 unterstützt die Versionen 1.2 (AJPv12) und 1.3 (AJPv13). Das Apache-Modul mod_jk hat also die Hauptaufgabe, die eingehenden Requests vom HTTP-Protokoll auf ein für Tomcat verständliches Protokoll zu übersetzen.

<img src="http://cdn.sandsmedia.com/ps/onlineartikel/pspic/picture_file//52/import3e34fc5fc3c97.tif<
Abb.2: Aufbau eines AJPv12 Request-Packets

Das erste Byte eines Packets enthält den ASCII-Wert 1. Mit diesem Wert wird dem Servlet-Container signalisiert, dass es sich um ein Request-Packet handelt. Die darauf folgenden 25 Request-Felder beinhalten sämtliche Informationen über die im Servlet aufzurufende URL, den Client und den Webserver, der den Request als erstes entgegen genommen hat (siehe Kasten Request Felder eines AJPv12-Packets). Dem Leser sollten einige der Feldnamen aus dem täglichen PHP-Alltag bereits bekannt sein. Das Füllen der Felder mit den korrekten Werten nimmt selbst die mod_jk-Implementierung nicht allzu genau, diese setzt einige der Felder auf null. Feld 16 ist in den mod_jk-Quellen nur mit einem leeren Kommentar geschmückt und wird getrost auf null gesetzt.

Request-Felder eines AJPv12 Packets Vor jedem Request-Feld befinden sich zwei Bytes zur Angabe der Länge des Feldes. Ist ein Feld leer (null), so werden die beiden Bytes mit dem ASCII-Wert 255 gefüllt. Feld Nr. Feld Name 1     Request Method* 2     Servlet Zone 3     Servlet 4     Server Name* 5     Document Root 6     Path Info 7     Path Translated 8     Query String* 9     Remote Address* 10   Remote Host* 11   Remote User* 12   Auth Type* 13   Server Port* 14   Request Method* 15   Request URI* 16   unbenannt (null) 17   Script Name 18   Server Name* 19   Server Port* 20   Server Protocol* 21   Server Signature 22   Server Software* 23   Jserv Route* 24   AJPv12 Compatibility (empty String) 25   AJPv12 Compatibility (empty String) * Felder die von mod_jk ausgefüllt werden.

Die darauf folgenden Request-Attribute werden häufig dazu verwendet, um einem Servlet benutzerdefinierte Parameter zu übermitteln. Konfigurationsparameter und Pfade finden hier einen guten Platz, auch Session-IDs und andere Werte, die für das Servlet von Interesse sein könnten, können hier eingefügt werden. Auch diese Daten müssen noch kodiert werden: Nach einer ASCII 5 folgt zuerst der Attributname, dann der Attributwert, jeweils inklusive der gewohnten zwei Byte Längenangabe. Im Gegensatz zu den vorangegangenen Request-Variablen ist die Anzahl der Request-Attribute frei. Der nächste Abschnitt eines Packets besteht aus den Request Headern, welche vom Browser zum Webserver übermittelt wurden. An dieser Stelle seien Cookies, User_Agent, und browserspezifische Informationen wie das Accept-Encoding-Feld genannt. Auch hier ist die Kodierung im bekannten Schema: Jeder Request-Header wird durch eine ASCII 3 eingeleitet, dicht gefolgt von Headername und Headerwert samt Längenangabe. Damit ist der Kopf des Packets fertig. Signalisiert wird dies durch eine abschließende ASCII 4. Für den Packet-Rumpf bleibt noch die so genannte Request-Entitiy. Das sind Daten, die bei einem POST-Request übermittelt werden. Der Browser sendet diese ähnlich wie bei einem GET-Request in dem Format name=wert&name2=wert2. Ebenso wird die Request-Entitiy einfach an das Packet angehängt.

function ajpv12AddString($str) {
if ($this->debug) echo $str." n";
if ($str != null) {
$bufferlen   = strlen($str);
$str_return  = chr(($bufferlen >> 8) & 0xff);
$str_return .= chr($bufferlen & 0xff);
$str_return .= $str;
}
else {
$str_return = chr(255).chr(255);
}
return $str_return;
}

Damit sind bereits alle Vorraussetzungen geschaffen, um mit dem Zusammenbauen eines AJPv12-Packets anzufangen. Das Packet ist in PHP nichts weiter als ein String, die einzelnen Datenfelder und Markierungsbytes werden mittels Stringkonkatenation nacheinander angehängt. Im Quellcode ist zu sehen, wie nach dem ersten Markierungsbyte die 25 Request-Variablen eingefügt werden. Die Werte für die Request-Variablen erhalten wir aus dem PHP-Array $_SERVER, welches in der Klasse Ajp in das Attribut serverVars kopiert wurde. Natürlich müssen noch einige der Werte angepasst werden: Die Request-URI aus $_SERVER entspricht ja noch nicht der URI, welche im Servlet aufgerufen werden soll. Die oben beschriebene function ajpv12AddString() sorgt dafür, dass alle Felder korrekt mit Längeninformation verpackt werden (siehe Listing 1). Listing 1

// Mark 4 = end of headers
$packet .= chr(4);

// REQUEST ENTITY
if (count($this->postVars) > 0) {
foreach ($this->postVars as $key => $value) {
$entity[] = $key."=".$value;
}
$packet .= join("&", $entity);
}

Das fertig geschnürte Packet soll nun zum Servlet-Container gelangen. Dazu wird per fsockopen() eine Socketverbindung zum Host und Port des Containers aufgebaut, fwrite kümmert sich um die Übermittlung. Sobald das Packet gesendet wurde, kommt auch schon die Antwort vom Servlet. Eingangs wurde erwähnt, dass das Format der Antwort identisch mit der Ausgabe eines gewöhnlichen Webservers ist. Wir müssen also mit einigen Headerzeilen, gefolgt von einer leeren Zeile und der eigentlichen HTML-Seite, rechnen. Die Trennung von Headerzeilen und HTML-Quellcode ist dank der Leerzeile recht einfach: Bis wir die erste Leerzeile erhalten, behandeln wir die Daten als Header, nach der Leerzeile kommt die Seite (vergleiche Listing 2). Listing 2

// TRANSMIT PACKET
$this->ajpReply = "";
$this->ajpHeaders = array();

$fp = fsockopen($this->serverHost, $this->serverPort, $errno, $errstr, 2);
if (!$fp) {
echo "$errstr ($errno) n";
}
else {
fwrite($fp, $packet, strlen($packet));

// GET REPLY
$head = true;
while (!feof($fp)) {
$line = fgets($fp, 1024);
if ($head && ($line == "rn")) {
$head = false;
} else if ($head) {
$this->ajpHeaders[] = $line;
} else {
$this->ajpReply .= $line;
}
}
fclose($fp);
}
}

Das Protokoll ist fertig. Jedoch gestaltet sich die Anwendung noch etwas kompliziert. Gesucht ist daher eine einfache Möglichkeit, eine Methode, um genau zu sein, die das Senden von Requests an das Servlet erleichtert. Eine Art include() für Servlets. Die Methode soll einen String mit der URL eines Servlets übergeben bekommen, den Request an das Servlet senden und die Antwort aus- bzw. zurückgeben. Bei der Implementierung erwies sich die PHP-Funktion parse_url() als nützliches Werkzeug. Ein wenig knifflig wird das korrekte Besetzen der Request-Variablen, schließlich steuern diese, welche URL mit welchen Parametern im Servlet aufgerufen werden soll. Einen Teil der Request-Variablen kann man direkt aus $_SERVER übernehmen. Andere, wie z.B. REQUEST_URI, müssen für das Servlet neu zusammengebaut werden. Ist auch diese Hürde überwunden, folgt der Aufruf der ajpv12Send()-Methode. Die Antwort vom Servlet befindet sich nun in der Membervariable ajpReply (siehe Listing 3). Listing 3

function ajpv12Request($url) {
$request = parse_url($url);
if (isset($request["host"])) {
$this->serverHost = $request["host"];
}
if (isset($request["port"])) {
$this->serverPort = $request["port"];
}
$ap = explode("/", $request["path"]);
$this->servlet = array_pop($ap);
$this->servletZone = join("/",$ap);

$this->serverVars["REQUEST_URI"] = $request["path"];
if (isset($request["query"])) {
$this->serverVars["REQUEST_URI"] .= "?".$request["query"];
$this->serverVars["QUERY_STRING"] = $request["query"];
} else if (isset($this->serverVars["QUERY_STRING"])) {
$this->serverVars["REQUEST_URI"] .= "?".$this->serverVars["QUERY_STRING"];
}
$this->ajpv12Send();
return $this->ajpReply;
}

Als Test soll ein einfacher Request auf ein Beispielservlet generiert werden. Das Servlet liefert als Ausgabe einige der übermittelten Request-Informationen. In Listing 4 (ajpRequest.php) befindet sich das dazugehörige PHP-Skript. Zusätzlich wird ein Request-Attribut übermittelt, welches ebenfalls anschließend vom Servlet ausgegeben wird. Neben der eigentlichen Seite gibt das PHP-Skript auch alle Response-Header des Servlets aus. Das soll an dieser Stelle natürlich nur der Veranschaulichung dienen. Im normalen Einsatz sollte man die Header mit der PHP-Funktion Header() an den Browser weiterleiten. Listing 4

<img src="http://cdn.sandsmedia.com/ps/onlineartikel/pspic/picture_file//52/import3e34fc5fcb1c1.tif<
Abb.3: Screenshot der Ausgabe des PHP-Skripts

Der große Vorteil der AJP-Kommunikation mit PHP besteht darin, dass sowohl Request als auch die Antwort des Servlets durch PHP laufen. Es ist dadurch möglich, Servlets komplett hinter PHP zu verstecken. Besonders lassen sich Servlets auflockern, die auf der Website unersetzlich sind und von keiner Seite mehr gewartet werden können, da weder Support noch Quellcode verfügbar sind. So können z.B. die IP-Adresse des Clients geändert, Cookies entfernt oder dazu geschnitten oder gar GET-Requests in POST-Requests umgewandelt werden. Man kann mod_jk ganz durch PHP ersetzen und alle Servlet-Requests durch PHP laufen lassen. Dies wiederum freut den Webserver: ein Modul weniger. Aber auch für zukünftige Projekte kann die direkte Kommunikation hilfreich sein. In dem Request-Packet ist reichlich Platz für anwendungsspezifische Daten, das ermöglicht einen Datenaustausch zwischen PHP und Java Servlet, der über die üblichen POST– und GET-Variablen hinausgeht. So können Backends als Java Servlet implementiert werden, während die Frontends in PHP handlicher bleiben.

AJPv13

In dieser Implementierung wurde das AJPv12-Protokoll behandelt – was ist eigentlich mit AJPv13? Wie die höhere Versionsnummer vermuten lässt, handelt es sich um eine Weiterentwicklung von AJPv12. Die wesentlichen Neuerungen sind persistente Socketverbindungen, ein überarbeitetes Packet-Design und die daraus resultierende bessere Performance. Auch SSL kam bei AJPv13 nicht zu kurz. Eine Verwendung des AJPv13-Protokolls ist vor allem dann notwendig, wenn man es mit Tomcat-Versionen größer 3.3 zu tun hat. Im Gegensatz zu AJPv12 stehen hier im Netz ausreichend Dokumentationen bereit, die die Portierung etwas erleichtern. Jedoch dürfte es bei AJPv13 mit 150 Zeilen PHP-Quellcode nicht getan sein. Martin Franz studiert Informatik an der Universität Passau. Er ist unter martin@franz63.de erreichbar.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -