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();