PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Archive for the ‘chat server’ tag

Wie erstelle ich einen Socket-Server in PHP?

with 23 comments

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:

chatserver2

chatserver1

chatserver3

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 😉

Written by Michael Kliewe

August 24th, 2009 at 9:04 pm

Posted in PHP

Tagged with , ,