Die Socket-Erweiterung von PHP - Teil 2

Good Connections
Kommentare

Im ersten Teil dieses zweiteiligen Artikels haben wir die Grundlagen von Sockets und deren konkreten Einsatz in PHP mittels der Socket-Erweiterung vermittelt. Als Praxisbeispiel haben wir einen PHP-basierenden Webserver geschrieben. Leser des ersten Teils werden sich jedoch erinnern, dass der Webserver nur eine einzige Anfrage zur gleichen Zeit verarbeiten konnte. Ein Aspekt, der sich im echten Betrieb eines Webservers negativ auf die Performance auswirkt. Wie man mehrere Verbindungen parallel verarbeiten kann und auch den Problemen des ersten Webservers aus dem Wege gehen kann, möchten wir in diesem zweiten Teil erklären.

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

Anschließend konnte man per Telnet (telnet 192.168.1.1 31337) eine Verbindung zu diesem Server aufbauen und Zeichenketten mittels Tastatur eingeben, die der Server auf der Konsole ausgegeben hat. Mit quit konnte man die Verbindung des Clients beenden, mit shutdown konnte man sogar den Socket-Server herunterfahren. Der zweite Parameter der Funktion socket_listen() kennzeichnet die Anzahl jener Verbindungen, die in einer Warteschlange warten können. Wird die aktuelle Verbindung zum Client beendet, so kann der nächste Client in dieser Warteschlange mit dem Server kommunizieren. 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.

  • 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.

Auch socket_select() kann den Skriptablauf blockieren. Man kann daher als vierten und fünften Parameter optional der Funktion auch noch einen Timeout in Sekunden beziehungsweise Millisekunden angeben. Listing 1

#!/usr/local/bin/php -q
 0) {
if (in_array($socket, $readSocks)) {
// Wenn unser socket immer noch in den lesesockets ist, hat
// ein neuer connect stattgefunden
if (($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 nachricht
echo 'Nachricht von ' . $clientSock . ': ' . $buffer . "n";
}
}
}
}
}
// Socket vor dem beenden schliessen
socket_shutdown($socket);
socket_close($socket);

Im Listing 1 befindet sich der geänderte Socket-Server. Dieser hat sich im Grunde nicht viel gegenüber der ursprünglichen Version geändert. Neben IP-Adresse und Port des Servers definieren wir zu Beginn die drei genannten Arrays (Lese-, Schreib- und Fehlerarray). Da wir in unserem Beispiel nur vom Lesearray Gebrauch machen, können wir die anderen mit NULL initialisieren. Des Weiteren wird ein Array angelegt, in dem sich später die Sockets aller aktuell verbundenen Clients befinden. 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:

Server Software:        PHPHTTPD/1.0 
Server Hostname:        192.168.1.1 
Server Port:            12346 
Document Path:          / 
Document Length:        7 bytes 
Concurrency Level:      1000 
Time taken for tests:   10.090 seconds

Würde man nicht nur eine ganz kleine einfache HTML-Seite ausgeben, würden die Unterschiede noch größer sein, abhängig von der Abarbeitung der Seite. Listing 2

function loop () {

// Read array neu initialisieren
unset($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 verarbeiten
foreach($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 Socketn";
$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 senden
socket_write($clientSocket, $answer, strlen($answer));

// Verbindung zum Client wieder schließen
socket_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;
}

Die Verarbeitung der Verbindungen unseres Webservers findet in der Methode loop() statt (Listing 2). Anstatt hier wie gehabt direkt mittels socket_accept() einen Connected Socket zu erzeugen, verwenden wir die Socket-Arrays. Der Quellcode hierzu (Listing 2) ist analog zu dem Telnet-Beispiel aufgebaut. Die verwendeten Arrays werden hier nicht lokal gehalten, sondern sind Attribute der Klasse bzw. des Objekts. 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.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -