Web Sockets

This is a fun time to be a web developer. We are getting some really great tools to make some really cool sites. We'll now be able to create desktop like applications in the browser without relying on third party technologies that users have to install like Java or Silverlight. One of those tools is web sockets.

Web sockets allow full-duplex communication over TCP. What does that mean? It means we'll always have an open channel for the client (browser) to talk to the server and vice verca using events. Now we get to do some really cool things that don't need setTimeout() or long polling ajax. For instance we could now make an interactive site with multiple clients that reacts to changes in real time.

And that's just what we're going to make.

Socket.IO

The NodeJS module Socket.IO lets us take advantage of web sockets with fallbacks built in should the client not support web sockets. This makes using sockets so easy you'll want to use them all the time. So let's get started on our app.

Draw Together

To demonstrate the basics of how to use Socket.IO and web sockets we're going to create a drawing application using the HTML5 canvas. When a client draws on the canvas it will show up on all of the other connected client's canvases.

The Basics

All we're going to use for this is Socket.IO so we'll create our package.json file and add the following:



    
{

  "name": "DrawTogether",

  "version": "0.0.1",

  "description" : "A drawing application to interact in real time with other people",

  "dependencies": {

    "socket.io" : "*"

  }

}

Save that in your project folder and in your terminal run npm install.

Now we'll create a pretty basic app structure. In you project folder add an index.html file and open it up to add the following markup:

<html>



  <head>

    <title>Draw Together</title>

    <script src="/socket.io/socket.io.js"></script>

  </head>

  <body>

    <canvas id="drawCanvas"></canvas>

  </body>

  <script>

      window.onload = function() {

          var socket = io.connect("http://localhost");

      };

      </script>

</html>

Save that and in your root project directory create the app.js file and add the following:

 
//app.js

var http = require('http'),

    fs = require('fs'),

    html = fs.readFileSync('index.html', 'utf8'),

    htmlLength = Buffer.byteLength(html, 'utf8');



function handler(req, res) {

    res.setHeader('Content-Type', 'text/html');

    res.setHeader('Content-Length', htmlLength);

    res.end(html);

}



var app = http.createServer(handler),

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



// Listen for a client to connect

io.sockets.on('connection', function(socket) {

    console.log('Someone Connected!');

    // Listen for disconnect event for the given socket (client).

    socket.on('disconnect', function(socket) {

        console.log('Someone Disconnected!');

    });

});



app.listen(3000);

Save that and then open your terminal and start with node app.js. If all is well you should see 'Draw Something' when you navigate to localhost:3000 and in the console it should read 'Someone Connected!' along with some Socket.IO stuff.

If you're used to Node already most of this makes sense but if not we'll go through it now. At the top of the file we're reading in our index.html page and holding it in the variable html and we also keep the length of the file. The handler function will handle incoming http requests serving up our loaded html file.

The Canvas

Inside the js directory we're going to make our drawTogether.js file which will be our client side app. I'm going to use the algorithm I found in this Stackoverflow answer to actually draw on the canvas.

//drawTogether.js

(function(app, io){

var socket,

    canvas,

    ctx,

    painting = false,

    lastX = 0,

    lastY = 0,

    lineThickness = 1,

    fillStyle = '#fff';



var draw = function(x, y, prevX, prevY, fillColor) {

        ctx.fillStyle = fillColor;

    // find all points between

    var x1 = x,

        x2 = prevX,

        y1 = y,

        y2 = prevY;



    var steep = (Math.abs(y2 - y1) > Math.abs(x2 - x1));

    if (steep){

        var x = x1;

        x1 = y1;

        y1 = x;



        var y = y2;

        y2 = x2;

        x2 = y;

    }

    if (x1 > x2) {

        var x = x1;

        x1 = x2;

        x2 = x;



        var y = y1;

        y1 = y2;

        y2 = y;

    }



    var dx = x2 - x1,

        dy = Math.abs(y2 - y1),

        error = 0,

        de = dy / dx,

        yStep = -1,

        y = y1;



    if (y1 < y2) {

        yStep = 1;

    }



    lineThickness = 5 - Math.sqrt((x2 - x1) *(x2-x1) + (y2 - y1) * (y2-y1))/10;

    if(lineThickness < 1){

        lineThickness = 1;

    }



    for (var x = x1; x < x2; x++) {

        if (steep) {

            ctx.fillRect(y, x, lineThickness , lineThickness );

        } else {

            ctx.fillRect(x, y, lineThickness , lineThickness );

        }



        error += de;

        if (error >= 0.5) {

            y += yStep;

            error -= 1.0;

        }

    }

};



app.initialize = function(canvasId) {

    // Get the canvas and context

    canvas = document.getElementById(canvasId);

    ctx = canvas.getContext('2d');

    canvas.width = canvas.height = 600;

    ctx.fillRect(0, 0, 600, 600);



    // Initialize our functions to listen for events

    canvas.onmousedown = function(e) {

        painting = true;

        lastX = e.pageX - this.offsetLeft;

        lastY = e.pageY - this.offsetTop;

    };



    canvas.onmouseup = function(e) {

        painting = false;

    };



    canvas.onmousemove = function(e) {

        if(painting) {

            var mouseX = e.pageX - this.offsetLeft,

                mouseY = e.pageY - this.offsetTop;

            draw(mouseX, mouseY, lastX, lastY, fillStyle);

            lastX = mouseX;

            lastY = mouseY;

        }

    }



  };

})(window.drawTogether = window.drawTogether || {});

In our initialize function we accept the canvas id and are setting up the event listeners and calling the draw function when the mouse button is down and the mouse is moving. draw() is taking the current mouse position and the previous mouse positions along with a fill color for the line we draw. Lets try this out. In your index.html file add the following below the canvas element:

<script src='drawTogether.js'></script>

<script>

  window.onload = function() {

      var socket = io.connect("http://localhost");

      if(drawTogether) {

          drawTogether.initialize('drawCanvas');

      }

  }

</script>

Since we're not using Express we're going to have to handle serving the drawsomething.js file ourselves. We're already doing that for the HTML file so its simple enough just a few more lines of code and a couple modules.

//app.js

var http = require('http'),

    parse = require('url').parse,

    ....

    dtJS = fs.readFileSync('drawtogether.js', 'utf8'),

    dtJSLength = Buffer.byteLength(dtJS, 'utf8');



...

function handler(req, res) {

    var url = parse(req.url);

    if(url.pathname === '/drawtogether.js') {

        res.setHeader('Content-Type', 'application/javascript');

        res.setHeader('Content-Length', dtJSLength);

        return res.end(dtJS);       

    }

....

}

Up in our variable declarations we're adding the url module that will allow us to check paths. After that we're reading in our drawsomething.js file and getting they byte length. Then inside our handler function we're checking to see if the client is requesting the drawsomething.js file in which case if they are we'll send it on over and return so we don't send the HTML back over too.

Now when you reload app.js and refresh the browser you should see a black box which you can draw on with your mouse.

Add Some Friends

Now that socket.io is going lets write the code to actually draw with some other people. I mean this isn't call "Draw Alone" am I right? (insert bad joke eel)

In app.js at your variable declerations add the following code:

//app.js

....

// hold some colors

colors = ['#3399FF', '#FF0066', '#00FF00', '#FFF'],

colorIndex = 0;



...

console.log('Somone Connected');



socket.emit('color', { hex: colors[colorIndex] });

colorIndex = (colorIndex > (colors.length - 1)) ? 0 : colorIndex + 1;



socket.on('draw', function(data) {

    socket.broadcast.emit('draw', data);

});

Now when someone connects we use the emit function to send them an event called 'color' which will give them a color from our array via JSON. Then we attach a callback to a 'draw' event. The draw event will be called when someone is drawing on the canvas. We use broadcast.emit to send that data to the other clients. It is important to note that we used broadcast because we only want to send it to other clients and not the current one who sent the data.

All that is left is to edit the drawTogether.js file to send and receive drawing data. Remove the io connection from the onload function in index.html and we'll put it in drawTogether.js along with some socket event emitters and handlers.

//drawTogether.js

app.initalize = function(canvasId) {

    ....

    if(io) {

        socket = io.connect('http://localhost');

        // Get our color from the server

        socket.on('color', function (data) {

            fillStyle = data.hex;

        });

        //Get incoming draw data.

        socket.on('draw', function(data) {

            draw(data.x, data.y, data.prevX, data.prevY, data.color);

        });

    }

  ...

    canvas.onmousemove = function(e) {

        ...

        draw(mouseX, mouseY, lastX, lastY, fillStyle);

        // Send our draw data to server

        if(socket) {

            socket.emit('draw', {x: mouseX, y: mouseY, prevX: lastX, prevY: lastY, color: fillStyle });

        }

        ...

};

})(window.drawTogether = window.drawTogether || {}, io);

We should be all good. To test this out restart the application and open up your browser and a new private browser session. When you draw on the canvas you should see it appear on the other browser windows' canvas.

Mess Around

Hopefully now you see how easy it is to create real-time web applications with socket.io and the possibilities you could use it for. Now go create some awesome app with it.

You can find the files I created for this application here. If you want to see a working demo, check it out here.

Thanks for reading!