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:
socket_create
to create the socketsocket_bind
to bind that socket to an address and portsocket_listen
that enable listening for incoming connectionssocket_accept
that accept datasocket_read
to read datasocket_write
to write datasocket_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