Archive for the ‘chat server’ tag
Wie erstelle ich einen Socket-Server in PHP?
In einem meiner letzten Artikel über Windows-Dienste habe ich ja bereits angesprochen, dass auch Dienste aller Art in PHP realisiert werden können. In jenem Artikel erwähnte ich auch, dass es bereits Webserver, FTP-Server, DNS-Server etc. in PHP gibt.
Heute zeige ich euch, wie man das machen kann. Grundsätzlich haben all diese Dienste gemeinsam, dass sie auf einem TCP-Port auf Verbindungen warten, und dort im Verbindungsfall Befehle entgegennehmen, Aktionen durchführen und Daten zurückliefern können.
Hier möchte ich einen kleinen Chat-Server erstellen, mit dem man sich verbinden kann, und mit allen anderen verbundenen Clients chatten kann. Dieser Chat-Server soll auch gleich als Dienst permanent laufen.
Dazu benötigen wir sogenannte Socket-Funktionen, die PHP seit Version 4.3 bietet. Die entsprechende Extension ist bereits in PHP enthalten, muss aber evtl. in der php.ini noch aktiviert werden:
extension=php_sockets.dll
Dann kann es auch schon loslegen. Das grobe Konzept: Wir erstellen ein Socket und lassen diesen auf Port 33380 lauschen. Wenn eine Verbindung reinkommt, begrüßen wir den neuen Benutzer und fügen ihn einem Array hinzu. Sollte sich ein weitere Benutzer verbinden, tun wir natürlich das selbe. Schreibt ein Benutzer irgendetwas, wird es an alle anderen Benutzer broadcasted. Es soll auch Spezial-Kommandos geben: mit „exit“ oder „quit“ trennt sich der Benutzer vom Server. Mit „term“ stoppt er den Chatserver.
Genug geschwatzt, hier der Code der Klasse:
class SocketChatServer { private $address = '0.0.0.0'; // 0.0.0.0 means all available interfaces private $port = 33379; // the TCP port that should be used private $maxClients = 10; private $clients; private $socket; public function __construct() { // Set time limit to indefinite execution set_time_limit(0); error_reporting(E_ALL ^ E_NOTICE); } public function start() { // Create a TCP Stream socket $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // Bind the socket to an address/port socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1); socket_bind($this->socket, $this->address, $this->port); // Start listening for connections socket_listen($this->socket, $this->maxClients); $this->clients = array('0' => array('socket' => $this->socket)); while (true) { // Setup clients listen socket for reading $read[0] = $this->socket; for($i=1; $i<count($this->clients)+1; ++$i) { if($this->clients[$i] != NULL) { $read[$i+1] = $this->clients[$i]['socket']; } } // Set up a blocking call to socket_select() $ready = socket_select($read, $write = NULL, $except = NULL, $tv_sec = NULL); /* if a new connection is being made add it to the client array */ if(in_array($this->socket, $read)) { for($i=1; $i < $this->maxClients+1; ++$i) { if(!isset($this->clients[$i])) { $this->clients[$i]['socket'] = socket_accept($this->socket); socket_getpeername($this->clients[$i]['socket'], $ip); $this->clients[$i]['ipaddy'] = $ip; socket_write($this->clients[$i]['socket'], 'Welcome to my Custom Socket Server'."\r\n"); socket_write($this->clients[$i]['socket'], 'There are '.(count($this->clients) - 1).' client(s) connected to this server.'."\r\n"); $this->log("New client #$i connected: " . $this->clients[$i]['ipaddy']); break; } elseif($i == $this->maxClients - 1) { $this->log('Too many Clients connected!'); } if($ready < 1) { continue; } } } // If a client is trying to write - handle it now for($i=1; $i<$this->maxClients+1; ++$i) { if(in_array($this->clients[$i]['socket'], $read)) { $data = @socket_read($this->clients[$i]['socket'], 1024, PHP_NORMAL_READ); if($data === FALSE) { unset($this->clients[$i]); $this->log('Client disconnected!'); continue; } $data = trim($data); if(!empty($data)) { switch ($data) { case 'exit': case 'quit': socket_write($this->clients[$i]['socket'], "Thanks for trying my Custom Socket Server, Goodbye.\r\n"); $this->log("Client #$i is exiting"); unset($this->clients[$i]); continue; case 'term': // first write a message to all connected clients for($j=1; $j < $this->maxClients+1; ++$j) { if(isset($this->clients[$j]['socket'])) { if($this->clients[$j]['socket'] != $this->socket) { socket_write($this->clients[$j]['socket'], "Server will be shut down now...\r\n"); } } } // Close the master sockets, server termination requested socket_close($this->socket); $this->log("Terminated server (requested by client #$i)"); exit; default: for($j=1; $j < $this->maxClients+1; ++$j) { if(isset($this->clients[$j]['socket'])) { if(($this->clients[$j]['socket'] != $this->clients[$i]['socket']) && ($this->clients[$j]['socket'] != $this->socket)) { $this->log($this->clients[$i]['ipaddy'] . ' is sending a message to ' . $this->clients[$j]['ipaddy'] . '!'); socket_write($this->clients[$j]['socket'], '[' . $this->clients[$i]['ipaddy'] . '] says: ' . $data . "\r\n"); } } } break(2); } } } } } // end while } private function log($msg) { // instead of echoing to console we could write this to a database or a textfile echo "[".date('Y-m-d H:i:s')."] " . $msg . "\r\n"; } }
Dieser Code funktioniert sowohl unter Windows als auch unter Linux und kann dort als Dienst installiert werden.
Hier einige Screenshots von Clients und vom Server:
Man kann natürlich noch leicht weitere Kommandos implementieren, wie zB ein kleiner Login für Admins, die Liste aller Clients anzeigen, einzelne Clients kicken, die Konfiguration ändern (dann müßte man sie in eine .ini auslagern und neu laden können) usw.
Man könnte auch eine Admin-Webseite implementieren, d.h. wenn man sich mit einem Browser auf den Port verbindet, erkennt das der Dienst und bietet eine html-Oberfläche mit diverse Admin-Funktionen.
Es gibt viele Dinge, die man so realisieren kann. Beispielsweise ein Online-Multiplayer-Spiel in der Konsole oder sogar mit grafischer Java-Oberfläche, eine eigene Steuerung für seinen Server im Keller, einen eigenen Mailserver, ein Backend für Flash-Casual-Games (wenn HTTP zuviel Overhead hat oder zu langsam ist) und und und.
Wie PHP bei einer sehr großer Anzahl von Verbindungen skaliert weiß ich nicht, kann ja jemand von euch mal ausprobieren 😉