Bevor wir auf das zwar praktischere, aber auch etwas komplexere Beispiel des Webservers kommen, möchten wir die Grundlangen erneut anhand eines simplen Telnet-Beispiels veranschaulichen. Der Einfachheit halber haben wir den alten Server mit der Erweiterung _old.php den Quellcodes auf der CD beigelegt. Bei unserem ursprünglichen Beispiel wurde der Server zunächst mit PHP gestartet:
./socketserver_old.php oder aber php -q socketserver_old.php
Bei unserem Telnet-Beispiel bedeutet dies aber Folgendes: Lässt man die erste Verbindung des Clients bestehen und baut parallel dazu eine weitere Verbindung zum Socket-Server auf, so müssten der zweite Client und auch weitere Clients warten. Erst nachdem der erste Client mittels quit beendet wird, kann der nächste Client Textnachrichten an den Server senden, der diese Nachrichten darauf ausgibt.
Parallele Verarbeitung
Neben dem herkömmlichen Weg, einzelne Verbindungsanfragen nacheinander per socket_accept() abzuarbeiten, gibt es noch eine weitere Möglichkeit in PHP, Anfragen zu verarbeiten. Hierbei kommt die Funktion socket_select() zum Einsatz, die es ermöglicht, mehrere Anfragen gleichzeitig zu handeln. Die Funktion selbst überwacht dabei verschiedene Sockets auf Veränderung. Als Parameter nimmt sie mehrere Arrays entgegen, die für einzelne Anfragetypen zur Verfügung stehen. Es werden also Sockets in diesen einzelnen Arrays abgelegt. Erfahrene C-Programmierer werden diese Arrays schnell wiedererkennen, denn in C verwendet man so genannte FD-Sets (File Descriptor Sets) für jeden Anfragetyp anstelle von Arrays. Um aber ein Variablenchaos zu vermeiden, hat man sich entschieden, in PHP Arrays zu verwenden. Folgende Arrays werden im socket_select()-Aufruf benötigt:- read array - Dieses Array enthält nach dem Aufruf von socket_select() nur noch die Socket-Verbindungen, auf denen bei einem Lesevorgang mindestens ein Byte an Daten zurückkam. Sockets, auf denen keine Leseverbindungen stattfanden, werden hieraus entfernt.
- write array - Dieses Array enthält die Socket-Verbindungen, auf denen ein Schreibvorgang stattgefunden hat.
- except array - In diesem Array sind die Sockets enthalten, bei denen während des Lese- beziehungsweise Schreibvorgangs ein Fehler aufgetreten ist.
Listing 1
#!/usr/local/bin/php -q<?php$ipaddress = '192.168.1.1';$port = 31339;// Ressourcen-arrays definieren$readSocks = array();// write und exception sockets werden in diesem script nicht benötigt$writeSocks = null;$exceptSocks = null;// hier kommen alle neuen socket-verbindungen mit den clients hinein$clients = array();// erzeugen des sockets mittels socket_create()$socket = socket_create(AF_INET, SOCK_STREAM, 0);if ($socket === false) {die('Fehler beim erzeugen des Sockets! Grund: ' . socket_strerror($socket) . "\n");}// socket an gegebene IP-Addresse und port binden.if (($ret = socket_bind($socket, $ipaddress, $port)) === false) {die('Fehler beim Binden des Sockets! Grund: ' . socket_strerror($ret) . "\n");}// auf dem socket auf einkommende verbindungen wartenif (($ret = socket_listen($socket, 5)) === false) {die('Fehler beim lauschen auf dem Socket! Grund: ' . socket_strerror($ret) . "\n");}// einkommende Verbindung annehmen.while (true) {// der hauptsocket gehört ebenfalls zu den lesesockets$readSocks = array();$readSocks[] = $socket;foreach($clients as $clientSock) {$readSocks[] = $clientSock;}if (false === ($num_changed = socket_select($readSocks, $writeSocks, $exceptSocks, 0))) {die('Fehler bei socket_select! Grund:' . socket_strerror(socket_last_error()) . "\n");} elseif ($num_changed > 0) {if (in_array($socket, $readSocks)) {// Wenn unser socket immer noch in den lesesockets ist, hat// ein neuer connect stattgefundenif (($newsocket = socket_accept($socket)) === false) {die('Fehler beim akzeptieren der Verbindung! Grund:' . socket_strerror($newsocket) . "\n");} else {// neuen client in die liste aller clienten aufnehmen$clients[] = $newsocket;}}foreach ($clients as $clientSock) {if (in_array($clientSock, $readSocks)) {// Ein Client hat uns daten geschickt.$buffer = socket_read($clientSock, 2048);if (!$buffer = trim($buffer)) {// Verbindung wurde von der gegenpartei beendet.socket_shutdown($clientSock);socket_close($clientSock);$key_index = array_keys(array_keys($clients), $clientSock);array_splice($clients,$key_index[0]);break;}if ($buffer == 'quit') {// Verbindung möchte von der gegenstelle beendet werden.socket_shutdown($clientSock);socket_close($clientSock);$key_index = array_keys(array_keys($clients), $clientSock);array_splice($clients,$key_index[0]);break;} else {// Normale nachrichtecho 'Nachricht von ' . $clientSock . ': ' . $buffer . "\n";}}}}}// Socket vor dem beenden schliessensocket_shutdown($socket);socket_close($socket);
Anschließend erzeugen wir unseren Hauptsocket, binden ihn an die vorher definierte Adresse und warten auf Verbindungen mittels socket_listen(). In der Endlosschleife ist nun die wichtige Änderung gegenüber der Vorversion: Das Annehmen einkommender Verbindungen wird jetzt anders verarbeitet. Dem Anfang der Schleife schenken wir etwas später Beachtung. Interessant ist zunächst der Aufruf von socket_select(), der unsere drei Arrays verändert. Nach dem Aufruf befinden sich nur noch jene Sockets in den Arrays, bei denen eine Veränderung stattgefunden hat. socket_select() liefert bei einem Fehler FALSE zurück. Im Erfolgsfall wird jedoch ein Integerwert zurückgegeben, der die Anzahl der geänderten Verbindungen kennzeichnet. Da der Rückgabewert entweder vom Typ boolean - also TRUE oder FALSE - oder aber vom Typ integer sein kann, benutzen wir in der If-Anweisung den typenbasierenden Operator ===. Mit in_array() wird geprüft, ob sich unser ursprünglicher Socket ($socket) in dem Lesearray befindet. In diesem Spezialfall, wurde eine neue Verbindung zum Server geöffenet. Also verwenden wir hier socket_accept() und erhalten in $newsocket unseren Connected Socket. Diesen neuen Socket fügen wir dem Array $clients hinzu. Darin werden alle Sockets jener Clients gehalten, zu denen unser Server eine offene Verbindung hat.
Im zweiten Teil der Schleife gehen wir das Array $clients mit einer foreach-Schleife durch, also alle aktuellen Verbindungen. Da wir aber nur jene Sockets verarbeiten möchten, wo eine Veränderung stattgefunden hat, prüfen wir nach, ob sich der jeweils aktuelle $clientSock in der foreach-Schleife auch in dem Lesearray befindet. In diesem sind dank socket_select() nur noch veränderte Sockets vorhanden.
Wird nun eine Verbindung beendet oder übermittelt der Client die Zeichenfolge quit, so beenden wir den Socket wie gehabt mittels socket_shutdown() und socket_close(). Weiterhin lassen wir uns mit array_keys() den Index dieses Sockets in $key_index zurückgeben, um das geschlossene Socket aus dem Array der aktuellen Clients zu entfernen. Falls die Verbindung nicht beendet werden soll, geben wir die übermittelte Zeichenfolge an der Konsole aus. Der shutdown-Befehl würde bei diesem neuen Server keinen Sinn machen. Er würde den Socket-Server herunterfahren, was bei paralleler Verarbeitung der Verbindung unerwünscht ist.
Der Anfang der while-Schleife wird nun selbsterklärend. Wir erzeugen hier das Lesearray neu, da es von socket_select() manipuliert wird. Unseren Hauptarray fügen wir ebenfalls dem Lesearray hinzu. Falls dieses später nach dem Aufruf von socket_select() noch vorhanden ist, so kennzeichnet das wie besprochen einen neuen Verbindungsaufbau. Die foreach-Schleife im Anschluss fügt nun noch alle Sockets aus $clients dem Lesearray hinzu. Falls also auf einer der aktuellen Verbindungen eine Aktivität stattgefunden hat, überleben die Sockets dieser Verbindungen den Aufruf von socket_select() und die Kommunikation kann verarbeitet werden.
HTTP Server revisited
Werden auf dem ursprünglichen HTTP Server parallele Verbindungen geöffnet, so kann nur eine verarbeitet werden. Alle übrigen Verbindungen werden verworfen. Um so genannte Concurrency zu testen, kann man beispielsweise das Benchmarktool von Apache ab (Apache Benchmark) verwenden, das auf jedem System vorhanden ist, auf dem der Apache Webserver installiert wurde.Im ersten Durchlauf benutzen wir noch den alten Server, der als class.httpserver_old.php der CD beigelegt ist, und lassen ab (Apache Benchmark) laufen:
ab http://192.168.1.1:12345/ -c 1000 -n 1000
Server Software: PHPHTTPD/1.0Server Hostname: 192.168.1.1Server Port: 12345Document Path: /Document Length: 7 bytesConcurrency Level: 1000Time taken for tests: 62.988 seconds
Server Software: PHPHTTPD/1.0Server Hostname: 192.168.1.1Server Port: 12346Document Path: /Document Length: 7 bytesConcurrency Level: 1000Time taken for tests: 10.090 seconds
Listing 2
function loop () {// Read array neu initialisierenunset($this->read);$this->read = array();$this->read[] = $this->socket;reset($this->clients);foreach ( $this->clients as $clientSocket ) {$this->read[] = $clientSocket;}if (($num_changed = @socket_select($this->read, $this->write, $this->except, 0)) === FALSE ) {echo 'Fehler bei socket_select! Grund: '.socket_strerror(socket_lasterror())."\n";return FALSE;} elseif ( $num_changed > 0 ) {if(in_array($this->socket, $this->read)) {if ( ($clientsocket = socket_accept($this -> socket)) === FALSE ) {echo 'Fehler beim akzeptieren der Verbindung! Grund:'.socket_strerror($clientsocket)."\n";return FALSE;} else {$this->clients[] = $clientsocket;}}// Jeden einzelnen client-request verarbeitenforeach($this->clients as $clientSocket ) {if (in_array($clientSocket, $this->read)) {// Neue nachricht auf dem Socket$buffer = socket_read ( $clientSocket, 1024 );if ( !$buffer = trim($buffer) ) {echo "Fehler beim lesen auf dem Socket\n";$key_index = array_keys(array_keys($this->clients), $clientSocket);array_splice($this->clients,$key_index[0]);socket_close($clientSocket);return TRUE;} else {// Nachricht verarbeiten und antwort erzeugen$answer = $this->handleClientRequest( $buffer );// Daten an den Client-Socket sendensocket_write($clientSocket, $answer, strlen($answer));// Verbindung zum Client wieder schließensocket_close($clientSocket);// Client aus dem clients-array entfernen$key_index = array_keys(array_keys($this->clients), $clientSocket);array_splice($this->clients,$key_index[0]);}}}}return TRUE;}
Zunächst wird das Lesearray reinitialisiert. Anschließend werden Hauptsocket und alle aktuellen Verbindungssockets dem Lesearray hinzugefügt. socket_select() filtert jene Sockets heraus, auf denen seit dem letzten Schleifendurchgang keine Veränderung stattgefunden hat. Falls eine neue Verbindung stattgefunden hat, wird dies dem Clients-Array hinzugefügt, worauf all die Sockets darin durchlaufen werden. Unsere HTTP-Anfrage wird wie auch in der ersten Version von der Methode handleClientRequest() verarbeitet. Das angeforderte Dokument wird mit socket_write() an den jeweiligen Socket geschrieben. Anders aber als bei unserem Telnet-Beispiel schließen wir darauf explizit die Verbindung, da HTTP ein zustandsloses Protokoll ist (vergleiche Teil 1 des Artikels). Um das aktuelle Socket beim nächsten Durchgang nicht mehr unter den aktiven Client-Sockets zu haben, entfernen wir mithilfe von array_splice() diesen Socket aus dem $clients-Array. Dies geschieht wiederum analog zum Telnet-Beispiel.
Fazit
Ab der Version 4.3 kann die Socket-Erweiterung eingesetzt werden, ohne API-Änderungen befürchten zu müssen. Wie unser zweiteiliger Artikel zeigte, kann man nicht nur einfache Verbindungen mit PHP meistern. Die Implementierung der FD-Sets in ihrer speziellen Form in PHP macht es leicht, auch jenen Anforderungen gerecht zu werden, bei denen die gleichzeitige Verarbeitung von Sockets notwendig ist.Verglichen mit unserem ersten Teil, war dieser zweite Teil etwas theoretischer und abstrakter. Wir hoffen dennoch, dass er Spaß gemacht hat und lehrreich war.
Marcel Beerta arbeitet als Softwareentwickler für die Innovative Software AG in Frankfurt/Main. Er ist Spezialist bei der Erstellung von Applikationen im Bereich des Online Brokering. Zudem ist er Entwickler des freien Chats Mazen's PHP Chat (www.mazenphp.de), der auf den Socket-Erweiterungen von PHP basiert.
Richard Samar (richard-samar.de) ist freiberuflicher Berater und Softwarearchitekt und wohnt in der Nähe von Frankfurt/Main. Seit vielen Jahren ist er PHP-Enthusiast und der deutschsprachigen PHP Community wohl bekannt. Er ist Koautor von PHP de Luxe (php-de-luxe.de) - dem momentan aktuellsten deutschen PHP-Buch für fortgeschrittene PHP-Programmierung.
Links
- [1] Socket-Funktionen von PHP: de.php.net/sockets
- [2] UNIX Socket FAQ: www.developerweb.net/sock-faq/
- [3] RFC zu HTTP 1.0: www.ietf.org/rfc/rfc1945.txt
- [4] RFC zu HTTP 1.1: www.ietf.org/rfc/rfc2616.txt
- [5] Multifunktionaler Webserver basierend auf PHPs Socket-Erweiterungen: nanoweb.si.kz/
- [6] Ein weiterer Webserver in PHP: daniel.lorch.cc/projects/phpserv/




