PHP/Zugriff auf Ressourcen mit Socketfunktionen

Aus Mikiwiki
< PHP
Zur Navigation springen Zur Suche springen

Socketprogrammierung

PHP eignet sich gut, um auf alle Ressourcen eines Rechners zuzugreifen. Bekannt sind etwa die Free-Mail-Angebote im Internet, die oft webbrowserbasierte Schnittstellen bereitstellen, sodass die eigenen E-Mails von überall aus mit einem Webbrowser gelesen werden kann. Man ist damit unabhängig von der Benutzung eines besonderen E-Mail-Clients. Die üblichen Protokolle für E-Mail sind POP3 und IMAP4. Am grössten ist das Angebot an POP3-Servern. Die über SMTP empfangene E-Mail wird dann den Clients per POP3 zum Herunterladen zur Verfügung gestellt. Um per Webbrowser auf ein solches POP3-Konto zugreifen zu können, bietet sich PHP an. Benötigt werden Funktionen für die Netzwerkprogrammierung.

Die einfachste Funktion zum Socketzugriff heisst "fsockopen". Der POP3-Server lauscht normalerweise auf Port 110 nach den für ihn bestimmten Befehlen. Die folgende Beispielanwendung zeigt den Ablauf der Anmeldung und Authentifizierung an einem POP3-Server. Das Skript verzichtet auf das Herunterladen und Löschen der E-Mails, kann also auch gefahrlos auf einem "Live"-System arbeiten. Das Skript beginnt mit der Definition zweier Ausnahmen, die der Fehlerverwaltung dienen. Auf den Inhalt kommt es nicht wiert an, weshalb die Klassen selbst leer sind (benötigt werden sie dennoch). Es folgt in der Hauptklasse "Pop3" die Definition des POP3-Servers und des Benutzerkontos, das abgefragt werden soll.

<?php
class SocketException extends Exception { }
class CommandException extends Exception { }
class Pop3
{
  ## Diese drei Werte muessen auf einen POP3-Server eingestellt werden
  const SERVER = "mail.defgh.xx";
  const USER   = "abc@defgh.xx";
  const PASS   = "xyz";

  private $LF = "\r\n";
  private $pop3;
    
  ## Aufbau der Verbindung, die als Handle den Dateifunktionen zur Verfügung
  ## gestellt wird, welche die weitere Kommunikation übernehmen.
  public function __construct()
  {
    ## Für POP3 wird Port 110 benutzt
    $this->pop3 = fsockopen(self::SERVER, 110);
    ## Im Fehlerfall wird eine Ausnahme ausgelöst, die im Haupteil
    ## verarbeitet wird. So werden unkontrollierte Laufzeitfehler vermieden.
    if (!is_resource($this->pop3))
    {
       throw new SocketException("Konnte nicht verbinden");
    }
    ## Mir "GetResponse()" wird immer die Antwort des Servers abgeholt,
    ## damit der vollständige Ablauf des Protokolls erfüllt wird.
    $this->Write('OPEN', $this->GetResponse());
  }

  ## Im Destruktor wird die Verbindung geschlossen, damit keine offenen
  ## Sockets zurückbleiben, die künftige Skriptaufrufe stören könnten.
  public function __destruct()
  {
    fclose($this->pop3);
  }

  ## Für doe Befehle wird die virtuelle "__call"-Methode genutzt, die immer
  ## aufgerufen wird, wenn keine Methode passenden Namens vorhanden ist.
  ## Damit werden auch die für das Beispiel benötigten vier POP3-Befehle
  ## (USER, PASS, STAT, QUIT) gesendet.
  public function __call($command, $param)
  {
    $send = "";
    ## das Senden der Befehle übernimmt die Funktion "fputs". Im Fehlerfall
    ## wird der Laufzeitfehler mit "@" unterdrückt. Die Funktion gibt dann
    ## "FALSE" zurück.
    switch ($command)
    {
      case "SendUser":
        $send = "USER $param[0]{$this->LF}";
        $r    = @fputs($this->pop3, $send);                
        break;
      case "SendPass":
        $send = "PASS $param[0]{$this->LF}";
        $r    = @fputs($this->pop3, $send);
        break;
      case "GetStat":
        $send = "STAT{$this->LF}";
        $r    = @fputs($this->pop3, $send);
        break;
      case "Quit":
        $send = "QUIT{$this->LF}";
        $r    = @fputs($this->pop3, $send);
        break;
      default:
        return;
    }
    ## Wird "FALSE" zurückgegeben, so wird eine Ausnahme vom Typ
    ## "CommandException" ausgelöst.
    if ($r === FALSE)
    {
      throw new CommandException("Befehl $command nicht ausgeführt");
    }
    ## Nach dem Senden des POP3-Befehls wird wieder mit "GetResponse" die
    ## Antwort geholt und mit "$this->Write" für den Benutzer ausgegeben.
    $this->Write($send, $this->GetResponse());
  }

  ## Die Eigenschaften "User" und "Pass" geben die in der Klasse 
  ## versteckten Kontoinformationen aus.
  public function __get($property)
  {
    switch ($property)
    {
      case "User":
        return self::USER;
      case "Pass":
        return self::PASS;
    }
  }

  ## Die Schreibmethode "Write" erzeugt lediglich eine Kontrollausgabe.
  private function Write($strCommand, $strResult) 
  {
    echo "<b>$strCommand</b>: $strResult<br />\n";
  }

  ## Die Methode "GetResponse()" holt mit "gets" wird die Antwort des 
  ## Servers ab. Hier wird davon ausgegangen, dass die Antwort nie 
  ## länger als 1024 Zeichen ist.
  private function GetResponse()
  {
    return fgets($this->pop3, 1024);
  }
}

## Das eigentliche Hauptprogramm, das innerhalb des "try"-Zweigs im 
## Konstruktor die Verbindung aufbaut und dann die vier Befehle 
## nacheinander sendet.
try
{
  $pop3 = new Pop3();
  $pop3->SendUser($pop3->User);
  $pop3->SendPass($pop3->Pass);
  $pop3->GetStat();
  $pop3->Quit();
}
## Auftretende Ausnahmen werden mit "catch" abgefangen und entsorgt.
catch (SocketException $ex)
{
  echo "Socket-Fehler: {$ex->getMessage()}";
}
catch (CommandException $ex)
{
  echo "Kommando-Fehler: {$ex->getMessage()}";
}
?>

OPEN: +OK Hello there.
USER abc@defgh.xx: +OK Password required.
PASS xyz: +OK logged in.
STAT: +OK 0 0
QUIT: +OK Bye-bye.

Mit weiteren Befehlen kann nun mit dem POP3-Postfach gearbeitet werden. "RETR" holt beispielsweise die Nachrichten ab; "DELE" löscht die E-Mails dann endgültig vom Server. Das Beispiel ist zwar primitiv, zeigt aber alle nötigen Techniken. Zuerst sollte man sich natürlich über die Art und Weise der Kommunikation mit dem POP3-Server (oder einem beliebigen anderen Server) im Klaren sein. Die Kommunikation findet über den Austausch einfachster Befehle statt, die Übertragung erfolgt offen im ASCII-Format.

Erweiterte Funktionen beim direkten Zugriff

In Abhängigkeit von der Reaktion des Servers werden zusätzliche Funktionen benötigt, beispielsweise wenn der Server nicht sofort reagiert. Wird ein Port im Polling abgefragt (z. B. mit einer Schleife), so kann die Belastung für Server und Verbindung sehr gross sein. Andererseits kann das Skript womöglich ohne vorliegende Antwort nicht fortgesetzt werden.

Die Funktion "set_socket_blocking" kann zur Kontrolle des Verhaltens eingesetzt werden. Im POP3-Beispiel war das nicht notwendig, da der Server - notfalls mit Fehlermeldung - jede Anfrage beantworten wird. Fällt der Server aber aus, so kann sich das Skript aufhängen, zumindest bis zum Ablauf der Zeitüberschreitung. Mit der folgenden Anweisung wird der nichtgeblockte Modus eingestellt, das Skript läuft so auch ohne Antwort weiter.

$mode = set_socket_blocking($fp, 0);

Folgender Aufruf erzwingt dagegen das Abwarten einer Antwort.

$mode = set_socket_blocking($fp, 1);

Problematisch ist auch die Belastung des Servers durch das andauernde Öffnen und Schliessen der Verbindungen. Auch wenn das ausdrückliche Schliessen mit "fclose" am Ende des Skripts nicht erfolgt, wird PHP die Verbindungen schliessen. Einen Ausweg bieten persistente Verbindungen. Dazu wird einfach die Funktion "fsockopen" durch "pfsockopen" ersetzt.

$fp = pfsockopen("server.tld", "80");

Fehlerbehandlung

Die beiden Socket-Funktionen "fsockopen" und "pfsockopen" können um Elemente der Fehlerbehandlung erweitert werden. Das ist wichtig, da gerade bei Internetverbindungen sehr häufig Fehler auftreten. Der Funktionsaufruf wird dazu wie folgt erweitert. Die Variable "$errno" enthält damit eine Fehlernummer, in "$errdesc" findet sich eine von PHP erzeugte Beschreibung. Der weitere Umgang mit dem Fehler muss nun selbst programmiert werden.

$fp = pfsockopen("server.tld", "80", &$errno, &$errdesc);