Skip to content

How to implement a socket listener in PHPΒΆ

PHP provides socket functions and is able to fork threads. This allows to build a socket listener which creates child processes to handle incoming connections like in the following example.

You can connect to port 4711 TCP which will create a server process to handle incoming messages. Then you can send any kind of message up to 1 MiB. All messages will be acknowledged with a generic response. When a client sends the word "close" at the beginning of a messsage, the socket will be closed by the server process.

This example also demonstrates the use of a signal handler, so the process can always be stopped using SIGHUP (or pressing ^C in a console).

#!/usr/bin/php
<?php
class Service
{
    const PORT = 4711;
    const SOCKET_TIMEOUT_SECONDS = 600;

    protected $input;
    protected $output;
    protected $run;

    /**
     * Handle signals
     */
    public function signalHandler($signal, $info): void
    {
        if (SIGCHLD === $signal) {
            // Clean up child processes
            $pid = pcntl_waitpid(-1, $status, WNOHANG);
            while ($pid > 0) {
                $exitCode = pcntl_wexitstatus($status);
                $pid = pcntl_waitpid(-1, $status, WNOHANG);
            }
        } else {
            $this->run = false;
        }
    }

    /**
     * Main run loop
     */
    public function run(): int
    {
        pcntl_signal(SIGTERM, [$this, 'signalHandler']);
        pcntl_signal(SIGHUP, [$this, 'signalHandler']);
        pcntl_signal(SIGINT, [$this, 'signalHandler']);
        pcntl_signal(SIGCHLD, [$this, 'signalHandler']);

        // Create socket
        printf("Creating socket for incoming connections on port %d.\n", self::PORT);
        $socket = @socket_create_listen(self::PORT);
        if (!$socket) {
            printf("Error: Could not create socket for port %d.\n", self::PORT);
            return 1; 
        }

        // Main loop to handle incoming connections
        $this->run = true;
        declare(ticks = 1) {
            while ($this->run) {
                $listRead = [$socket];
                $listWrite = null;
                $listExcept = null;
                $socketRead = @socket_select($listRead, $listWrite, $listExcept, 60);
                if ($socketRead) {
                    $connection = socket_accept($socket);
                    $this->handleClient($connection);
                }
            }
        }
        printf("Shutting down socket.\n");
        socket_shutdown($socket);
        socket_close($socket);

        return 0;
    }

    /**
     * Handle client connection
     */
    protected function handleClient($connection)
    {
        $pid = pcntl_fork();
        if ($pid == -1) {
            // Fork did not succeed!
            printf("Error: could not fork for client handling.\n");

            // Usually this also means it makes no sense to continue, so we end the listener here
            $this->run = false;
        } else if ($pid) {
            // This is the parent process
        } else {
            // This is the child process
            printf("Created new worker for client connection.\n");
            $this->clientWorker($connection, self::SOCKET_TIMEOUT_SECONDS);
            exit(0);
        }
    }

    /**
     * Client worker
     */
    protected function clientWorker($connection, $timeout)
    {
        socket_getpeername($connection, $remoteAddress, $remotePort);
        printf("Incoming connection from %s.\n", $remoteAddress);

        $runtime = 0;
        $done = false;
        declare(ticks = 1)
        {
            while ($this->run && !$done && $runtime < $timeout) {
                $listRead = [$connection];
                $listWrite = null;
                $listExcept = null;
                $socketRead = @socket_select($listRead, $listWrite, $listExcept, 1);
                if ($socketRead !== false) {
                    printf("Incoming message, reading up to 1 MiB of data.\n");
                    $message = socket_read($connection, 1048576);

                    printf("Sending response\n");
                    $response = "response\n";
                    if (socket_write($connection, $response, strlen($response)) === false) {
                        printf("Error: Could not send response.\n");
                        $done = true;
                    }

                    if (str_starts_with($message, 'close')) {
                        printf("Received \"close\", closing connection.\n");
                        $done = true;
                    }
                } else {
                    $done = true;
                }
                $timeout++;
            }
        }

        printf("Shutting down socket.\n");
        @socket_shutdown($connection);
        socket_close($connection);
    }
}

$service = new Service();
return $service->run();