Sunday, February 8, 2015

Setting up a Node.js server in Cloud9 that can communicate with clients via websockets

This one took me a while to figure out, so I figured I would share it here.
  1. Make an account on Cloud9 (if you don't already have one), and log in.
  2. Go to the Cloud9 homepage and click on Dashboard in the top right corner.
  3. Click on Create New Workspace, the green button to the right, then Create a New Workspace, (not Clone From URL).
  4. Name your workspace, set Hosting to hosted, click on Node.js in the menu below, then click Create.
  5. You'll be redirected to your Dashboard. On the left you will see your project with a loading thing next to it. Wait for your project to be created. It might help to refresh the page as well.
  6. Click on your project, then press Start Editing.
  7. Click on Window at the top, then press New Terminal
  8. Type in the terminal "npm install --save ws". See ws for more information on this Node.js websocket library.
  9. Double click on Server.js in the navigation bar to the left, then paste in the Server.js code below.
  10. Press Run, the green arrow at the top of the screen.
    • The server starts via the current file that you have open, so make sure Server.js's contents are displayed before pressing run.
    • If you accidentally run when selecting, say, an .html file, an Apache server will start up. 
    • If this happens, stop the server, then go to your terminal and type "/etc/init.d/apache2 stop". This is needed because the Apache server keeps running in the background otherwise.
    • Then when you go and start your server again after having Server.js's contents displayed, it will blow up. Stop the server and restart it, and then it will work fine. No idea why this happens but restarting it seems to work.
    • Sometimes Cloud9 claims it "lost connection" and everything freezes up. Simply refresh the page and this should fix it. I think this has to do with internet browsers trying to "optimize" by not letting Cloud9 do some stuff when it's not the active tab.
  11. Open http://jsfiddle.net/wfvok3bd/, and modify :"ws://appName.userName.c9.io/" to your domain name, which you can find in the console below in Cloud9 once you press Run. Specifically you will see something like "Your code is running at https://appName.userName.c9.io/." You might need to scroll up to find this. Make sure that you specifically put "ws://appName.userName.c9.io/" with the / at the end.
  12. Run the jsfiddle, and it should open, send "yo wazzup" to the server, and the server will echo this message and send a "yo wazzup" back to the client. You should the messaged received on the server console and in the jsfiddle output box.  Then you are done.
Server.js code:
var WebSocketServer = require('ws').Server
  , wss = new WebSocketServer({ port: process.env.PORT });



wss.on('connection', function connection(ws) {
  console.log("connection opened");
  ws.on('message', function incoming(message) {
    console.log("connection has message: " + message)
    ws.send(message);
    
  });
  ws.on('close', function closeSocket() {
    console.log("connection closed");
  });
  ws.on('error', function socketError() {
    console.log("connection has error");
  });
});
Alternatively:
var WebSocketServer = require('websocket').server;
var http = require('http');

var server = http.createServer(function(request, response) {
    console.log((new Date()) + ' Received request for ' + request.url);
    response.writeHead(404);
    response.end();
});
server.listen(process.env.PORT, function() {
    console.log((new Date()) + ' Server is listening on port 8080');
});

var wsServer = new WebSocketServer({
    httpServer: server,
    autoAcceptConnections: false
});

function originIsAllowed(origin) {
  // put logic here to detect whether the specified origin is allowed.
  return true;
}

wsServer.on('request', function(request) {
    if (!originIsAllowed(request.origin)) {
      // Make sure we only accept requests from an allowed origin
      request.reject();
      console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
      return;
    }

    var connectionAccepted = false;
    try {
        var connection = request.accept('echo-protocol', request.origin);
        connectionAccepted = true;
    } catch (e) {
        console.log("Invalid protocol given");
    }
    if (connectionAccepted)
    {
        console.log((new Date()) + ' Connection accepted.');
        connection.on('message', function(message) {
            if (message.type === 'utf8') {
                console.log('Received Message: ' + message.utf8Data);
                connection.sendUTF(message.utf8Data);
            }
            else if (message.type === 'binary') {
                console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
                connection.sendBytes(message.binaryData);
            }
        });
        connection.on('close', function(reasonCode, description) {
            console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
        });
    }
});
The second one is from here. I'm listing it as well because I can't seem to get ws working with http servers, while websocket does.

Note that you will need to type "npm install websocket" into the terminal before use.

A protocol is also used in this example code, so you will also need to modify
new WebSocket("ws://appName.userName.c9.io/");
to
new WebSocket("ws://appName.userName.c9.io/", "echo-protocol");
see http://jsfiddle.net/jraqx1ye/.

There is nothing special about a protocol except that all incoming connections that aren't using this protocol will be ignored by the server (and it will log "Invalid protocol given").

Here's an example of how to make a chat server:
// Remember that we can't simply log connection because I guess internally it has a reference to itself, see http://stackoverflow.com/questions/4816099/chrome-sendrequest-error-typeerror-converting-circular-structure-to-json

var WebSocketServer = require('websocket').server;
var http = require('http');
var express = require('express');
var app = express();

var server = http.createServer(app, function(request, response) {
    console.log((new Date()) + ' Received request for ' + request.url);
    response.writeHead(404);
    response.end();
});
server.listen(process.env.PORT, function() {
    console.log((new Date()) + ' Server is listening on port 8080');
});

var wsServer = new WebSocketServer({
    httpServer: server,
    autoAcceptConnections: false
});

function originIsAllowed(origin) {
  // put logic here to detect whether the specified origin is allowed.
  return true;
}

var connections = [];
var curConnectionID = 0;

function SendMessageToEveryone(messageSending)
{
    for(var i = 0; i < connections.length; i++)
    {
        connections[i].sendUTF(messageSending);
    }
}

function ProcessClientMessage(connection, clientMessage)
{
    if(!connection.yamsInfo.hasUsername && clientMessage === "Username: ")
    {
        connection.sendUTF("Username may not be blank")
    }
    
    else if(!connection.yamsInfo.hasUsername && clientMessage.length > 10 && clientMessage.substring(0, 10) === "Username: ")
    {
        var curUsername = clientMessage.substring(10);
        var uniqueUsername = true;
        for(var i = 0; i < connections.length; i++)
        {
            if(curUsername === connections[i].yamsInfo.username)
            {
                uniqueUsername = false;
            }
        }
        
        if(uniqueUsername)
        {
            connection.yamsInfo.username = curUsername;
            
            connection.sendUTF("Name is unique");
            SendMessageToEveryone(curUsername + " connected.");
            connection.yamsInfo.hasUsername = true;
        }
        else
        {
            connection.sendUTF("Someone else already has that username")
        }
    }
    else if(connection.yamsInfo.hasUsername && clientMessage.length >= 9 && clientMessage.substring(0, 9) === "Message: ")
    {
        SendMessageToEveryone("Message from " + connection.yamsInfo.username + ": " + clientMessage.substring(9));
    }
}

function ProcessClientBinaryData(processClientBinaryData)
{
    
}

// Todo - Figure out why https:// doesn't work

// Much of this is from http://stackoverflow.com/questions/14273978/integrating-websockets-with-a-standard-http-server
wsServer.on('request', function(request) {
    if (!originIsAllowed(request.origin)) {
      // Make sure we only accept requests from an allowed origin
      request.reject();
      console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
      return;
    }

    var connectionAccepted = false;
    try {
        var connection = request.accept('chat-server', request.origin);
        connectionAccepted = true;
    } catch (e) {
        console.log("Invalid protocol given");
    }
    if (connectionAccepted)
    {
        // Using this name so we're (fairly) sure it doesn't override anything else, sorry it's painful
        connection.yamsInfo = {id: curConnectionID, hasUsername: false};
        connections.push(connection);
        curConnectionID++;
        connection.sendUTF("Connection accepted");
        
        console.log((new Date()) + ' Connection accepted.');
        connection.on('message', function(message) {
            if (message.type === 'utf8') {
                console.log('Received Message: ' + message.utf8Data);
                ProcessClientMessage(connection, message.utf8Data);
            }
            else if (message.type === 'binary') {
                console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
                ProcessClientBinaryData(message.binaryData);
            }
        });
        connection.on('close', function(reasonCode, description) {
            console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
            
            // Remove connection from connection list
            for(var i = 0; i < connections.length; i++)
            {
                if(connections[i].yamsInfo.id == connection.yamsInfo.id)
                {
                    connections.splice(i, 1);
                    i = i - 1;
                }
            }
            
            if(connection.yamsInfo.hasUsername)
            {
                SendMessageToEveryone(connection.yamsInfo.username + " disconnected.")
            }
            
        });
    }
});


var fs = require('fs');


// This fetches html files from the client folder (if they exist), and returns a "Page could not be found" error otherwise (this can be customized to some other 404 error page as desired)
app.get('*', function (req, res) {

    var urlReading = req.url;
    if (urlReading === "/")
    {
        urlReading = "/index.html";
    }
    urlReading = __dirname + "/client" + urlReading;

    console.log("Loading: " + urlReading);

    fs.readFile(urlReading, function (err, html) {
        if (err) {
            console.log("Could not find " + urlReading)
            res.writeHead(200, { 'Content-Type': 'text/html' });
            res.end("Page could not be found

Page could not be found

"); } else { console.log("Found " + urlReading) res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); } }); });

For the client, see http://jsfiddle.net/ocgpos98/. You can put this in an index.html in your client folder in cloud9 (just make sure to put the script in the head and include jsquery), and that will cause it to load as your default page. You can even just put it in an html file on your desktop and run it, it will still work as normal.

If you make your server hold this client code, then give a client that requests https://appName.userName.c9.io/page.html this client code, you'll find that this example work. If you change it to http instead of https it will though.