Commit ad98ea34 authored by John E. Vincent's avatar John E. Vincent

adding websocket example and README

parent 915d3153
require 'rubygems'
require 'sinatra'
require File.join('.', 'lib','noah')
## Uncomment the following to hardcode a redis url
#ENV['REDIS_URL'] = "redis://localhost:6379/0"
noah = Noah::App.new do
set :run, false
set :environment, :production
## Set your server list up here
# set :server, %w[thin mongrel webrick Jetty Kirk]
end
ENV['REDIS_URL'] = "redis://localhost:6379/1"
noah = Noah::App.new
run noah
# Examples
These are some notes regarding the following examples
## websocket.rb
This is an example of using Websockets, EventMachine and Redis PubSub to provide a "status" console of sorts.
### Requirements
You'll need to grab a few additional gems
* (em-hiredis)[https://github.com/mloughran/em-hiredis]
You'll have to compile/install from source. Sorry. Should pull in the `hiredis` native ext.
* (em-websocket)[https://github.com/igrigorik/em-websocket]
Available via rubygems
### Running
To get the maximum effect, start with a clean Redis database.
* Start the server:
~/development/noah/examples$ ./websocket.rb
>> Thin web server (v1.2.7 codename No Hup)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:3000, CTRL+C to stop
You should be able to load up the "normal" Noah sample page on [http://localhost:3000].
* Load the "websocket" file
In another browser window, open the `websocket.html` file.
* Send a message
From another terminal window send the following:
curl -X PUT -d '{"name":"testhost2","status":"down"}' http://localhost:3000/h/testhost2
You should see the message come across in the browser window like so:
connected...
2 connected and waiting....
(noah.Host[testhost2].create) {"id":"1","name":"testhost2","status":"down","created_at":"2011-02-14 20:58:04 UTC","updated_at":"2011-02-14 20:58:04 UTC","services":[]}
(noah.Host[testhost2].save) {"id":"1","name":"testhost2","status":"down","created_at":"2011-02-14 20:58:04 UTC","updated_at":"2011-02-14 20:58:04 UTC","services":[]}
(noah.Host[testhost2].save) {"id":"1","name":"testhost2","status":"down","created_at":"2011-02-14 20:58:04 UTC","updated_at":"2011-02-14 20:58:04 UTC","services":[]}
(noah.Host[testhost2].update) {"id":"1","name":"testhost2","status":"down","created_at":"2011-02-14 20:58:04 UTC","updated_at":"2011-02-14 20:58:04 UTC","services":[]}
You can see the Watcher pattern in the parenthesis and then the JSON message body.
For fun, refresh the page to clear it and then run the sample data population rake task.
### Known issues
When I started working on the Watcher stuff, I realized that I'm sending A LOT of extranous messages. These are mostly the result of the way I'm creating new objects with Ohm (i.e. via `.create`).
I'll be cleaning that up and trying to get down to a single message per operation.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
// Lincense: New BSD Lincense
// Reference: http://dev.w3.org/html5/websockets/
// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol
(function() {
if (window.WebSocket) return;
var console = window.console;
if (!console) console = {log: function(){ }, error: function(){ }};
function hasFlash() {
if ('navigator' in window && 'plugins' in navigator && navigator.plugins['Shockwave Flash']) {
return !!navigator.plugins['Shockwave Flash'].description;
}
if ('ActiveXObject' in window) {
try {
return !!new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
} catch (e) {}
}
return false;
}
if (!hasFlash()) {
console.error("Flash Player is not installed.");
return;
}
WebSocket = function(url, protocol, proxyHost, proxyPort, headers) {
var self = this;
self.readyState = WebSocket.CONNECTING;
self.bufferedAmount = 0;
WebSocket.__addTask(function() {
self.__flash =
WebSocket.__flash.create(url, protocol, proxyHost || null, proxyPort || 0, headers || null);
self.__flash.addEventListener("open", function(fe) {
try {
if (self.onopen) self.onopen();
} catch (e) {
console.error(e.toString());
}
});
self.__flash.addEventListener("close", function(fe) {
try {
if (self.onclose) self.onclose();
} catch (e) {
console.error(e.toString());
}
});
self.__flash.addEventListener("message", function(fe) {
var data = decodeURIComponent(fe.getData());
try {
if (self.onmessage) {
var e;
if (window.MessageEvent) {
e = document.createEvent("MessageEvent");
e.initMessageEvent("message", false, false, data, null, null, window);
} else { // IE
e = {data: data};
}
self.onmessage(e);
}
} catch (e) {
console.error(e.toString());
}
});
self.__flash.addEventListener("stateChange", function(fe) {
try {
self.readyState = fe.getReadyState();
self.bufferedAmount = fe.getBufferedAmount();
} catch (e) {
console.error(e.toString());
}
});
//console.log("[WebSocket] Flash object is ready");
});
}
WebSocket.prototype.send = function(data) {
if (!this.__flash || this.readyState == WebSocket.CONNECTING) {
throw "INVALID_STATE_ERR: Web Socket connection has not been established";
}
var result = this.__flash.send(data);
if (result < 0) { // success
return true;
} else {
this.bufferedAmount = result;
return false;
}
};
WebSocket.prototype.close = function() {
if (!this.__flash) return;
if (this.readyState != WebSocket.OPEN) return;
this.__flash.close();
// Sets/calls them manually here because Flash WebSocketConnection.close cannot fire events
// which causes weird error:
// > You are trying to call recursively into the Flash Player which is not allowed.
this.readyState = WebSocket.CLOSED;
if (this.onclose) this.onclose();
};
/**
* Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
*
* @param {string} type
* @param {function} listener
* @param {boolean} useCapture !NB Not implemented yet
* @return void
*/
WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
if (!('__events' in this)) {
this.__events = {};
}
if (!(type in this.__events)) {
this.__events[type] = [];
if ('function' == typeof this['on' + type]) {
this.__events[type].defaultHandler = this['on' + type];
this['on' + type] = WebSocket_FireEvent(this, type);
}
}
this.__events[type].push(listener);
};
/**
* Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
*
* @param {string} type
* @param {function} listener
* @param {boolean} useCapture NB! Not implemented yet
* @return void
*/
WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
if (!('__events' in this)) {
this.__events = {};
}
if (!(type in this.__events)) return;
for (var i = this.__events.length; i > -1; --i) {
if (listener === this.__events[type][i]) {
this.__events[type].splice(i, 1);
break;
}
}
};
/**
* Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
*
* @param {WebSocketEvent} event
* @return void
*/
WebSocket.prototype.dispatchEvent = function(event) {
if (!('__events' in this)) throw 'UNSPECIFIED_EVENT_TYPE_ERR';
if (!(event.type in this.__events)) throw 'UNSPECIFIED_EVENT_TYPE_ERR';
for (var i = 0, l = this.__events[event.type].length; i < l; ++ i) {
this.__events[event.type][i](event);
if (event.cancelBubble) break;
}
if (false !== event.returnValue &&
'function' == typeof this.__events[event.type].defaultHandler)
{
this.__events[event.type].defaultHandler(event);
}
};
/**
*
* @param {object} object
* @param {string} type
*/
function WebSocket_FireEvent(object, type) {
return function(data) {
var event = new WebSocketEvent();
event.initEvent(type, true, true);
event.target = event.currentTarget = object;
for (var key in data) {
event[key] = data[key];
}
object.dispatchEvent(event, arguments);
};
}
/**
* Basic implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-interface">DOM 2 EventInterface</a>}
*
* @class
* @constructor
*/
function WebSocketEvent(){}
/**
*
* @type boolean
*/
WebSocketEvent.prototype.cancelable = true;
/**
*
* @type boolean
*/
WebSocketEvent.prototype.cancelBubble = false;
/**
*
* @return void
*/
WebSocketEvent.prototype.preventDefault = function() {
if (this.cancelable) {
this.returnValue = false;
}
};
/**
*
* @return void
*/
WebSocketEvent.prototype.stopPropagation = function() {
this.cancelBubble = true;
};
/**
*
* @param {string} eventTypeArg
* @param {boolean} canBubbleArg
* @param {boolean} cancelableArg
* @return void
*/
WebSocketEvent.prototype.initEvent = function(eventTypeArg, canBubbleArg, cancelableArg) {
this.type = eventTypeArg;
this.cancelable = cancelableArg;
this.timeStamp = new Date();
};
WebSocket.CONNECTING = 0;
WebSocket.OPEN = 1;
WebSocket.CLOSED = 2;
WebSocket.__tasks = [];
WebSocket.__initialize = function() {
if (!WebSocket.__swfLocation) {
//console.error("[WebSocket] set WebSocket.__swfLocation to location of WebSocketMain.swf");
//return;
WebSocket.__swfLocation = "js/WebSocketMain.swf";
}
var container = document.createElement("div");
container.id = "webSocketContainer";
// Puts the Flash out of the window. Note that we cannot use display: none or visibility: hidden
// here because it prevents Flash from loading at least in IE.
container.style.position = "absolute";
container.style.left = "-100px";
container.style.top = "-100px";
var holder = document.createElement("div");
holder.id = "webSocketFlash";
container.appendChild(holder);
document.body.appendChild(container);
swfobject.embedSWF(
WebSocket.__swfLocation, "webSocketFlash", "8", "8", "9.0.0",
null, {bridgeName: "webSocket"}, null, null,
function(e) {
if (!e.success) console.error("[WebSocket] swfobject.embedSWF failed");
}
);
FABridge.addInitializationCallback("webSocket", function() {
try {
//console.log("[WebSocket] FABridge initializad");
WebSocket.__flash = FABridge.webSocket.root();
WebSocket.__flash.setCallerUrl(location.href);
for (var i = 0; i < WebSocket.__tasks.length; ++i) {
WebSocket.__tasks[i]();
}
WebSocket.__tasks = [];
} catch (e) {
console.error("[WebSocket] " + e.toString());
}
});
};
WebSocket.__addTask = function(task) {
if (WebSocket.__flash) {
task();
} else {
WebSocket.__tasks.push(task);
}
}
// called from Flash
function webSocketLog(message) {
console.log(decodeURIComponent(message));
}
// called from Flash
function webSocketError(message) {
console.error(decodeURIComponent(message));
}
if (window.addEventListener) {
window.addEventListener("load", WebSocket.__initialize, false);
} else {
window.attachEvent("onload", WebSocket.__initialize);
}
})();
<html>
<head>
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js'></script>
<script src='js/swfobject.js'></script>
<script src='js/FABridge.js'></script>
<script src='js/web_socket.js'></script>
<script>
$(document).ready(function(){
function debug(str){ $("#debug").append("<p>" + str); };
ws = new WebSocket("ws://localhost:3001/");
ws.onmessage = function(evt) { $("#msg").append("<p>"+evt.data+"</p>"); };
ws.onclose = function() { debug("socket closed"); };
ws.onopen = function() {
debug("connected...");
};
});
</script>
</head>
<body>
<div id="debug"></div>
<div id="msg"></div>
</body>
</html>
#!/usr/bin/env ruby
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))
require 'rubygems'
require 'em-websocket'
require 'em-hiredis'
require 'thin'
require 'noah'
## Uncomment the following to hardcode a redis url
#ENV['REDIS_URL'] = "redis://localhost:6379/0"
EventMachine.run do
# Passing messages...like a boss
@channel = EventMachine::Channel.new
Thin::Server.start Noah::App
r = EventMachine::Hiredis::Client.connect
r.psubscribe("noah.*")
r.on(:pmessage) do |pattern, event, message|
@channel.push "(#{event}) #{message}"
end
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 3001) do |ws|
ws.onopen {
sub = @channel.subscribe { |msg|
ws.send msg
}
@channel.push "#{sub} connected and waiting...."
ws.onmessage { |msg|
@channel.push "<#{sub}>: #{msg}"
}
ws.onclose {
@channel.unsubscribe(sub)
}
}
end
end
......@@ -19,6 +19,7 @@ module Noah
set :logging, true
set :raise_errors, false
set :show_exceptions, false
set :run, false
end
configure(:development) do
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment