User:Laurier Rochon/prototyping/asyncprog

From XPUB & Lens-Based wiki

Laurier's adventures into event-driven programming, node.js, non-blocking servers, socket.io, now.js and other fun configuration purgatories.

Well, in preparation for the final project, I've been spending considerable amounts of time looking into a few technologies that will surely come in handy to build this mofo. Simply put, I need a system that takes incoming data from clients, gets processed by server(s) and then notifies other client(s) back in real-time. Do to this, I need a push mechanism rather than a traditional request/response model, and the clients and server should not need to know about each other - they should just care about the protocol they use to communicate with each other. So I've been building :

  • A mobile phone client (Android) to let you select a contact (java)
  • A storage server that handles the mobile client requests, stores them and creates a request to the push server (python, sqlite3)
  • A push server that takes incoming requests and pushes to all connected client's sockets (node.js)
  • A front-end for the actual app (the slot machine) (html/css/js)
  • An API to expose the DB data, accessible from javascript (added Access-Control-Allow-Origin: *) to headers


Technologies

  • Java/eclipse/android-sdk and whatever else needed to make an Android app. Not. Fun.
  • Node.js, which is a server-side implementation of Google's V8 javascript engine for Chrome. It allows you to run an event-driven, non-blocking, single-thread server that handles a bunch of concurrent connections easily.
  • Now.js (a library that bolts onto node.js that gives you a 'now' object that is shareable between client and server...bit weird) and socket.io which is really useful to write back and forth to sockets. Sockets is getting really popular with HTML5 these days on the browser level, but it turns out that Heroku (the main hosting platform for node.js projects, with nodester) doesn't support sockets at all. Meh.
  • nvm and npm, node package managers
  • 'express', yet another damn framework, this time for node (node is very recent still - you have to download, configure, make, install it - and very basic things like including JS and CSS files is practically impossible without a framework. hopefully this will change in time...)

{{#ev:youtube|jo_B4LTHi3I|640}}


And then, just to be fair I suggest you read the hilarious article on node.js by Ted Dziuba : Node is Cancer -> http://teddziuba.com/2011/10/node-js-is-cancer.html


First slot machine prototype

Ss slot machine.png

The first iteration of my HTML5 js slot machine...needs a bunch more bells, whistles, lights and obnoxious shit, but it can spin, slow down and tell you if you've won! (logs in the console). Lots#1 of jQuery and other cutting-bleeding-edge stuff that most people won't have on their browsers : will put a warning if html5-less browser is detected.


Deploying on a server

I actually tried to deploy my app on the PZI server - node.js would compile, socket.io wouldn't. Other than that, I think there's only 2 or 3 companies that offer node.js hosting, including

  • nodester
  • heroku
  • some other guys I can't remember, powered by nodester basically

1 and 3 are in private beta. As the invite email is taking a long time (2 days later, still no news), I just went on Heroku. I was a bit eager to see it run non-dev as this project is kind of useless if it can only run locally. Deploying was a lot of config and many steps (you need to use git. you need to have foreman installed to test. you need to scale your processes. you need a Procfile. you need a packages.json for depencies. blablabla). So here's what I set out to make, more or less.

Img d.jpg

and the actual demo is here! Nice huh? Nothing happens.... So, to activate, you need to make a post request to the server. Open up a terminal and type

UPDATE : have just hooked up the main PZI server component to Heroku after much cross site scripting issues. PZI now check DB records against what you've sent and takes care of pushing the next POST to Heroku.

It's a bit slow...gotta check that out.

curl -d "phone_id=some_unique_id&contact_name=Bobby&contact_number=+3101020304" http://pzwart3.wdka.hro.nl/~lrochon/cgi-bin/facilitator/gateway.cgi

And check the page!Spin spin spin...You can also open a bunch of browsers, they should all get the signal, as the app running on Heroku catches the request and writes back to the client sockets. that's it!

You can also curl that sucker a bunch of times, and your credit adds up on the right. Of course this is just demoing...


A bit of software

The main difficulty in building something like this is usually to get the environment and configurations right, the programming itself is not massive. Here are a few of the files

  1. the main js server, running off node
var express = require('express')
  , routes = require('./routes')

var app = module.exports = express.createServer();

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(require('stylus').middleware({ src: __dirname + '/public' }));
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 
});

app.configure('production', function(){
  app.use(express.errorHandler()); 
});



// Routes

app.get('/', routes.index);

var port = process.env.PORT || 3000;

app.listen(port);
console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);

io = require('socket.io').listen(app);

io.configure(function () { 
  io.set("transports", ["xhr-polling"]); 
  io.set("polling duration", 10); 
});

app.post('/', function(request, response){
  console.log("got a post");
  response.writeHead(200, {'Content-Type': 'text/plain'});
  io.sockets.emit('new_credit', "some data in here!");
  response.end();
});


  1. simple front-end part that catches the socket write
  var socket = io.connect();
  socket.on('new_credit', function (data) {
    console.log(data);
    addcredit()
  });


  1. the slot machine, adapted from someone else's
    var showingMsg = false
    var credit = Array()
    
    function slideup(t){
        $(currentE).slideUp("fast",function(){
            showingMsg = false
        });
    }
    
    function addcredit(){
        str = "John Smith (+31 06 12 34 56 78)"
        credit.push(str)
        $("#notifications").html("Got new payment of "+str+". Starting slots...")
        $("#notifications").slideDown("fast",function(){
            showingMsg = true
            currentE = this
            setTimeout("slideup()",4000)
        });
        refreshStats(str)
    }
    
    function refreshStats(str){
        $("#nbcredit").html(credit.length)
        $("#list ul").html("")
        $.each(credit,function(i,value){
            $("#list ul").append("<li>"+value+"</li>")
        })
    }

$(document).ready(function () {

    $("#applink").slideDown("fast")
    $("#notifications").slideDown("fast",function(){
        showingMsg = true
        currentE = this
        setTimeout("slideup()",3000)
    });

    function randomRange(minNum, maxNum) {
	    var multiplier = maxNum-minNum+1;
	    return Math.floor(Math.random()*multiplier+minNum);
    }

    $('.l').each( function(){
	    var currentList = $(this)
	    var firstItem = currentList.children('li:first')
	    firstItem.clone().appendTo( currentList )
	    var myList = currentList.children('li')
	    var listHeight = -( firstItem.outerHeight() * ( myList.length-1) )
	    currentList.css('margin-top',listHeight)
		
    })


    function playSlots() {
	    isPlaying = true
	    sevenCount = 0
	    finishedCount = 0
	    results = Array()
	    
	    $('.l').each(function(index){
		
		    var currentList = $(this)
		
		    var firstItem = currentList.children('li:first')
		    var myList = currentList.children('li')
		    var listHeight = -( firstItem.outerHeight() * ( myList.length-1) )
		
		    var spinSpeed = 200
		
		    function lowerSpeed() {
			
			    if ( spinSpeed < 1000 ) {
				    spinSpeed +=200
				    spinEm()
			    } else {
				
				    spinSpeed +=200
				    var myNum = randomRange(1, (myList.length-1) )
								
				    var finalPos = - ( (firstItem.outerHeight() * myNum)-firstItem.outerHeight() )
				    var finalSpeed = ( (spinSpeed * .5) * (myList.length-1) ) / myNum				
				    results[index] = finalPos
				
				    function checkWinner() {
					    finishedCount++
					    if(index==$(".l").length-1){
                            if(results[index]==results[index-1] && results[index]==results[index-2]){
                                console.log("WINNAR!")
                            }
                            setTimeout(flipIsPlaying,1000)
                        }
				    }
				    currentList.css('margin-top',listHeight).animate( {'margin-top': finalPos}, finalSpeed, 'swing', checkWinner)
			    }
		    }
		    function flipIsPlaying(){
		        isPlaying = false
		    }
		    function spinEm() {
			    currentList.css('margin-top',listHeight).animate( {'margin-top': '0px'}, spinSpeed, 'linear', lowerSpeed )
		    }
		    spinEm()
	    });
    }

    isPlaying = false

    function checkbalance(){
        if(credit.length>0 && isPlaying==false && showingMsg==false){
            credit.pop()
            refreshStats()
            playSlots()
            
        }
    }

    timer = setInterval(checkbalance,500)


})
  1. some simple store/retrieve /w python and mysqlite
#!/usr/bin/python

import sqlite3 as lite
import sys
import urllib2
import json

#db settings
location = 'data'
con = lite.connect('data')

#get params
get =  urllib2.unquote(sys.argv[1]).decode('utf8')
params = get.split("---");
print params

#db cursor
cur = con.cursor()    

#insert into DB
if len(params) >=3:
	with con:
		cur.execute("CREATE TABLE IF NOT EXISTS contacts(id INTEGER PRIMARY KEY, phoneid TEXT, name TEXT, number TEXT, used INT)")
		cur.execute("SELECT count(*) FROM contacts WHERE phoneid = ? AND name = ? AND number = ?", (params[2],params[0],params[1],))
		nb=cur.fetchone()[0]
		if(nb==0):
			cur.execute('INSERT INTO contacts VALUES (NULL, ?, ?, ?, ?)',(params[2],params[0], params[1], 0))
			con.commit()
			print "Accepted your payment of "+params[0]+" ("+params[1]+"). Look up!"
		else:
			#already exists
			print "This contact has already been used. Choose another one."
else:
	#clear the table
	if params[0]=="clear":
		cur.execute("DROP TABLE IF EXISTS contacts;")
		con.commit()
		print "Cleared contacts table"
	#return all new entries in JSON format (for javascript)
	if params[0]=="retrieve":
		cur.execute("SELECT * FROM contacts WHERE used=0")
		rows = cur.fetchall()
		for row in rows:
			 cur.execute("UPDATE contacts SET used=? WHERE id=?", (1, row[0]))
			 con.commit()
		print json.dumps(rows)
  1. I also have an android app up and running that posts data to the server...will show some later.

Lots more work on the way...