Friday, 1 February 2008

PHP: Getting signals through to blocking socket_accept

Have you ever written a sockets server in PHP and wanted to use the blocking option but found you where unable to kill the process off cleanly and instead had to resort to kill -9 or even worse set blocking to false and set a delay in the main event loop?

Well there is a way to use blocking and be able to shutdown the process cleanly. socket_accept() blocking may ignore general signals, but it does not ignore pcntl alarms. An alarm fired every second or so will break you out of a blocking call to socket_accept() (with a warning) and allow your normal flow of logic to detect a shutdown signal i.e. sigterm.


Example code



<?php
declare(ticks = 1);

class test
{
private $shutdown = FALSE;

function sigTerm()
{
$this->shutdown = TRUE;
}

function sigAlarm()
{
pcntl_alarm(1);
}

public function doit()
{
$socket = socket_create(AF_INET, SOCK_STREAM, 0);
socket_bind($socket,'127.0.0.1', 5555);
socket_listen($socket);

socket_set_block($socket);

pcntl_signal(SIGTERM, array(&$this, "sigTerm"));
pcntl_signal(SIGALRM, array(&$this, "sigAlarm"));
pcntl_alarm(1);

while(1)
{
if($c = @socket_accept($socket))
{
echo "Got a connection\n";
socket_close($c);
}
else
{
echo "Alarm has broken us out of socket_accept blocking, ";

if($this->shutdown)
{
echo "and it is time to shutdown now Dave.\n";
return;
}

echo "but it is not time to shutdown yet Dave.\n";
}
}
}
}

$test = new test;
$test->doit();



And Finally

To test the code simply telnet 127.0.0.1 5555 to see it handling connections. Then when you're done a simple killall php should do the trick (just be sure you don't kill any other php process).

I should point out that socket_stream_accept() will respond to signals without the need for an alarm. So if you can, it may be worth going with the stream functions over the cardinal socket functions. (Edit: 04/02/2008) However the stream function response to signals seems a little erratic so I would use an alarm anyway.


UPDATE - 5th March 2008

I was stung by a nasty little oversight that you should know. When reading from a socket, any interrupt (read: alarm) will cause the read to abort. To solve this, after successfully establishing a connection with socket_accept() be sure to run pcntl_alarm(0) then socket_read() and then pcntl_alarm(1) again.

1 comment:

Verty said...

Hi Andrew,

Many thanks for this great fix!

I've been messing around with blocking sockets and signals in PHP for some time now. This is the most elegant workaround yet for this PHP "feature".

What I find strange however is that all signals are blocked except this one.

One more thing, could you post this on php.net somewhere? It would make a nice example on the socket_accept function page.

Cheers!