Monday, March 21, 2016

Amphibian.com's got Web Sockets

Today's comic might look like a simple joke about planting light bulbs, but it is really so much more. Well, actually it's only a little bit more. But still more.

The lamp post in the third frame can be turned on and off by clicking on it. But it doesn't just go on and off for you the clicker; it goes on and off for everyone looking at the comic. Try it. Go to Amphibian.com and look at the lamp. Call a friend on the other side of the world and have them also go to Amphibian.com and look at the lamp. Click on the lamp. You'll both see it toggle state at the same time.

I finally got around to integrating Web Sockets via Socket.io into the site. The state of the lamp is stored on the server and clicks emit "lamp-toggle" events from the clients. When a toggle event is received on the server, the state of the lamp switches and the new state (on or off) is broadcast out to all the clients. The clients show or hide the "glow" effect accordingly.

Server Code:

var lampOn = true;

io.on("connection", function (socket) {

    socket.on("lamp-toggle", function(data) {

        if (lampOn) {
            io.emit("lamp-off");
        } else {
            io.emit("lamp-on");
        }

        lampOn = !lampOn;

    });

});

Client Code:

var socket = io("http://amphibian.com");

socket.on("lamp-off", function(d) { 
    $("#glow").hide();
});

socket.on("lamp-on", function(d) {
    $("#glow").show();
});

$("#lamp").click(function() {
    socket.emit("lamp-toggle");
});

On the server side, I had a little trouble with the fact that I have Node HTTP servers for both secure and insecure web traffic in the application. I wanted the same Socket.io instance to service both. Fortunately, it is easy to attach a Socket.io server to multiple HTTP servers. In the code snippet below, I call attach on an existing Socket.io instance to bind it to a second HTTP server (line 17).

var app = express();

var server = http.createServer(app).listen(3000, function() {
    console.log('listening on port %d', server.address().port);
});

// create a Socket.io server attached to the HTTP server just created
var io = require('socket.io')(server);

if (ssl) {

    var secureServer = https.createServer(sslOptions, app).listen(4443, function() {
        console.log('listening securely on port %d', secureServer.address().port);
    });

    // attach the Socket.io server to the secure HTTP server as well
    io.attach(secureServer);

}

Back in the client, I just had to be smart about which URL to connect to. I never got Web Sockets to work correctly with Nginx, so I'm just bypassing it when I make the connections back from the client. Nginx listens on ports 80 and 443 and proxies HTTP traffic to the Node application which actually listens on ports 3000 and 4443. When I set up Socket.io in the client, I use these ports directly. Browsers still allow Web Socket connections to different ports on the same server without raising cross-site scripting concerns. But you can't mix insecure HTTP and secure Web Sockets (or vice versa). Look at this update to the client code from above, where I examine the browser's location to determine what URL I should use for connecting:

var urlParts = window.location.href.split("/");
var cUrl = urlParts[0] + "//" + urlParts[2];
if (!window.location.port) {
    if (urlParts[0] !== "https:") {
        cUrl += ':3000';
    } else {
        cUrl += ':4443';
    }
}

var socket = io(cUrl);

// ... rest of client code from above ...

So go play around with that lamp! And feel free to read more comics while you're there.

Amphibian.com comic for 21 March 2016