PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Wie erstelle ich einen Socket-Server in PHP?

with 21 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 , ,

21 Responses to 'Wie erstelle ich einen Socket-Server in PHP?'

Subscribe to comments with RSS or TrackBack to 'Wie erstelle ich einen Socket-Server in PHP?'.

  1. Hi Michael,

    Eins ist mir noch nicht so ganz klar geworden:

    Warum wird mit if(in_array($this->socket, $read)) überprüft, ob eine neue Verbindung gestartet wurde wenn $read[0] = $this->socket vorher sowieso in Array aufgenommen wurde?

    Fitschi

    25 Aug 09 at 18:18

  2. Hi,

    es hat damit zu tun, dass socket_select() das $read Array verändert, deshalb muss das geprüft werden.

    Man übergibt also dem socket_select() ein $read-Array, das überwacht werden soll. Wenn da nun etwas passiert (sich der Status ändert), gibt socket_select() die Anzahl der Sockets zurück, die sich geändert haben. Im $read-Array steht dann noch der Socket drin, der seinen Status geändert hat.
    Wenn das nun der „Master Socket“ ist, können wir uns sicher sein, dass das ein neuer Client ist.

    In Zeile 63 prüfen wir ja auch nochmal genau das selbe mit allen Client-Sockets. Falls in $read also ein Client-Socket drin steht, hat ein Client was gesendet, und wir müssen dementsprechend reagieren.

    Beim nächsten Durchlauf der Endlosschleife füllen wir wieder das $read-Array mit dem Master-Socket und allen Client-Sockets und es geht von vorn los, socket_select überwacht wieder alle Sockets auf Veränderungen.

    Hoffe habe es halbwegs verständlich erklärt 😉

    http://de2.php.net/socket_select
    „On exit, the arrays are modified to indicate which socket resource actually changed status.“

    Michael Kliewe

    25 Aug 09 at 18:43

  3. Hi Michael,

    Danke für die super Erklärung.

    Was mir allerdings nicht klar geworden ist durch das Manual, ist der unterschied von socket_recv und socket_read?

    Fitschi

    26 Aug 09 at 00:11

  4. Afaik ist ersteres UDP und zweiteres TCP, bin mir aber nicht 100%ig sicher.

    Martin Kuckert

    1 Sep 09 at 10:08

  5. Hallo,

    Ich bin vielleicht etwas „spät“, aber ich habe noch ein bis zwei Fragen zu dem Thema hier…

    Du sagst im oberen Abschnitt:
    „Dieser Chat-Server soll auch gleich als Dienst permanent laufen.“

    Nun hab ich von deinem vorherigen Artikel den Dienst kopiert und ausprobiert. Er läuft soweit. Dann hab ich die Klasse hier kopiert und IN den Dienst hineingepackt. Zuunterst hab ich dann eine neue Instanz erstellt (von SocketChatServer)…
    Was muss ich aber noch machen und wie rufe ich den Chat-Server dann auf, dass ich Chatten kann?

    Gruss

    raphaelniederer

    10 Nov 09 at 13:04

  6. Moment, ich habs rausgefunden. Ich hab noch $object->start() aufgerufen und dann den Dienst gestartet. Via telnet 127.0.0.1 33379 in der Kommandozeile aufgerufen. Alles schön und gut, doch eine Frage bleibt noch offen.
    Deine unterster Screenshot zeigt die „Admin“-Konsole. Die rufst du mit php -q chatserver.php auf. Ich hab das auch versucht (C:\xampp\php\php.exe -q socket.php). Folgende Meldung kommt dann:

    PHP Warning: Xdebug MUST be loaded as a Zend extension in Unknown on line 0
    PHP Warning: Module ‚xdebug‘ already loaded in Unknown on line 0
    bogus args, please use install/uninstall/run

    Kannst mir hier jemand weiterhelfen?

    raphaelniederer

    10 Nov 09 at 14:32

  7. Ich glaube, den Fehler hatte ich auch schonmal. Der kommt daher, dass du die xdebug.dll via
    extension=xdebug.dll
    eingebunden hast. Diese Extension darf aber nur als
    zend_extension_ts=”C:\xampp\php\ext\php_xdebug.dll”
    eingebunden werden, siehe hier beispielsweise:
    http://www.phpgangsta.de/181

    Schau da mal.

    Michael Kliewe

    10 Nov 09 at 21:12

  8. Danke, werd ich bei Gelegenheit mal ausprobieren. Das mit dem Messen (im Artikel) ist auch noch ganz interessant 😀

    Gruss

    raphaelniederer

    10 Nov 09 at 21:57

  9. Ich hab es dann irgendwie zum Laufen gebracht.
    Nun hab ich das Script zu Hause ausprobiert und musste feststellen, dass der Xampp 1.7.2 gar keine php_win32ps.dll (und php_win32scheduler.dll, etc) enthählt.

    Fehlermeldung:
    Fatal error: Call to undefined function win32_create_service() in C:\xampp\htdoc
    s\service\index.php on line 3

    Ich hab mir die nötigen DLL’s geholt und in das xampp\php\ext\ verzeichnis verschoben, wo auch alle anderen DLL’s liegen. Im php.ini-File hab ich dann die Extensions geladen. Nach dem Neustart des Apache-Servers kamen dann folgende Meldungen:

    1. Meldung
    C:\xampp\php\ext\php_win32ps.dll ist entweder nicht für alle Ausführungen unter Windows vorgesehen oder enthält einen Fehler. Installieren Sie das Programm mit dem Originalinstallationsmedien erneut, oder wenden Sie sich an den Systemadministrator oder Softwarelieferanten, um Unterstützung zu erhalten.

    2. Meldung
    PHP Startup: Unable to load dynamic library ‚\xampp\php\ext\php_win32ps.dll‘ – %1 ist keine zulässige Win32-Anwendung.

    ….

    Auszug aus der php.ini
    extension=php_win32ps.dll
    extension=php_win32scheduler.dll
    extension=php_win32service.dll
    extension=php_win32std.dll
    extension=php_xdebug.dll

    Hab die DLL’s von einer anderen Quelle heruntergeladen und wollte den Apache wieder neu starten, da kam dieser Fehler:
    PHP Startup: win32ps: Unable to initialize module
    Module compiled with module API=20071006
    PHP compiled with module API=20090626
    These options need to match

    Und das wieder für jede DLL…

    Kann mir hier jemand sagen, warum win32_create_service() nicht mehr standardmässig „unterstützt“ wird?

    Puh, langer Kommentar, aber der musste einfach sein 😀

    raphaelniederer

    11 Nov 09 at 21:17

  10. Der Xampp 1.7.2 enthält PHP 5.3, dafür gibt es noch keine offiziellen PECL-Extensions für Windows. Habe auch damals schon ewig gesucht.

    Es gibt inoffizielle Binaries hier:
    http://downloads.php.net/pierre/

    Die haben bei mir funktioniert. Ich glaube, die Threadsafe VC6 Version funktionierte unter Apache (VC9 für IIS). Einfach mal probieren.

    Michael Kliewe

    11 Nov 09 at 21:37

  11. Alles schön und gut, doch es gibt von jedem File zwei Versionen. (immer noch mit „nts“ am Ende…)
    Welche brauch ich nun?

    Kleine Zusatzfrage:
    Brauch ich einfach alle win32-DLL’s und php_xdebug.dll?

    Sry, möchte nur ganz sicher gehen… 😀

    raphaelniederer

    11 Nov 09 at 22:12

  12. Das schrieb ich ja, du brauchst glaube ich die TS (Threadsafe VC6 Versionen).

    Nein, du brauchst nicht alle. Nur die, die du benötigst (win32_service.dll für den Dienst und die xdebug.dll für xXDebug)

    Michael Kliewe

    11 Nov 09 at 22:29

  13. Ich glaub ich geh noch drauf! Hab nun die win32_service.dll und xdebug heruntergeladen und in das ext-Verzeichnis kopiert.

    Auszug aus der DLL:
    [PECL]
    extension=php_dbase.dll
    extension=php_fbsql.dll
    extension=php_mime_magic.dll
    extension=php_ming.dll
    extension=php_msql.dll
    extension=php_pdf.dll
    ;extension=php_pdo_oci.dll
    ;extension=php_pdo_oci8.dll

    zend_extension = „C:\xampp\php\ext\php_eaccelerator.dll“
    zend_extension = „C:\xampp\php\ext\php_xdebug.dll“

    extension=php_win32service.dll

    Ich hab auch die Einstellungen, die du in diesem (http://www.phpgangsta.de/181) Artikel hast noch ausprobiert. Ohne Erfolg.
    Weisst du denn auch nicht, was ich alles brauche, um das Script zum Laufen zu bringen?

    Fehlermeldung:
    Xdebug requires Zend Engine API version 220060519.
    The Zend Engine API version 220090626 which is installed, is newer.
    Contact Derick Rethans at http://xdebug.org for a later version of Xdebug.

    long(1073) refcount(2)

    raphaelniederer

    12 Nov 09 at 09:29

  14. Bitte lade nur eine Zend Extension. Du überschreibst dort die Variable.
    Außerdem muß man Xdebug als TS-Zend-Extension laden, siehe http://www.phpgangsta.de/181

    [XDebug]
    ; Only Zend OR (!) XDebug
    zend_extension_ts=”C:\xampp\php\ext\php_xdebug.dll”

    Und lass den eaccelerator erstmal weg. Dort bitte aufpassen. Vielleicht gehts ja dann.

    Michael Kliewe

    12 Nov 09 at 10:33

  15. SUUUUPER! Es hat tatsächlich geklappt! Ich fass es noch einmal kurz zusammen:

    1. Auf http://downloads.php.net/pierre/ die Files php_win32service.dll und php_xdebug.dll herunterladen

    2. Die beiden DLL’s in das \xampp\php\ext\-Verzeichnis kopieren

    3. und die php.ini mit folgenden Zeilen ergänzen:
    – zend_extension_ts = „C:\xampp\php\ext\php_xdebug.dll“
    – extension=php_win32service.dll

    Fertig! So einfach wäre es 😀

    Vielen vielen Dank für deine Hilfe!

    raphaelniederer

    12 Nov 09 at 11:23

  16. Vielen Dank für das Tutorial.

    Der Server läuft bei mir, solang ich ihn mit telnet teste.
    Nun wollte ich eine kleine Java-Anwednung schreiben, welche den Server verwendet, allerdings bekommt der Server gleich nach der erfolgreichen Anmeldung des Clients einen Disconnect vom Client o.O Woran kann das liegen?

    Yss

    6 Mai 13 at 16:07

  17. @Yss Gute Frage, ich würde im Java-Client suchen. Ohne weitere Infos kann ich jedoch nicht helfen.

    Michael Kliewe

    6 Mai 13 at 20:34

  18. Ich komme zwar noch viel später mit meinem Kommentar, aber das Thema wird mit den immer häufiger verwendeten WebSockets immer interessanter! Danke an dieser Stelle für deine Mühe Michael :)

    Ich schreibe zur Zeit eine chat-App für ein OSS-Projekt und beschäftige mich erst seit kurzem mit dieser Technik. Daher meine Frage, ist es irgendwie möglich ohne shell_exec() aus zu kommen, um den Chatserver zu starten. Kann man auch auf andere Weise ein PHP-Script starten und es am laufen halten (ohne nach >meist< 30 Sekunden einen timeout zu erzeugen)?
    Wäre es z.B. möglich eine Socket-Verbindung zum eigenen Script (oder vielleicht gibt es einen externen Anbieter für so etwas) auf zu bauen und auf weitere Verbindungen zu warten? Also sozusagen um den ChatServer am leben zu halten.

    Das Problem ist nämlich besagtes Projekt wird von vielen Usern betrieben, welche nur über einen normalen Webspace verfügen und ihnen somit zumeist nur socket-Verbindungen, aber keine Kommandozeilen-Zugriffe möglich sind.

    Vielleicht kann mir ja jemand ein paar Tipps geben? (im schlimmsten Fall würde mir nur Polling bleiben :/ )

    Grüße André

    André

    12 Nov 13 at 06:09

  19. @Andre

    Du kannst den Server am laufen lassen in dem du die server.php so zum Beispiel startest:

    nice -n 19 nohup php server.php >> tcpServer/startServer.log 2>&1 &

    nice -n 19
    => setzt die Prio runter
    nohup
    => damit kannst du dein Server in den Hintergrund schieben, so darfst du dann auch die Konsole oder die sshConection schließen :)
    >>tcpServer/startServer.log 2>&1
    => bedeutet das du die server.php ausgabe in diese Datei umleitest, 2 ist die Fehlerausgabe und 1 die Standartausgabe, also die Fehlerausgabe dort hin wo die Standartausgabe hinkommt :)
    damit du nicht in nohup verharrst ist das & am Ende von nöten!!! dann brauchst du kein Strg C

    Hier mein startScript:

    #!/bin/bash

    testArgument=$1
    if [ „$testArgument“ = „localhost“ ]
    then
    serverPath=“/opt/lampp/htdocs/html/websockets“
    elif [ „$testArgument“ = „webhost“ ]
    then
    serverPath=“Pfad auf deinem Webhost“
    fi

    cd „$serverPath“

    # Die Verzeichniss und Dateiabfrage sowie das erstellen auslagern!!!

    test -d „tcpServer“
    testDir=$?

    tulpenPort=“$(netstat -tulpen | grep 64440)“
    testPort=$?

    if [ -d „tcpServer“ ]
    then

    echo „Ordner tcpServer ist vorhanden“
    dirReady=0

    else

    echo „Ordner tcpServer muss erstellt werden“

    mkdir „tcpServer“

    if [ -d „tcpServer“ ]
    then

    dirReady=0

    else

    dirReady=1

    fi
    fi

    if [ „$dirReady“ = 0 ]
    then

    echo „Jetzt kanns los gehen“

    serverPidLog=“tcpServer/serverPID.log“

    if [ -f „$serverPidLog“ ]
    then
    echo „serverPID.log ist vorhanden“
    fileReady=“old“
    else
    echo „serverPID.log muss erstellt werden“
    touch „tcpServer/serverPID.log“

    if [ -f „$serverPidLog“ ]
    then
    fileReady=“new“
    else
    fileReady=“error“
    fi
    fi

    if [ „$fileReady“ = „old“ ]
    then
    serverPID=“$(head tcpServer/serverPID.log)“
    testServerPID=$?
    getPID=“$(pidof php)“
    testGetPID=$?

    if [ ${#serverPID} -gt 0 ]
    then
    echo „ProzessID in serverPID.log vorhanden – werde prüfen ob der Prozess mit der ID $serverPID noch aktiv ist“

    if [ „$testGetPID“ = 0 ]
    then

    if [ „$getPID“ -eq „$serverPID“ ]
    then
    echo „der Server läuft noch und die serverPID.log ist synchron“
    else
    echo „der Server läuft noch, aber die PID in der serverPID.log ist veraltet“
    echo „$getPID“ >tcpServer/serverPID.log
    fi
    elif [ „$testGetPID“ = 1 ]
    then
    echo „der Server wird jetzt gestartet und die serverPID.log aktualisiert“

    cd „$serverPath“

    nice -n 19 nohup php server.php >> tcpServer/startServer.log 2>&1 &
    serverPID=“$(pidof php)“
    echo „der Server wurde gestartet und kann über die ProzessID $serverPID bearbeitet werden“
    echo „________________________________“
    echo „——————————–“
    echo „Viel Spaß mit deinem TCP-Server.“
    echo „________________________________“
    echo „$serverPID“ >tcpServer/serverPID.log
    fi

    else
    echo „PID noch nicht vorhanden, werde prüfen ob der Prozess aktiv is“

    if [ „$testGetPID“ = 0 ]
    then
    echo „der Server läuft, werde jetzt die serverPID.log aktualisieren“
    echo „$getPID“ >tcpServer/serverPID.log
    elif [ „$testGetPID“ = 1 ]
    then
    echo „der Server wird jetzt gestarten und die serverPID.log aktualisiert“
    cd „$serverPath“
    nice -n 19 nohup php server.php > tcpServer/startServer.log 2>&1 &
    serverPID=“$(pidof php)“
    echo „der Server wurde gestartet und kann über die ProzessID $serverPID bearbeitet werden“
    echo „$serverPID“ >tcpServer/serverPID.log
    fi

    fi

    elif [ „$fileReady“ = „new“ ]
    then
    if [ „$testPort“ = 1 ]
    then
    cd „$serverPath“
    nice -n 19 nohup php server.php > tcpServer/startServer.log 2>&1 &
    serverPID=“$(pidof php)“
    cd „~“
    echo „der Server wurde gestartet und kann über die ProzessID $serverPID bearbeitet werden“
    echo „$serverPID“ >tcpServer/serverPID.log
    else
    echo „Port ist schon vergeben“
    fi
    elif [ „$fileReady“ = „error“ ]
    then
    echo „Kann die Datei serverPID.log nicht erstellen“
    fi

    else

    echo „hier stimmt etwas ganz und garnicht“

    fi

    svenskanda

    20 Jun 14 at 02:44

  20. ach ja…

    das script musst du natürlich ausführbar machen

    chmod +x setTCPServer

    dann kannst du es aufrufen mit

    ./setTCPServer localhost

    oder

    ./setTCPServer dein webhost

    — die Argumentenabfrage musst du dir selbst zusammen wurschteln

    in der serverPID.log steht die Prozessnummer, falls du mehrere PHP-Anwendungen laufen haben solltest,

    ansonsten einfach

    pidNumber=“$(pidof php)“

    kill „$pidNumber“

    nacht :)

    svenskanda

    20 Jun 14 at 03:08

  21. […] Windows-Rechner wird per PHP ein TCP-Socket geöffnet. Dieser Code basiert auf einem Beispiel von Michael Kliewe. Um Scriptkiddies nicht direkt Zugang zu geben ist etwas (sehr schlechte) […]

Leave a Reply

You can add images to your comment by clicking here.