8

here is my problem: I have a script (let's call it comet.php) whic is requsted by an AJAX client script and wait for a change to happen like this:

while(no_changes){
    usleep(100000);
    //check for changes
}

I don't like this too much, it's not very scalable and it's (imho) "bad practice" I would like to improve this behaviour with a semaphore(?) or anyway concurrent programming technique. Can you please give me some tips on how to handle this? (I know, it's not a short answer, but a starting point would be enough.)

Edit: what about LibEvent?

ArtoAle
  • 2,939
  • 1
  • 25
  • 48
  • What are you trying to achieve? The usual method is to have the client to call the script periodically to check for changes. Is there some reason why you want to do it server-side? – JJJ Sep 19 '11 at 10:26
  • 4
    PHP is not really a language you should use for COMET. Use Node.js or something else that works asynchronously (python tornado or greenlets for example). By using PHP running on a thread/process-based webserver you have a huge overhead. – ThiefMaster Sep 19 '11 at 10:29
  • @Juhana, the reason is to `avoid` periodical check for change and have a `reverse-ajax` solution. @thiefMaster I know there are some COMET server solution out there, but I really think that a php backend can be possible, and as long as I write my business login in PHP it would be really better not to rewrite in in another language avoiding code duplication. Can you please explain me why a PHP comet backend would be that overheading? – ArtoAle Sep 19 '11 at 10:38
  • I can see that, but is there a reason to avoid the periodical check? – JJJ Sep 19 '11 at 10:41
  • Yes, it produce lot of network overhead, depending on how frequently you check for new update – ArtoAle Sep 19 '11 at 10:44

5 Answers5

14

You can solve this problem using ZeroMQ.

ZeroMQ is a library that provides supercharged sockets for plugging things (threads, processes and even separate machines) together.

I assume you're trying to push data from the server to the client. Well, a good way to do that is using the EventSource API (polyfills available).

client.js

Connects to stream.php through EventSource.

var stream = new EventSource('stream.php');

stream.addEventListener('debug', function (event) {
    var data = JSON.parse(event.data);
    console.log([event.type, data]);
});

stream.addEventListener('message', function (event) {
    var data = JSON.parse(event.data);
    console.log([event.type, data]);
});

router.php

This is a long-running process that listens for incoming messages and sends them out to anyone listening.

<?php

$context = new ZMQContext();

$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind("tcp://*:5555");

$pub = $context->getSocket(ZMQ::SOCKET_PUB);
$pub->bind("tcp://*:5556");

while (true) {
    $msg = $pull->recv();
    echo "publishing received message $msg\n";
    $pub->send($msg);
}

stream.php

Every user connecting to the site gets his own stream.php. This script is long-running and waits for any messages from the router. Once it gets a new message, it will output this message in EventSource format.

<?php

$context = new ZMQContext();

$sock = $context->getSocket(ZMQ::SOCKET_SUB);
$sock->setSockOpt(ZMQ::SOCKOPT_SUBSCRIBE, "");
$sock->connect("tcp://127.0.0.1:5556");

set_time_limit(0);
ini_set('memory_limit', '512M');

header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");

while (true) {
    $msg = $sock->recv();
    $event = json_decode($msg, true);
    if (isset($event['type'])) {
        echo "event: {$event['type']}\n";
    }
    $data = json_encode($event['data']);
    echo "data: $data\n\n";
    ob_flush();
    flush();
}

To send messages to all users, just send them to the router. The router will then distribute that message to all listening streams. Here's an example:

<?php

$context = new ZMQContext();

$sock = $context->getSocket(ZMQ::SOCKET_PUSH);
$sock->connect("tcp://127.0.0.1:5555");

$msg = json_encode(array('type' => 'debug', 'data' => array('foo', 'bar', 'baz')));
$sock->send($msg);

$msg = json_encode(array('data' => array('foo', 'bar', 'baz')));
$sock->send($msg);

This should prove that you do not need node.js to do realtime programming. PHP can handle it just fine.

Apart from that, socket.io is a really nice way of doing this. And you could connect to socket.io to your PHP code via ZeroMQ easily.

See also

igorw
  • 27,759
  • 5
  • 78
  • 90
  • 3
    Please note: I recently did some benchmarks. At some point this does not scale very well. I tried sending a few thousand messages in a short period of time with a few hundred connected clients. If you are using Apache, it will slow down due to the amount of context switching between the threads. At some point node or nginx-push-stream module will be the better tools for doing this. Nevertheless, it is possible with PHP and it works. – igorw Feb 25 '12 at 13:37
  • 1
    Just love the ease and simplicity of this as well as the very descriptive example. 6 years down the line and still an up to date simple answer. Thank you! – Emil Borconi May 05 '17 at 19:26
4

It really depends on what you are doing in your server side script. There are some situations in which your have no option but to do what you are doing above.

However, if you are doing something which involves a call to a function that will block until something happens, you can use this to avoid racing instead of the usleep() call (which is IMHO the part that would be considered "bad practice").

Say you were waiting for data from a file or some other kind of stream that blocks. You could do this:

while (($str = fgets($fp)) === FALSE) continue;
// Handle the event here

Really, PHP is the wrong language for doing stuff like this. But there are situations (I know because I have dealt with them myself) where PHP is the only option.

DaveRandom
  • 87,921
  • 11
  • 154
  • 174
  • 1
    actually, I would like to reduce CPU overhead by removing the noop while (that's why there's the usleep() call ) – ArtoAle Sep 19 '11 at 10:33
  • 2
    @ArtoAle You can't wait for an event without some kind of loop in PHP - it has no event handlers like e.g. javascript. But by doing it as outlined above, you are letting the `fgets()` call do the waiting, rather than check-sleep-check-sleep-check... In your example, if the event occurs during a `usleep()` you will have to wait for the end of the sleep before you can handle it. By using the (blocking) check as the loop condition, `fgets()` returns immediately as soon as the event has happened and you can handle it straight away. – DaveRandom Sep 19 '11 at 10:40
  • ok, you're right. The point is that with [System V IPC](http://www.php.net/manual/en/intro.sem.php) I can use blocking call to semaphore aquiring (wich result in the same solution you proposed) The problem is that I don't really know how to use semaphore to have an "event-like" application behaviour – ArtoAle Sep 19 '11 at 10:43
  • Can't help you there I'm afraid, semaphore is something I never tried to implement. What exactly are you trying to do? What is the event you are waiting for? – DaveRandom Sep 19 '11 at 10:54
  • Mmm...let's say for example a new message to send over a chat channel (of course, reimplementig a chat engine it's not the best choice, but is just for example) – ArtoAle Sep 19 '11 at 11:12
  • So presumably you would have some daemon process that collects the messages, then when one is available the daemon would send the PHP process a semaphore, and the PHP process would fetch the message (from a database?) and push it to the client? – DaveRandom Sep 19 '11 at 11:27
2

As much as I like PHP, I must say that PHP isn't the best choice for this task. Node.js is much, much better for this kind of thing and it scales really good. It's also pretty simple to implement if you have JS knowledge.

Now, if you don't want to waste CPU cycles, you have to create a PHP script that will connect to a server of some sort on a certain port. The specified server should listen for connections on the chosen port and every X amount of time check for whatever you want to check (db entries for new posts for example) and then it dispatches the message to every connected client that the new entry is ready.

Now, it's not that difficult to implement this event queue architecture in PHP, but it'd take you literally 5 minutes to do this with Node.js and Socket.IO, without worrying whether it'll work in majority of browsers.

N.B.
  • 13,688
  • 3
  • 45
  • 55
0

You need a realtime library.

One example is Ratchet http://socketo.me/

The part that takes care of the pub sub is discussed at http://socketo.me/docs/wamp

The limitation here is that PHP also needs to be the one to initiate the mutable data.

In other words this wont magically let you subscribe to when MySQL is updated. But if you can edit the MySQL-setting code then you can add the publish part there.

William Entriken
  • 37,208
  • 23
  • 149
  • 195
0

I agree with the consensus here that PHP isn't the best solution here. You really need to be looking at dedicated realtime technologies for the solution to this asynchronous problem of delivering data from your server to your clients. It sounds like you are trying to implement HTTP-Long Polling which isn't an easy thing to solve cross-browser. It's been tackled numerous times by developers of Comet products so I'd suggest you look at a Comet solution, or even better a WebSocket solution with fallback support for older browsers.

I'd suggest that you let PHP do the web application functionality that it's good at and choose a dedicated solution for your realtime, evented, asynchronous functionality.

leggetter
  • 15,248
  • 1
  • 55
  • 61