NAME
Async::Selector::Example::Mojo - Example of using Async::Selector for real-time Web
SYNOPSIS
This example shows how Async::Selector can be used to build a so-called real-time Web application. An Async::Selector object is attached to a piece of resource, and its events are published via both Comet (long-polling) and WebSocket. This example uses Mojolicious::Lite for a Web application framework because of its great support for real-time Web. However you can use Async::Selector with any framework that supports real-time transaction somehow.
CODE
The code is available as eg/mojo.pl
in the Async::Selector distribution package.
#!/usr/bin/env perl
use strict;
use warnings;
use Mojolicious::Lite;
use Mojo::IOLoop;
use Async::Selector;
use constant {
UPDATE_RATE => 1,
RESOURCE_LENGTH => 1024,
};
sub randomData {
return pack("C*", map { int(rand(0x7E - 0x21)) + 0x21 } 1..RESOURCE_LENGTH);
}
my $selector;
{
################## Resource part: Setup resource and selector
$selector = Async::Selector->new();
my $resource = randomData;
my $sequence = 1;
$selector->register(res => sub {
my ($given_sequence) = @_;
return ($sequence > $given_sequence)
? {resource => $resource, sequence => $sequence} : undef;
});
## Update the resource periodically
Mojo::IOLoop->recurring(1/UPDATE_RATE, sub {
$resource = randomData;
$sequence++;
$selector->trigger('res');
});
}
{
################## HTTP part: Setup HTTP frontend
get '/' => sub {
my $self = shift;
$self->render('index');
};
get '/comet' => sub {
my $self = shift;
my $client_sequence = $self->param('seq');
my $watcher = $selector->watch(res => $client_sequence, sub {
my ($w, %resources) = @_;
my ($resource, $sequence)
= ($resources{res}{resource}, $resources{res}{sequence});
$self->render_data("$sequence $resource");
$w->cancel();
});
$self->on(finish => sub {
$watcher->cancel();
});
};
websocket '/websocket' => sub {
my $self = shift;
Mojo::IOLoop->stream($self->tx->connection)->timeout(0);
my $watcher = $selector->watch(res => 0, sub {
my ($w, %resources) = @_;
my ($resource, $sequence)
= ($resources{res}{resource}, $resources{res}{sequence});
$self->send("$sequence $resource");
});
$self->on(finish => sub {
$watcher->cancel();
});
};
app->start;
}
__DATA__
@@ index.html.ep
<!DOCTYPE html>
<html>
<head>
<title>Async::Selector test</title>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script><!--
$(function() {
var RECONNECT_BACKOFF = 2000;
// Setup Comet (long-polling)
var my_sequence = 0;
var sendCometRequest = function() {
$.get("<%= url_for('comet') %>?seq=" + my_sequence)
.done(function(data) {
data = data.split(" ");
my_sequence = data[0];
$('#comet_sequence').text(data[0]);
$('#comet_resource').text(data[1]);
sendCometRequest();
})
.fail(function() {
setTimeout(sendCometRequest, RECONNECT_BACKOFF);
});
};
sendCometRequest();
// Setup WebSocket
var connectWebsocket = function() {
var ws = new WebSocket("<%= url_for('websocket')->to_abs %>");
ws.onmessage = function(event) {
var data = event.data.split(" ");
$('#websocket_sequence').text(data[0]);
$('#websocket_resource').text(data[1]);
};
ws.onclose = function() {
setTimeout(connectWebsocket, RECONNECT_BACKOFF);
};
};
connectWebsocket();
});
//--></script>
</head>
<body>
<div>
<h1>Comet (long-polling)</h1>
<p>Sequence number: <span id="comet_sequence"></span></p>
<textarea id="comet_resource" rows="10" cols="100" readonly="true"></textarea>
</div>
<div>
<h1>WebSocket</h1>
<p>Sequence number: <span id="websocket_sequence"></span></p>
<textarea id="websocket_resource" rows="10" cols="100" readonly="true"></textarea>
</div>
</body>
</html>
USAGE
Install Async::Selector and Mojolicious.
Save the code as
mojo.pl
for example.Run
mojo.pl
.$ perl mojo.pl daemon
Access
http://localhost:3000/
from Web browsers.You will see two random sequences of characters displayed in textareas, both of which represent the same resource.
One of the two sequences is updated periodically using Comet (long-polling), while the other using WebSocket.
DESCRIPTION
The code above consists of three parts, the resource part, the HTTP part and the HTML/Javascript part.
Resource Part
The resource part sets up the resource ($resource
) and its Async::Selector object ($selector
). In this example, $resource
is a random sequence of printable characters, and its size is 1024 bytes.
$resource
is associated with a sequence number ($sequence
). $sequence
is incremented when $resource
is updated.
$resource
is then registered with $selector
by register()
method. In the resource provider subroutine, $given_sequence
is given as the condition input. $given_sequence
represents the sequence number the client currently has. If $sequence
is greater than $given_sequence
, it means that the client needs to be updated. In this case the resource provider exports $resource
and $sequence
in a hash reference.
Finally, we use Mojo::IOLoop to periodically update $resource
. When $resource
is updated, trigger()
method is called on $selector
to notify the clients of the update.
HTTP Part
In the HTTP part, the events from $selector
are exported to Web browsers via Comet (long-polling) and WebSocket. You might have to check out Mojolicious::Lite documentation if you are not familiar with it.
First, we set up the request path for /
, which just renders an HTML page.
Second, we set up the request path for /comet
, which is the request point for Comet (long-polling).
/comet
path accepts a query parameter seq
, which is supposed to be the sequence number the Web browser currently has. We pass the content of seq
to watch()
method on $selector
object. That way, the response is deferred until the $sequence
on the server side is higher than $client_sequence
given by the browser.
When $sequence
on the server side is higher than $client_sequence
, the callback function given to watch()
is called with $resource
and $sequence
. We encode these values in a single string separated by a space, and send it to the browser.
The comet request is not persistent, i.e. a session is finished after the server sends a response. That is why we call $w->cancel()
at the end of the callback function.
Finally, we set up the request path for /websocket
, which accepts WebSocket connections. We use Mojo::IOLoop to set the connection's timeout to 0 (infinite).
Unlike /comet
, we call watch()
method on $selector
with the condition input of 0. There are two reasons for this.
One reason is that we want to send the $resource
as soon as the WebSocket connection is established, so that the $resource
can be immediately rendered in the HTML page. Because $sequence
is always greater than 0, the $resource
is immediately provided to the callback function for watch()
method. Thus we don't have to wait for the first update of the $resource
.
The other reason is that WebSocket connections are persistent. For persistent connections, we can let $selector
execute the callback function every time the $resource
is trigger()
-ed.
Because WebSocket connections are persistent, we don't call $w->cancel()
in the callback function. That way, the selection remains after sending a message to the Web browser.
HTML/Javascript Part
HTML/Javascript part is an HTML text for Web browsers to render. It contains a Javascript program using jQuery (http://jquery.com/).
In the Javascript program, we set up Comet (long-polling) and WebSocket connections.
In sendCometRequest()
function, we send an HTTP Ajax request to /comet
with seq
query parameter being set from my_sequence
variable. my_sequence
variable is maintained by the Javascript program and it is the current sequence number of the resource.
When we get a successful response for the Ajax request, we extract the sequence number and the resource from the response, show them in the HTML page, update my_sequence
and repeat the request recursively.
Updating my_sequence
is very important for Comet connections. If you do not update it, the server thinks the client is out of date and needs to be updated. As a result, the server responds to every request immediately. This is not Comet (long-polling) but just regular polling. By setting my_sequence
appropriately, the client can get a response ONLY WHEN it needs to be updated. This is possible because Async::Selector is level-triggered.
my_sequence
is initialized to 0. Because the $sequence
on the server side is always greater than 0, the first request for /comet
always gets an immediate response from the server. That way, the resource is rendered immediately after the HTML page is loaded. We do not have to wait for the first update of the resource.
In connectWebSocket()
function, we initiate a WebSocket connection to /websocket
request path. When we get a message via the WebSocket, we execute the same decode-and-show routine as in sendCometRequest()
.
Unlike Comet, we do not have to maintain my_sequence
for the WebSocket connection. This is because the WebSocket connection is persistent. We can get the resource when it is updated through the persistent connection.
AUTHOR
Toshio Ito, <debug.ito at gmail.com>