WebSockets vs Server-Sent Events vs Long-polling

WebSockets vs Server-Sent Events vs Long-polling



12 May 2012

Apparently social networking is the banner of the nowadays web. Everybody intends bringing some features into his projects. Some of them require immediate notification. That is getting common, if you open a page with upcoming messages (status feed, notification subsystem, friends-list), you expected them being updated as soon as a new message (status, notification, friend-making action) arrives. As you well know, original web design allowed only one-way client-server communication (one requests, another one responds), though now HTML5 working group doing their best to fix it or rather to patch it. However, the web-projects are still using long-polling trick to emulate server-client communication.

Well, now new web browser versions appear every few months. Besides they update automatically. Thus a huge number of users have the latest browser versions, which support HTML 5 communication APIs. Is that the time to put long-polling away? Let’s find out.

Our test task will be something you may likely need if you have on your site any sort of user communication. That is notification of user actions. In the simplest case when the user gets a private message, the number of unread notifications increases in the user panel. We will solve the task using long-polling, Server-Sent Events and WebSockets. Then we compare the results.

First of all let’s examine the common code used in the examples. We will need configuration file, a library to access DB, a model to retrieve unread notification number and to add a new notification.

Usually such communication API examples don’t include any business logic, but execution delays to emulate the model working. I would like to make it close to the real application, what is meant to help when comparing memory/CPU usage on the server for each of the cases.

So, we will need a dump DB table:


CREATE TABLE IF NOT EXISTS `notification` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `recipientUid` int(10) unsigned NOT NULL,
  `eventId` int(10) unsigned NOT NULL,
  `isNew` tinyint(1) unsigned NOT NULL DEFAULT '1',
  PRIMARY KEY (`id`),
  KEY `IX_recipientUid` (`recipientUid`),
  KEY `IX_isNew` (`isNew`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COMMENT='User notifications';

Here is the model:

<?php
class Model_UserNotification
{
   private $_db;


   public function  __construct(Lib_Db $db)
   {
        $this->_db = $db;
   }
   /**
    *
    * @param int $recipientUid
    * @return int
    */
   public function fetchNumberByRecipientUid($recipientUid)
   {
       return $this->_db->fetch("SELECT count(*) as count "
           . " FROM notification WHERE recipientUid = %d AND isNew = 1"
           , $recipientUid)->count;
   }
   /**
    *
    * @param int $recipientUid
    * @param int $eventId
    */
   public function add($recipientUid$eventId)
   {
       $this->_db->update("INSERT INTO "
           . " notification (`id`, `recipientUid`, `eventId`, `isNew`) VALUES (NULL, '%d', '%d', '1')"
           , $recipientUid$eventId);
   }
   /**
    *
    * @param int $recipientUid
    */
   public function removeAll($recipientUid)
   {
       $this->_db->update("DELETE FROM "
           . " notification WHERE recipientUid = %d"
           , $recipientUid);
   }
}

Long pulling

 Long-polling technique explanation


How it works

Client application (browser) sends a request with event recipient id (here is the user, registered on the page) and current state (the displayed number of unread notification) to the server via HTTP. It creates an Apache process, which repeatedly checks DB until the state is changed in there. When the state eventually changed, the client gets the server response and sends next request to the server.


Implementation

Client side contains simple HTML with two input fields to show response data (updated number of unread notifications and time of the response event). JS module sends recipient user id and current state (unread notification number) to the server as XMLHttpRequest. To make possible cross-domain communication, we use JSONP and that means the handler must of the public scope.


...
<p>Recipient id: <?= $recipientUid ?></p>
<p>Notifications: <input id="notificationNum" size="4" name="some" value="<?= $displayedNotificationNum ?>" /></p>
<p>Last event arrived at: <input id="time" size="12" name="some" value="0" /></p>

<script type="text/javascript">

(function( $ ) {

var UID = <?= $recipientUid ?>;

$.NotifierLongPolling = (function() {
    var _stateNode = $('#notificationNum'), _timeNode = $('#time');
    return {
        onMessage : function(data) {
            _stateNode.val(data.updatedNotificationNum);
            _timeNode.val(data.time);
            setTimeout($.NotifierLongPolling.send, 3000);
        },
        send : function() {           
            $.ajax({
                    'url''server.php',
                    'type''POST',
                    'dataType''jsonp',
                    'jsonpCallback''$.NotifierLongPolling.onMessage',
                    'data''recipientUid=' + UID + '&displayedNotificationNum='
                        + _stateNode.val()
            });
        }
    }
}());

// Document is ready
$(document).bind('ready.app'function() {
   setTimeout($.NotifierLongPolling.send, 40); 
});

})( jQuery );


</script>

Server waits 3 second than check if the updated state matches the given one. If the state has changed in the DB, the server responds otherwise it repeats the cycle.


//...
$recipientUid = (int)$_REQUEST["recipientUid"];
$displayedNotificationNum = (int)$_REQUEST["displayedNotificationNum"];
$secCount = 0;

do {
    sleep(IDLE_TIME);
    $updatedNotificationNum = $model->fetchNumberByRecipientUid($recipientUid);
} while ($updatedNotificationNum == $displayedNotificationNum);

header("HTTP/1.0 200");
printf ('%s({"time" : "%s", "updatedNotificationNum" : "%d"});'
    , $_REQUEST["callback"], date('d/m H:i:s'), $updatedNotificationNum);

Client side receives new state from the server, displays and sends to the server the new state to repeat the workflow.


 Long-polling technique usage example screenshot


Server-Sent Events


 Server-Sent Event API explanation


How it works

Client (browser) sends a request to the server via HTTP. It creates a process, which fetches latest state in the DB and responds back. Client gets server response and in 3 seconds sends next request to the server.


Implementation

HTML on client-size has again two input fields to show response data. JS module opens EventSource and passes though the connection recipient user id.


...
<p>Recipient id: <?= $recipientUid ?></p>
<p>Notifications: <input id="notificationNum" size="4" name="some" value="<?= $displayedNotificationNum ?>" /></p>
<p>Last event arrived at: <input id="time" size="12" name="some" value="0" /></p>

<script type="text/javascript">

(function( $ ) {

var UID = <?= $recipientUid ?>;

NotifierSSE = (function() {
    var _stateNode = $('#notificationNum'),
        _timeNode = $('#time'),
        _src,
        _handler = {
        onMessage : function(event) {            
             var data = JSON.parse(event.data);
            _stateNode.val(data.updatedNotificationNum);
            _timeNode.val(data.time);
        }
    };
    return {
        init : function () {
        _src = new EventSource("server.php?recipientUid=" + UID);
        _src.addEventListener('message', _handler.onMessage, false);
        }
    }
}());


// Document is ready
$(document).bind('ready.app'function() {
   setTimeout(NotifierSSE.init, 40); 
});

})( jQuery );

</script>

Server responds into the data stream with the last state (unread notification number) regarding the recipient user id.


//...
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

$recipientUid = (int)$_REQUEST["recipientUid"];

function send($updatedNotificationNum)
{
    printf ("id: %s\n\n", PROC_ID);
    printf ('data: {"time" : "%s", "updatedNotificationNum" : "%d"}' . "\n\n"
        ,date('d/m H:i:s') , $updatedNotificationNum);
    ob_flush();
    flush();
}

while (true) {
    send($model->fetchNumberByRecipientUid($recipientUid));
    sleep(IDLE_TIME);
} 
//...

Client gets onMessage event handler invoked, the data displayed. Browser will repeat request every 3 second unless you close the connection (close method). You can change delay between requests by passing into the data stream on the server “retry: <time in milliseconds>\n”.


 Server-Sent Event API usage example screenshot


WebSockets


 WebSocket API explanation


How it works

Client notifies web-socket server (EventMachine) of an event, giving ids of recipients. The server immediately notifies all the active clients (subscribed to that type of event). Clients process event when given recipient Id matches the client’s one.


Implementation

Here we need an Event Machine supporting WebSockets. The easiest way, I see, is to deployWaterSpout Server . You write your own controller (notification_controller.php) based onlocke_controller.php.

Client opens WebSocket connection and subscribes a handler for events on /notification/updatespost. Now let’s add a button in the HTML which sends on /notification/presents recipient user and action ids. That will cause broadcast message on the all open connections. So every active client receives the notification. The client event handler checks if the recipient user id matches client’s logged in user id and if so increment unread notification number.


<p>Recipient id: <?= $recipientUid ?></p>
    <p>Notification: <span id="display"></span></p>
    <button id="test">Fire an event</button>

<script>
    realtime = new realtimeComm(window.location.host + ":20001");
    realtime.addListener('/notification/updates'function(response) {
        $('#display').html('Client #' + response.data.recipientUid + ' broadcast an action #' + response.data.actionId);
    });

    $('#test').bind('click'thisfunction(e){
        e.preventDefault();

        realtime.send('/notification/presence'{
            'actionId'1,
            'recipientUid': <?= $recipientUid ?>
        }function() {});

    });

</script>

Working a bit on the controller, you can make the system responds exclusively to the client of the given recipient user id.

You’ve probably noticed the example working perfect on Chrome, but for Firefox 6.0 it switches for long-polling. It announced that Firefox 6 supports WebSockets, but you ain’t going to find WebSocket object defined. Well, let’s change WebSocket availability check in the top of realtime.js with following:


if (!window.WebSocket) {
            window.WebSocket = window.MozWebSocket ? window.MozWebSocket : undefined;
 }

Oops, it doesn’t work anyway. Firefox 6 supports hybi-07 specification version, when Chrome and WaterSpout – hybi-00 (hixie-76). So, you will hardly able the connection through reverse-proxies and gateways. Besides, the old specification was disabled in Firefox and Opera due to security issues. The only browsers now supporting the latest WebSocket specification are betas of Firefox 7/8 and Chrome 14. As for EventMachine server implementations, I found none.


 WebSocket API usage example screenshot


Conclusion


..Long-pollingServer-Sent EventsWebSockets
Browser supportSupported by the most of currently used browsersSupported by Chrome 9+, Firefox 6+, Opera 11+, Safari 5+The latest hybi-10 protocol supported by Chrome 14, Firefox 7 betas, hybi-07 supported by Firefox 6
Server-loadingTakes little of CPU resources, but creates idle processes per user expending server memoryWorks in many ways as long-polling, unless SSE doesn’t need to close connection every time when response is sentThe best possible solution. Server has the only process serving any requests. No loops, memory/CPU expense per client, but per client action
Client-loadingDepends on implementation, though it always an asynchronous process.Natively implemented in the browser, takes minimum resourcesNatively implemented in the browser, takes minimum resources
TimelinessNear real-time, though black out period between sending response and making a new request adds some delayDefault delay 3 sec., but can be changedTrue real-time
Complexity of implementationVery easyEven easierRequires an EventMachine server and a custom port being open



Now you can download the package with the example here.

  • rainolf    3 days ago

    Hello,

    i've tried to merge your 2 example suggested with no luck.

    Could u give us a working example where monitor console trigger events from mysql database where its isNew field = 1?
    Thank you

  • Avatar
    facundo.olano    6 days ago

    Nice article. I've just learned about Server-Sent Events, and they look interesting.

    What I don't get is why you still have to loop and request every 3 seconds when using SSE. Isn't it possible to just push the updates to the client whenever an action that requires notification takes place?

    • Avatar
      Dmitry Sheiko  Mod   facundo.olano    3 days ago

      Now way with SSE - that's pulling approach. Though you can do it with WebSockets and probably with WebRTC

      • Avatar
        facundo.olano   Dmitry Sheiko    3 days ago

        I'm positive you don't need to keep requesting with SSE. Check out this flask app https://github.com/jkbr/chat/b...
        The client just connects to the stream and defines a function to handle messages, without requesting for updates. The server yields new data when available.

        • Avatar
          Dmitry Sheiko  Mod   facundo.olano    3 days ago

          Well, I wasn't exact. Let's put this way. With EventSource constructor we send a request to server. Browser opens connection and keeps it until the session exists or close method called:

          RESPONSE:
          HTTP/1.1 200 OK
          Server: xLightweb/2.12-HTML5Preview6
          Content-Type: text/event-stream
          Expires: Fri, 01 Jan 1990 00:00:00 GMT
          Cache-Control: no-cache, no-store, max-age=0, must-revalidate
          Pragma: no-cache
          Connection: close

          : time stream
          retry: 5000

          id: 7
          data: Thu Mar 11 07:31:30 CET 2010

          id: 8
          data: Thu Mar 11 07:31:35 CET 2010

          ...

          We subscribe on the client an event listener (onMessage) which receives the connection messages as soon as they added to the stream by the server.

          I meant that this reminds Comet pulling, where server cannot request the client, instead client sort of listens to the server stream.

          You can change interval time-outs with stream "retry:" directive, but still you cannot make the sever to talk directly to client. Compare to WebSocket: There is an open bi-directional connection on which all the participates can send messages.

    • Avatar
      rainolf    8 days ago

      find the error:

      app.php line 38

      $path = BASE_PATH . '/Template/' . str_replace('_', "/", $templateName) . '.phtml';

      instead of

      $path = BASE_PATH . '/template/' . str_replace('_', "/", $templateName) . '.phtml';

      How can i integrate this code to generate events basedon mysql new rows like examples in this page?

      • Avatar
        rainolf    9 days ago

        Yeah, its exactly what i'm searching for but unfortunately i'm not a javascript developer.
        Is there the possibility to have a working downloadable example?
        Thank you

        • Avatar
          Dmitry Sheiko  Mod   rainolf    9 days ago

          Please find the attached code in here http://code.google.com/p/realt...

          • Avatar
            rainolf   Dmitry Sheiko    8 days ago

            Thank you, its giving me the following error:

            Fatal error: Uncaught exception 'Exception' with message 'Requested 
            module (NotificationModule) not found' in 
            /srv/www/htdocs/rta/Lib/App.php:40
            Stack trace:
            #0 /srv/www/htdocs/rta/Lib/App.php(58): 
            RTA\App::_renderTemplate('NotificationMod...', Object(RTA\View))
            #1 /srv/www/htdocs/rta/index.php(35): 
            RTA\App::renderModule('NotificationMod...')
            #2 {main}
            thrown in /srv/www/htdocs/rta/Lib/App.php on line 40

            I've simply unpack the code and put it on my root lamp server.

            Is there anything that must modified before?
            is there any mysql integration cause i would like to receive events from mysql table.

            Thank you again

        • Avatar
          rainolf    9 days ago

          Thank you for your answer,
          anyway i'm not a guru in this code and could u give me an example of where a have to modify the code in order to let pass more data based on my new database schema?
          i need to extend the database with new fields i could add:
          - log message (varchar)
          - event type (varchar)
          then i need to modify the code and print the results in a page where the data passes continuously like console style.
          So i don't need (i guess) an input text field on html page but something else.

          Thank you

        • Avatar
          rainolf    9 days ago

          Another modification i have to do is to pass more data. So i will insert more columns into the mysql table but i would like to know how to pass and let also to be updated int he same manner.So my question is:"Which is also the code parts i have to modify in order to pass more data".
          Thank you very much

          • Avatar
            Dmitry Sheiko  Mod   rainolf    9 days ago

            The best way would be WebSockets, but the simplest one SSE. In both cases you are not much limited in the message sizes, as far as I know. But that's more about your client application design. I would suggest to send only the commands and data required by client at the moment. Thus, you can make your client receiving every e.g. 3 sec. update message from the server. Based on this update information the script modifies the view (not building it every time, but updates)

          • Avatar
            rainolf    10 days ago

            i want to use the best and simplest way to do that,
            maybe could be Server-Sent Events. It the only i've tried at the moment but depends only on the best method suggested.

            • Avatar
              rainolf    10 days ago

              Very well done..
              I would like to use this in order to create a console log on my php webapp to trigger mysql e events write form a script.
              However its not so simple for me to modify this code in order to have in a frame or window a continuos data in console log style like tail -n -f .
              Could you suggest me or give me an example to achieve this?
              Thank you

            • Avatar
              mesuutt    3 months ago

              Websockets spend how much server resources ? If 1.000.000 user connect to the server same time with websockets, what happens ?

            • Avatar
              Tom Redfern    5 months ago

              Thanks Dmitry - great article!

              • Avatar
                diligentStudent    6 months ago

                I'm running PHP 5.1.2 on my Apache Server. I'll propably have to add an --enable feature when compiling. I had the same problem whith creating a shared memory or semaphore with PHP. I#ll check my environment. Thx

                • Avatar
                  Yaffle    7 months ago

                  for SSE "Timeliness" 
                  "Default delay 3 sec., but can be changed " 
                  should be "Near real-time"
                  (delay is reconnect delay if connection was closed)

                  • Avatar
                    diligentStudent    7 months ago

                    Hi there - Great article! I've downloaded the package with the examples. But when I send a new notification, nothing happens in any of the other php files. I'm trying to get to know SSE. Any idea what the problem could be? I'm running Firefox 12.0 right now, so the browser should support HTML5. But in the Server-Side Event Demo, nothing happens....
                    The line fetchNumberByRecipientUid($recipientUid); ?> is displayed on top of the rest of the page,
                    In the index.php file the line 
                    add(RECIPIENT_UID, EVENT_ID); break; case "cleanup": $model->removeAll(RECIPIENT_UID); break; } } ?> 
                    is diplayed on top of the rest of the page. Any idea what I'm doing wrong?

                    • Avatar
                      Dmitry Sheiko  Mod   diligentStudent    7 months ago

                      Just checked by myself. Everything is working on FF12.  Open in one window /examples/index.php and within another /examples/sse/index.php

                      In the first click the button Send new notification to user 1. Now observe as in another one Notification number and last arrived time values change

                      • Avatar
                        diligentStudent   Dmitry Sheiko    6 months ago

                        I'm sorry, I'm doing exactly as you describe, but instead of how your code looks like in the screenshot, i get the line add(RECIPIENT_UID, EVENT_ID);
                        break;
                        case "cleanup":
                        $model->removeAll(RECIPIENT_UID);
                        break;
                        }
                        }
                        ?>

                        displayed in my browser window in the first row in the Index Demo. In the Server-Side Event Demo, the code fetchNumberByRecipientUid($recipientUid);
                        ?>is displayed in the browser window, and = $displayedNotficationNum = is displayed in the "Notifications"-tab. I'm running this in an Apache Server, but also tried it by opening a normal browser... can't get it to work, is there any settings I have forgotten to install for using SSE?


                  • 0
                    点赞
                  • 0
                    收藏
                    觉得还不错? 一键收藏
                  • 0
                    评论
                  评论
                  添加红包

                  请填写红包祝福语或标题

                  红包个数最小为10个

                  红包金额最低5元

                  当前余额3.43前往充值 >
                  需支付:10.00
                  成就一亿技术人!
                  领取后你会自动成为博主和红包主的粉丝 规则
                  hope_wisdom
                  发出的红包
                  实付
                  使用余额支付
                  点击重新获取
                  扫码支付
                  钱包余额 0

                  抵扣说明:

                  1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
                  2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

                  余额充值