Yet again, I apologise for taking a long time to respond. However I wanted to post an update as I think I have now got this sorted out.
This may not be the most elegant solution on the planet, but it does seem to work, and I would hope that it may help out someone who's after the same thing.
Ok, so just to summarise first of all, the solution I am about to propose is to allow Sleep() calls in a PHP socket chat room/mp gaming server which will pause only one room at a time and allow all other rooms, and the main thread, to continue as normal. The code displayed here has been greatly stripped down to show only the necessary for this. If you'd like any assistance with any other aspect of the server, please contact me by PM.
This is just one of the many possible solutions. With the benefit of hindsight, I can see that possibly having each room created as it's own sub process would probably have been the smartest thing to do. However I have not gone down that route. In general, room processing is done by the parent process. Ok, so first off, this is vital as SneakyImp correctly pointed out:
declare(ticks = 1);
Now, the Room class:
class Room {
protected $cpid; // child process ID
protected $playerlist ;
public function __construct() {
$this->_playerlist = new PlayerList ();
$this->_cpid = 0;
}
public function removePlayerNick ($nick) {
$this->_playerlist->removePlayerNick($nick);
if ($this->_cpid != 0) {
posix_kill($this->_cpid, SIGTERM);
pcntl_waitpid ($this->_cpid, $status);
$this->_cpid = 0;
}
}
public function addPlayer (Player $p) {
$this->_playerlist->addPlayer ($p);
if ($this->_cpid > 0) {
// there is already a child process, so kill it, return the process ID and reset the room's CPID
posix_kill($this->_cpid, SIGTERM);
pcntl_waitpid ($this->_cpid, $status);
// this is absolutely vital (as I've discovered), otherwise calling waitpid later is unpredictable
$this->_cpid = 0;
}
$cpid = pcntl_fork(); // Fork off a new child process
echo 'CPID: '.$cpid;
if ($cpid > 0) { // parent, so record new CPID and do whatever parent processing is required
$this->_cpid = $cpid;
}
else if ($cpid == 0) { // child - set up the timer to delay the parent room.
//Note that the parent is still receptive to incoming socket data after this child has spawned
$gTimer = new Timer(20); // pause for 20 seconds
$gTimer->start();
exit( 0 );
}
else {
// not good - error handling should go here
exit( 0 );
}
}
public function timerTrigger () {
$this->_cpid = 0;
// This is where you should define all the things to do after pausing.
// In the actual server, I've defined a game status variable to hold the status of the room
// and increment it as it progresses through the stages of the game.
// This would say things like 'If status is 1, then we have finished waiting on more players and game can begin'
}
}
I've tried to add comments in the code, and I hope it's fairly self explanatory. I've left in the AddPlayer and RemovePlayer code, or some of it, in the hope that it demonstrates usage of the Timer/Sleep system. It will kick off a timer when a player enters the room. In the actual server, the room's current status is being checked to ensure that this it the state the room is in. If another player enters the room while it's waiting for players, the timer is reset (the process is killed), and a new timer is started which, if it finishes successfully, will start the game.
The WaitPid in both of these procs is the solution to a massive and long running problem I was having whereby unpredictable results were occurring, because the system was queuing up all the dead process ids which were hanging around during my ACTUAL call to waitpid, which will be shown later.
Ok, now the Timer Class:
class Timer {
protected $elapsed_time;
protected $trigger_time;
public function __construct($triggerTime) {
$this->_elapsed_time = 0;
$this->_trigger_time = $triggerTime;
}
public function start() {
while ($this->_elapsed_time <= $this->_trigger_time) {
sleep(1);
$this->_elapsed_time = $this->_elapsed_time + 1;
}
posix_kill(posix_getppid(), SIGUSR1);
}
}
Basically what this is doing, is running out the child process for the specified amount of time (yeah, I know I haven't taken the already given advice on this, but it seems to work ok as it is :-). Once the timer has run out, a SIGUSR1 is passed to the parent id and the child dies. Then, you need this function to pick up the signal back at the parent:
function sig_handler($signo)
{
global $allRooms;
switch ($signo) {
case SIGTERM:
exit;
break;
case SIGUSR1:
$child_process_id = -1;
$child_process_id = pcntl_wait($status);
for($i=1; $i < $allRooms->getNumRooms(); ++$i)
{
$curRoom = $allRooms->getRoom($i);
if (($allRooms->getRoom($i)->getCpid() == $child_process_id) &&
($allRooms->getRoom($i)->getCpid() != 0))
{
$curRoom->timerTrigger();
}
}
break;
default:
break;
}
}
Those of you still following this long winded story will note that this completes the circle - the child process calls this procedure within the parent, though of course both the Timer class and this function are within the same physical block of code, but pcntl_fork splits the process in two, so you have two identical programs running at the same time. So the method described here gives you one way of dealing with these forked processes.
Once this is called within the parent process, it will then match the child process ID to that of the appropriate room, and invoke the timerTrigger function of that room. The room will then check its own status and, assuming this is adequately maintained, carry out the correct processing based on that status. The cycle then starts again, and any time a pause is required, a new Timer is started, and the CPID is recorded to be caught later when the sig_handler does a sweep of all the rooms again.
Phew!
Again, thanks for the help of everyone on the forum, but in particular, a massive thank you to SneakyImp who has been the closest thing to a mentor on this project that I have had!
The project is now up and running, so to see the server in action, feel free to visit www.captionthat.com . But don't bring too high expectations :-)
It's pretty dead around there just now, but I'm improving it all the time, so hopefully one day I will have rooms full of users! In particular, feel free to come and populate/join my forum. It feels very lonely :-)