RFC 862 (echo) in PHP, Part 3: From stream to socket

So, in the previous two instalments [1] [2], I've wrangled together a basic echo server over TCP and UDP respectively. Before merging both code so we have a server that fully implement RFC 862, we need a wee bit of...

Refactoring

To explain the main motivation, we need to come back to an annoyance I've mentioned with the TCP server. It's the fact that stream_socket_* PHP functions require a resource type as first parameter and they cannot be used as native type hint value in PHP. So I had to wrap the usage of a socket in its own class to still be benefit of static type security on the high level functions I needed to write.

 final class Connection
    {
        /**
         * @param resource $stream
         */
        public function __construct(public readonly mixed $stream)
        {
            assert(is_resource($this->stream));
        }
    }
public function run(Connection $connection): void
        {
            while (!feof($connection->stream)) {
                $input = fread($connection->stream, self::READ_BUFFER);
                if ($input) {
                    fwrite($connection->stream, $input);
                }
            }
            fclose($connection->stream);
        }

So when I was reading PHP docs for socket_create [3] in the second installment when investigating working with UDP, the following snippets caught my attention:

Version Description
8.0.0 On success, this function returns a Socket instance now; previously, a resource was returned.

and for socket_bind [4]:

Version Description
8.0.0 socket is a Socket instance now; previously, it was a resource.

So it seems the lower level socket_* are doing away with resource, but the higher level stream_socket_* are not updated yet.

So I'm going to refactor the TCP implementation to use the socket_* set.
It allow us to get rid of the Connection class and use the same functions set as for the UDP implementation.

The core functionality (echo) is therefore changing from:

public function run(Connection $connection): void
        {
while (!feof($connection->stream)) {
                $input = fread($connection->stream, self::READ_BUFFER);
                if ($input) {
                    fwrite($connection->stream, $input);
                }
            }
            fclose($connection->stream);

to

public function run(Socket $connection): void
        {
            while ($input = socket_read($connection, self::READ_BUFFER)) {
                socket_write($connection, $input);

The sequence goes like this:

  1. socket_create to create the socket
  2. socket_bind to bind that socket to an address and port
  3. socket_listen that enable listening for incoming connections
  4. socket_accept that accept data
  5. socket_read to read data
  6. socket_write to write data
  7. socket_close

The last four are inside an infinite loop.
The last two loop over incoming data until EOF.

The full result of the refactoring can be seen below:

#!/usr/bin/env php
<?php

    declare(strict_types=1);

    error_reporting(E_ALL ^ E_WARNING);
    pcntl_async_signals(true);
    pcntl_signal(SIGINT, function () {
        echo Status::Stopped->value . PHP_EOL;
        exit(EXIT_OK);
    });
    const EXIT_WITH_ERROR = 1;
    const EXIT_OK = 0;

    enum Status: String
    {
        case Started = "Server has started. Press Ctrl-C to stop it.";
        case Stopped = "Server has stopped.";
        case TimeOut = "Time out, accepting connection again.";
        case Error = "Could not create a server socket. Exiting with error.";
    }

    interface Service
    {
        public function run(Socket $connection): void;
    }

    final class EchoService implements Service
    {
        public const READ_BUFFER = 4096;

        /**
         * Implement the echo functionality of writing to a socket what it read from it
         * @param Socket $connection
         * @return void
         */
        public function run(Socket $connection): void
        {
            while ($input = socket_read($connection, self::READ_BUFFER)) {
                socket_write($connection, $input);
            }
            socket_close($connection);
        }
    }

    final class TCPServer
    {
        /**
         * @param Socket $socket
         */
        private function __construct(public readonly Socket $socket)
        {
        }

        /**
         * Create and bind the socket
         * @param string $address
         * @param int $port
         * @return TCPServer
         */
        public static function create(string $address, int $port): TCPServer
        {
            if (! $socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP)) {
                echo Status::Error->value . PHP_EOL;
                exit(EXIT_WITH_ERROR);
            }
            if (! socket_bind($socket, $address,$port)) {
                echo Status::Error->value . PHP_EOL;
                exit(EXIT_WITH_ERROR);
            }
            echo Status::Started->value . PHP_EOL;
            return new self($socket);
        }

        /**
         * Listen to and continuously accept incoming connection
         * @param TCPServer $server
         * @param Service $service
         * @return void
         */
        public static function listen(TCPServer $server, Service $service): void
        {
            if (! socket_listen($server->socket,1)) {
                echo Status::Error->value . PHP_EOL;
                exit(EXIT_WITH_ERROR);
            }
            while (true) {
                while ($conn = socket_accept($server->socket)) {
                    $service->run($conn);
                }
                echo Status::TimeOut->value . PHP_EOL;
            }
        }
    }

    $server = TCPServer::create("0.0.0.0",7);
    TCPServer::listen($server, new EchoService());

[1] https://www.pommetab.com/2022/12/11/rfc/
[2] https://www.pommetab.com/2022/12/18/rfc-862-in-php-for-fun-part-2/
[3] https://www.php.net/manual/en/function.socket-create.php
[4] https://www.php.net/manual/en/function.socket-bind.php