Saturday, February 23, 2013

Node.js and Socket.io Allow Frogs to Play Together

It can be lonely out there for frogs. They need to interact with other frogs, and not just on Facebook. In the real world. But this isn't the real world, it's a virtual world that is supposed to be sorta like the real world. For frogs. Let's just say that if you want to connect multiple client browsers together to create some sort of online frog collaboration environment, there is no better way than with Node.js and Socket.io.

FFZ is where I play around with browser tech like WebSockets, Canvas, and HTML5 Audio. I like to make things with frogs in them. In this application, you can move a frog around a small world filled with trees, flowers, rocks, and streams. But it's even more fun when another person is on the site at the same time you are - you'll play in a collaborative environment. You will see their frog move in real-time and they'll see yours. It amuses my young children for hours. Just press "R" to ribbit.

How do I hook everybody's browser up together? Well first of all, don't expect it to work with Internet Explorer. Seriously. IE, how can you even show your face in public anymore? Chrome and Firefox work like browsers should. I'm talking WebSockets. Sure, you can do WebSockets the old-fashioned way...but Socket.io is a great utility that abstracts much of the complexity away from you. It's built for Node.js, the awesome server-side Javascript engine. Node allows you to hook up tons of clients together with very little overhead because of its event-driven IO model.

My FFZ "server" is a Node.js program that listens for events from the client browsers running the application and publishes events out to the clients as well. When one frog moves, it publishes its changed position to the server, which turns around the publishes it to all the other browsers so they can update the positions of that frog on their screens. And thanks to Socket.io, it's extremely simple.

Here's my Node server code:


var http = require("http");
var sockio = require("socket.io");

var frogs = [];

var io = sockio.listen(8080);
io.configure(function() {
    io.set('log level', 2);
    io.set('transports', [
      'websocket'
    ]);
});

io.sockets.on("connection", function(socket) {
      
    socket.on("frogmove", function(fdata) {
        socket.broadcast.emit("fm", fdata);
    });

    socket.on("objmove", function(odata) {
        socket.broadcast.emit('sm', odata);
    });
      
    socket.on("ribbit", function(fdata) {
        socket.broadcast.emit("rbbt", fdata);
    });
      
    socket.on("startup", function(msg) {
        console.log("frog " + msg.fid + " connected");
        socket.broadcast.emit("newfrog", msg.fid);
 socket.set("frog id", msg.fid);
 frogs.forEach(function(i) {
            socket.emit("newfrog", i);
 });
 frogs.push(msg.fid);
    });
      
    socket.on("disconnect", function() {
        socket.get("frog id", function(err, fid) {
            console.log("frog " + fid + " disconnected");
     socket.broadcast.emit("byefrog", fid);
         if (frogs.indexOf(fid) != -1) {
                frogs.splice(frogs.indexOf(fid), 1);
            }
 });
    });

});


So hopefully the first part of the code is fairly self-explanatory. In the configuration of the socket listener, I set the "transports" to be just "websocket" because I don't want it to automatically downgrade to Flash or long-polling. Those things are fine I suppose but I wanted to use FFZ to try out WebSockets. (I actually did try Flash and long-polling with FFZ. Flash is ok but long-polling just doesn't work with an application like this - there was just too much data being transmitted and the user experience was poor.)

The second block is where the real magic happens. On a "connection" event, we set up the event listeners for that socket. The first three events are very simple. When a socket, which represents a connection to a client, gets a "frogmove" event, for example, it just turns around and broadcasts it out again. The "broadcast" method sends a message to all known sockets except oneself - so the sender isn't going to get the message back but every other socket that Socket.io knows about will get it. This is how I handle the simple events - moving frogs and objects and making frogs ribbit.

The "startup" event gets a little more complicated. When a new client connects, they need to let all the other clients know to add them to their screens, but the new client also needs to know where all the existing frogs are so they can be added to their screen. So here I use the "socket.broadcast.emit" method again to tell everyone else about the new frog - and then I use "socket.emit" to send a "newfrog" event back to the socket that just joined. This is a bit of a trick because these aren't exactly new frogs (they existed before the new client joined) but the client will behave the same way as if a new frog joined - by adding the other frog(s) to the screen and tracking them for future updates. Finally, I add the new client id to the frogs array so the server knows about this frog in the future, at least until it disconnects.

That brings me to the "disconnect" event. Again, I use the broadcast to tell everyone else to stop displaying and tracking the frog that just left. I also then remove it from the frogs array.

So there you have it. A server in Node.js that keeps all the frogs playing nicely together. Easy peasy lemon squeezy. Now let's take a gander at the client code too. It's almost as simple.

The first thing to do in the client is to load the Socket.io code. But it serves itself! That's right, when Socket.io wants a sandwich it makes it and brings it to itself. You just source the JavaScript right from your Node.js server. So for example, if you look at the server code above you will notice that I'm running on port 8080 and I serve everything from amphibian.com. So I have this in my HTML:

<script type="text/javascript" src="http://www.amphibian.com:8080/socket.io/socket.io.js"></script>

Bam! You've got the Socket.io client now! By the way, you can actually serve the client manually if you need to for some obscure reason. See https://github.com/LearnBoost/socket.io-client

This is what I do to get the socket connected and set up the client's frog to publish events:


<script type="text/javascript">

sckt = io.connect("http://www.amphibian.com:8080");

sckt.on("connect", function() {

    console.log("socket connected");
    frog.bind('move', function(fdata) {
        // fdata will have the frog's id and position info
        sckt.emit("frogmove", fdata);
    });

    frog.bind('ribbit', function(fdata) {
        // fdata will have the frog's id and position info
        sckt.emit("ribbit", fdata);
    });

});

</script>


The "connect" call is fairly simple, you just give it the URL. Then you get up your connect event callback. When the "connect" event occurs, I bind some events on the client's frog to functions which will emit data over the socket. (I use MicroEvent.js to do this. You can read more about that here. It is awesome.) This works pretty much the same way as on the server side. Calling "emit" with an event name and some data sends that event+data to the server where (hopefully) there is a callback set up listening for that event. So in the code above, I emit the "frogmove" event and the "ribbit" event, both of which are listened for in the server code shown previously.

Remember that the server essentially rebroadcasts events from one client to all the other clients using custom event names. I'll just show you one here, the "rbbt" event, but the others are all very similar.


<script type="text/javascript">

sckt.on("rbbt", function(fdata) {
    for (var f = 0; f < otherfrogs.length; f++) {
        if (otherfrogs[f].uid == fdata.id) {
            otherfrogs[f].ribbit();
        }
    }
});

</script>

If you look at the server code and the first part of the client setup, you'll see that when a client's frog ribbits the event is published over the socket as the "ribbit" event. The server gets that and republishes to all other clients as a "rbbt" event. I removed the vowels. Servers don't like to broadcast vowels. No, that statement is completely false. Really I just wanted a slightly different event name so I could tell the client and server events apart. Anyway, this is an example of the client listening for the "rbbt" custom event. If this client gets such an event it means that some other client's frog is making a sound and so we should show that frog opening his mouth and play the sound as well. The data that comes in will have the other frog's id in it so we just look for which frog it should be and then call the "ribbit" method on that frog.

Couldn't be easier, right?

And there's even better news! What if you wanted to hook up your OUYA game to a Node.js server using Socket.io? I know I do! There is a Java client available for Socket.io that works on Android. So technically you could hook up web browsers, mobile phones, game consoles, and possibly even refrigerators all to the same server to share data in real-time. Yes, Node.js most assuredly rocks.


No comments:

Post a Comment