URL dieses Artikels:

zu Ausgabe: 02.2003
Java Servlets integrieren
Implementierung des Apache Jserv-Protokolls in PHP
von Martin Franz
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.

Ein erster Versuch
Beim Versuch, die Ausgabeseiten des Servlets mittels include(http://...) in eine PHP-Seite einzubinden, ergeben sich unangenehme Nebeneffekte: include() erzeugt einen Sub-Request, in dem sich das PHP-Skript beim Servlet mit der Adresse localhost und dem User-Agent PHP/x.x.x meldet. Cookies und andere HTTP-Header werden aus der Ausgabe des Servlets herausgeschnitten. Für die meisten Anwendungen keine adäquate Lösung. Auch wenn man dieses Problem mit einem manuellen Request mittels fsockopen auf den Webserver umgeht, bleibt immer noch die IP-Adresse des Webservers als Client-Adresse im Servlet. Um einen neuen Lösungsansatz zu finden, hilft ein Blick auf die Funktionsweise einer Webserver/Servlet Container-Kombination.

Zusammenspiel von Webserver und Servlet Container
Ein Request eines Browsers an ein Servlet durchläuft in der hier verwendeten Konfiguration als erstes den Apache Webserver. Dort wird er von mod_jk abgefangen und anstatt im Dateisystem nach einer auf die Request-URL passenden Datei zu suchen, wird dieses Apache-Modul eine Socketverbindung zum Servlet-Container, in diesem Falle ein Tomcat 3.3, aufbauen. Über diesen Socket gelangt der Request zum Servlet-Container. Die Antwort, identisch mit der Ausgabe eines normalen Webservers, wird durch die Socketverbindung an mod_jk zurück geschickt und gelangt von dort aus zum Browser.


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.

Die Lösung: AJP
Gelingt es, eines der Apache Jserv-Protokolle in PHP zu übersetzen, kann man sich den Schritt über mod_jk sparen und direkt mit dem Servlet-Container kommunizieren. Damit bekommt man die Ausgabe des Servlets bequem als String in das ausführende PHP-Skript und hat ferner die Möglichkeit, den Request vor dem Weiterleiten an den Servlet-Container abzuändern. Im Folgenden soll zur Veranschaulichung wegen der geringeren Komplexität die AJP Version 1.2 der aktuellen Version 1.3 vorgezogen werden. Die Quellen des AJPv12-Protokolls findet man in der Source-Distribution des jakarta-tomcat-connectors-Packages. Nach intensiver Studie des C-Quellcodes ergibt sich der in Abbildung 2 dargestellte Aufbau eines AJPv12 Request-Packets.


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.

Die Implementierung in PHP
Der Theorie soll nun Quellcode folgen. Für die Implementierung des Protokolls bat sich ein objektorientierter Ansatz an. In der Klasse Ajp sollen alle nötigen Attribute und Methoden zusammengefasst werden, die für die Protokollkommunikation notwendig sind. Bei der Beschreibung des Packet-Inhalts fiel auf, dass die in das Packet eingefügten Werte (Strings) immer von einer zwei Byte großen Längenangabe eingeleitet werden. Es lohnt sich daher, diese Funktionalität in eine eigene Methode zu kapseln. function ajpv12AddString ist ein möglicher Weg, dies zu realisieren. Die Methode erhält den zu kodierenden String, ermittelt dessen Länge und packt diese Information in die ersten beiden Bytes des Rückgabestrings. Sollte als Parameter null übergeben werden, so werden nur die von der Packet-Struktur geforderten zwei mit ASCII 255 gefüllten Bytes zurückgegeben:

function ajpv12AddString($str) {
if ($this->debug) echo $str."<br>\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

// PACKET START | Mark 1
$packet = chr(1);
$packet .= $this->ajpv12AddString($this->serverVars["REQUEST_METHOD"]);
$packet .= $this->ajpv12AddString($this->servletZone);
$packet .= $this->ajpv12AddString($this->servlet);
$packet .= $this->ajpv12AddString($this->serverVars["SERVER_NAME"]);
$packet .= $this->ajpv12AddString($this->serverVars["DOCUMENT_ROOT"]);
$packet .= $this->ajpv12AddString(null); // PATH_INFO
$packet .= $this->ajpv12AddString(null); // PATH_TRANSLATED
$packet .= $this->ajpv12AddString($this->serverVars["QUERY_STRING"]);
$packet .= $this->ajpv12AddString($this->serverVars["REMOTE_ADDR"]);
$packet .= $this->ajpv12AddString($this->serverVars["REMOTE_HOST"]);
$packet .= $this->ajpv12AddString($this->serverVars["REMOTE_USER"]);
$packet .= $this->ajpv12AddString($this->serverVars["AUTH_TYPE"]);
$packet .= $this->ajpv12AddString($this->serverVars["SERVER_PORT"]);
$packet .= $this->ajpv12AddString($this->serverVars["REQUEST_METHOD"]);
$packet .= $this->ajpv12AddString($this->serverVars["REQUEST_URI"]);
$packet .= $this->ajpv12AddString(null); // ???
$packet .= $this->ajpv12AddString(null); // SCRIPT_NAME
$packet .= $this->ajpv12AddString($this->serverVars["SERVER_NAME"]);
$packet .= $this->ajpv12AddString($this->serverVars["SERVER_PORT"]);
$packet .= $this->ajpv12AddString($this->serverVars["SERVER_PROTOCOL"]);
$packet .= $this->ajpv12AddString($this->serverVars["SERVER_SIGNATURE"]);
$packet .= $this->ajpv12AddString($this->serverVars["SERVER_SOFTWARE"]);
$packet .= $this->ajpv12AddString(null); // JSERV_ROUTE
$packet .= $this->ajpv12AddString(""); // AJPV12 Compatibility
$packet .= $this->ajpv12AddString(""); // AJPV12 Compatibility

Nach den Request-Variablen folgen, falls vorhanden, die Request-Attribute. Zur Repräsentation der Attribute eignet sich ein assoziativer Array. Für jedes Attribut muss nun der ASCII Wert 5, gefolgt von dem Key-Value-Paar eingefügt werden. Der PHP-Quellcode ist denkbar einfach:

// AJP ENV VARS | Mark 5
if (count($this->requestAttributes) > 0) {
foreach ($this->requestAttributes as $key => $value) {
$packet .= chr(5).$this->ajpv12AddString($key);
$packet .= $this->ajpv12AddString($value);
}
}

Analog kann man mit den Request-Headern verfahren. Die Header, von der PHP-Funktion getallheaders() bereits praktisch in einem assoziativen Array verpackt, werden nach dem Markierungsbyte mit dem ASCII-Wert 3 als Key-Value-Paar an das Packet angehängt:

// REQUEST HEADERS  | Mark 3
if (count($this->requestHeaders) > 0) {
foreach ($this->requestHeaders as $key => $value) {
$packet .= chr(3).$this->ajpv12AddString($key);
$packet .= $this->ajpv12AddString($value);
}
}

Damit ist das Packet fast fertig. Um das Ende des Packet-Kopfes zu signalisieren, wird noch ein Markierungsbyte mit dem ASCII-Wert 4 eingefügt. Zu guter letzt wird die Request Entity angehängt. Wieder traversieren wir ein assoziatives Array, diesmal die $_POST-Variablen. Auf Markierungsbytes kann man ab hier verzichten, die Request-Entity soll lediglich das Format name1=wert1&name2=wert2 erhalten:

// 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);
}

Ab die Post!
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)<br>\n";
}
else {
fwrite($fp, $packet, strlen($packet));

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

Include() für Servlets
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;
}

Eine Beispielanwendung
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

<?php
include("../lib/class.Ajpv12.php");

$ajp = new Ajp();
$ajp->requestAttributes = array("testAttribute" => "test");
$request = "ajpv12://localhost:8007/examples/servlet/RequestInfo?testParam=value";
$ret = $ajp->ajpv12Request($request);

echo join("<br>\n", $ajp->ajpHeaders);
echo $ret;
?>


Abb.3: Screenshot der Ausgabe des PHP-Skripts

In Abbildung 3 ist die Ausgabe des Servlets zu erkennen. Das Servlet hat alle nötigen Request-Informationen bekommen und hat nicht gemerkt, dass der Request von PHP weiter gegeben wurde - anstatt vom vertrauten Kollegen mod_jk oder einem anderen Connector aus dem Indianerlager. An den ausgegebenen Variablen kann man sehen, dass das Servlet korrekte Werte für die IP-Adresse des Browsers bekommen hat. Wir erinnern uns daran, dass dieses Problem auftritt, wenn die Servlet-Seite per Include eingebunden wird. Durch die direkte Verbindung zum Servlet-Container war kein Sub-Request notwendig, was dem Webserver wiederum etwas Arbeit erspart hat.

Möglichkeiten
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.

Links

© 2004 Software & Support Verlag GmbH. Vervielfältigung nur mit Genehmigung des Verlags. Fragen?