PHP on Windows Zone is brought to you in partnership with:

Gonzalo Ayuso is a Web Architect with more than 10 year of experience in the web development, specialized in Open Source technologies. Experienced delivering scalable, secure and high performing web solutions to large scale enterprise clients. Blogs at gonzalo123.com. Gonzalo is a DZone MVB and is not an employee of DZone and has posted 56 posts at DZone. You can read more from them at their website. View Full User Profile

Real Time Notifications With PHP

03.14.2011
| 15479 views |
  • submit to reddit
Real time communications are cool, isn’t it? Something impossible to do five years ago now (or almost impossible) is already available. Nowadays we have two possible solutions. WebSockets and Comet. WebSockets are probably the best solution but they’ve got two mayor problems:
  • Not all browsers support them.
  • Not all proxy servers allows the communications with websokets.

Because of that I prefer to use comet (at least now). It’s not as good as websockets but pretty straightforward ant it works (even on IE). Now I’m going to explain a little script that I’ve got to perform a comet communications, made with PHP. Probably it’s not a good idea to use it in a high traffic site, but it works like a charm in a small intranet. If you want to use comet in a high traffic site maybe you need have a look to Tornado, twisted, node.js or other comet dedicated servers.

Normally when we are speaking about real-time communications, all the people are thinking about a chat application. I want to build a simpler application. A want to detect when someone clicks on a link. Because of that I will need a combination of HTML, PHP and JavaScript. Let’s start:

For the example I’ll use jquery library, so we need to include the library in our HTML file. It will be a blend of JavaScrip and PHP:

<html>
02 <head>
03 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
04 <title>Comet Test</title>
05 </head>
06 <body>
07 <p><a class='customAlert' href="#">publish customAlert</a></p>
08 <p><a class='customAlert2' href="#">publish customAlert2</a></p>
09 <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js" type="text/javascript"></script>
10 <script src="NovComet.js" type="text/javascript"></script>
11 <script type="text/javascript">
12 NovComet.subscribe('customAlert', function(data){
13 console.log('customAlert');
14 //console.log(data);
15 }).subscribe('customAlert2', function(data){
16 console.log('customAlert2');
17 //console.log(data);
18 });
19
20 $(document).ready(function() {
21 $("a.customAlert").click(function(event) {
22 NovComet.publish('customAlert');
23 });
24
25 $("a.customAlert2").click(function(event) {
26 NovComet.publish('customAlert2');
27 });
28 NovComet.run();
29 });
30 </script>
31 </body>
32 </html>


The client code:

//NovComet.js
02 NovComet = {
03 sleepTime: 1000,
04 _subscribed: {},
05 _timeout: undefined,
06 _baseurl: "comet.php",
07 _args: '',
08 _urlParam: 'subscribed',
09
10 subscribe: function(id, callback) {
11 NovComet._subscribed[id] = {
12 cbk: callback,
13 timestamp: NovComet._getCurrentTimestamp()
14 };
15 return NovComet;
16 },
17
18 _refresh: function() {
19 NovComet._timeout = setTimeout(function() {
20 NovComet.run()
21 }, NovComet.sleepTime);
22 },
23
24 init: function(baseurl) {
25 if (baseurl!=undefined) {
26 NovComet._baseurl = baseurl;
27 }
28 },
29
30 _getCurrentTimestamp: function() {
31 return Math.round(new Date().getTime() / 1000);
32 },
33
34 run: function() {
35 var cometCheckUrl = NovComet._baseurl + '?' + NovComet._args;
36 for (var id in NovComet._subscribed) {
37 var currentTimestamp = NovComet._subscribed[id]['timestamp'];
38
39 cometCheckUrl += '&' + NovComet._urlParam+ '[' + id + ']=' +
40 currentTimestamp;
41 }
42 cometCheckUrl += '&' + NovComet._getCurrentTimestamp();
43 $.getJSON(cometCheckUrl, function(data){
44 switch(data.s) {
45 case 0: // sin cambios
46 NovComet._refresh();
47 break;
48 case 1: // trigger
49 for (var id in data['k']) {
50 NovComet._subscribed[id]['timestamp'] = data['k'][id];
51 NovComet._subscribed[id].cbk(data.k);
52 }
53 NovComet._refresh();
54 break;
55 }
56 });
57
58 },
59
60 publish: function(id) {
61 var cometPublishUrl = NovComet._baseurl + '?' + NovComet._args;
62 cometPublishUrl += '&publish=' + id;
63 $.getJSON(cometPublishUrl);
64 }
65 };


The server-side PHP

// comet.php
02 include('NovComet.php');
03
04 $comet = new NovComet();
05 $publish = filter_input(INPUT_GET, 'publish', FILTER_SANITIZE_STRING);
06 if ($publish != '') {
07 echo $comet->publish($publish);
08 } else {
09 foreach (filter_var_array($_GET['subscribed'], FILTER_SANITIZE_NUMBER_INT) as $key => $value) {
10 $comet->setVar($key, $value);
11 }
12 echo $comet->run();
13 }


and my comet library implementation:

// NovComet.php
02 class NovComet {
03 const COMET_OK = 0;
04 const COMET_CHANGED = 1;
05
06 private $_tries;
07 private $_var;
08 private $_sleep;
09 private $_ids = array();
10 private $_callback = null;
11
12 public function __construct($tries = 20, $sleep = 2)
13 {
14 $this->_tries = $tries;
15 $this->_sleep = $sleep;
16 }
17
18 public function setVar($key, $value)
19 {
20 $this->_vars[$key] = $value;
21 }
22
23 public function setTries($tries)
24 {
25 $this->_tries = $tries;
26 }
27
28 public function setSleepTime($sleep)
29 {
30 $this->_sleep = $sleep;
31 }
32
33 public function setCallbackCheck($callback)
34 {
35 $this->_callback = $callback;
36 }
37
38 const DEFAULT_COMET_PATH = "/dev/shm/%s.comet";
39
40 public function run() {
41 if (is_null($callback)) {
42 $defaultCometPAth = self::DEFAULT_COMET_PATH;
43 $callback = function($id) use ($defaultCometPAth) {
44 $cometFile = sprintf($defaultCometPAth, $id);
45 return (is_file($cometFile)) ? filemtime($cometFile) : 0;
46 };
47 } else {
48 $callback = $this->_callback;
49 }
50
51 for ($i = 0; $i < $this->_tries; $i++) {
52 foreach ($this->_vars as $id => $timestamp) {
53 if ((integer) $timestamp == 0) {
54 $timestamp = time();
55 }
56 $fileTimestamp = $callback($id);
57 if ($fileTimestamp > $timestamp) {
58 $out[$id] = $fileTimestamp;
59 }
60 clearstatcache();
61 }
62 if (count($out) > 0) {
63 return json_encode(array('s' => self::COMET_CHANGED, 'k' => $out));
64 }
65 sleep($this->_sleep);
66 }
67 return json_encode(array('s' => self::COMET_OK));
68 }
69
70 public function publish($id)
71 {
72 return json_encode(touch(sprintf(self::DEFAULT_COMET_PATH, $id)));
73 }
74 }

 

As you can see in my example I’ve created a personal protocol for the communications between the client (js at browser), and the server (PHP). It’s a simple one. If you’re looking for a “standard” protocol maybe you need have a look to bayeux protocol from Dojo people.

Let me explain a little bit the usage of the script:

  • In the HTML page we start the listener (NovComet.subscribe).
  • We can subscribe to as many events we want (OK it depends on our resources)
  • When we subscribe to one event we pass a callback function to be triggered.
  • When we subscribe to the event, we pass the current timestamp to the server.
  • Client side script (js with jquery) will call to server-side script (PHP) with the timestamp and will wait until server finish.
  • Server side script will answer when even timestamp changes (someone has published the event)
  • Server side will no keep waiting forever. If nobody publish the event, server will answer after a pre-selected timeout
  • client side script will repeat the process again and again.

There’s something really important with this technique. Our server-side event check need to be as simpler as we can. We cannot execute a SQL query for example (our sysadmin will kill us if we do it). We need to bear in mind that this check will be performed again and again per user, because of that it must be as light as we can. In this example we are checking the last modification date of a file (filemtime). Another good solution is to use a memcached database and check a value.

For the test I’ve also created a publishing script (NovComet.publish). This is the simple part. We only call a server-side script that touch the event file (changing the last modification date), triggering the event.

Now I’m going to explain what we can see on the firebug console:

  1. The first iteration nothing happens. 200 OK Http code after the time-out set in the PHP script
  2. As we can see here the script returns a JSON with s=0 (nothing happens)
  3. Now we publish an event. Script returns a 200 OK but now the JSON is different. s=1 and the time-stamp of the event
  4. Our callback has been triggered
  5. And next iteration waiting

And that’s all. Simple and useful. But remember, you must take care if you are using this solution within a high traffic site. What do you think? Do you use lazy comet with PHP in production servers or would you rather another solution?

You can get the code at github here.

References
Published at DZone with permission of Gonzalo Ayuso, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Rehman Khan replied on Sat, 2012/02/25 - 5:33am

Nice write up. You’re absolutely correct: we deployed this technique on a high traffic website and found the server frequently bottlenecking. We ended up using writing our own custom little server to handle the requests, as well as some back end caching to prevent unnecessary database calls.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.