While working at Motorola, the JavaScript code I wrote did a lot of communication with embedded devices via Web Sockets. Because the work by the embedded side was done in parallel to my work, I typically didn’t have access to the latest version, let alone a physical device. So I needed to be able to test, demo, and seamlessly integrate my work when we were ready. One of the major lessons I took from this is that, heavy front-end development should be agnostic to the back end.
This is where I love working with a tool like Node.js. You can use and develop using simple tools without having a backend/database connection and test all of your scenarios, and give a good demo. Likewise you can mock out your data so that it tests all of your edge cases and fail conditions easily. The best part is that it is self contained, and can be passed from developer to developer.
So what I want to show is how to create a super simple Node.js web socket test server that is then easily portable to the actual production back-end. I started my project using the yeoman Angular generator, however it isn’t necessary to use either for what I am going to show. What is necessary is Node.js, grunt, grunt-contrib-connect (version 0.8 at least), and a web socket node package (I used ws).
If you just want to get the code and work with it then you can download it here.
1. Setup
Starting with the angular-generator, and making sure that I added the 2 packages that referenced above, I modified my project structure just a bit by adding a factories folder in my app/scripts directory to separate simple web socket factory my data access services, and a server folder in my root project directory. In this folder we are going to put all of our test server code.
2. Front-end development
In the factory folder I created a simple angular factory module that pretty much just returns a Web Socket object. The reason I do this mostly has to do with Unit testing. It creates one simple place to grab a web socket instance whenever it is necessary, but most importantly it creates an easy place to use jasmine spies to mock the WebSocket object that comes back in any unit tests that I write.
From there I create a service that will get an instance of the web socket and basically abstracts the communication to and from the WebSocket from the rest of the app. So to the app I expose a way to connect, disconnect, and individual methods for each method type I want to send.
angular.module('blogApp') .factory('socketService', ['socketFactory', '$rootScope', function (socketFactory, $rootScope) { var _socket = null, _transactionCounter = 0, _messageHandlers = {}, Types = { Connect: "CONNECT", Disconnect: "DISCONNECT", SetServerTimer: "SET_TIMER", BroadcastMessage: "BROADCAST_MESSAGE", AsyncTimer: "ASYNC_TIMER", GenericResponse: "GENERIC_RESPONSE" }; function isConnected(){ return _socket !== null && _socket.readyState === WebSocket.OPEN; } function disconnect(callback){ if (isConnected()){ _socket.close(); if (callback){ callback(); } } } function setServerTimer(timeout, times, callback){ sendMessage({type: Types.SetServerTimer, data: {timeout: timeout, times: times} }); } function sendBroadcastMessage(user, message){ sendMessage({type: Types.BroadcastMessage, data: {user: user, message: message}}); } function connect(callback){ if (!isConnected()){ _socket = socketFactory.createSocket(); _socket.onopen = function onOpen(){ if (isConnected()){ sendMessage({type: Types.Connect, data: "Hello World"}, callback); } }; _socket.onerror = function onError(){ console.log("Error"); }; _socket.onmessage = onMessageRecieved; _socket.onclose = function onClose(){ //goodnight socket _socket = null; $rootScope.$broadcast(Types.Disconnect); }; } } function sendMessage(message, callback){ if (!isConnected()){ return false; } message.transactionId = _transactionCounter++; if (callback != null){ _messageHandlers[message.transactionId] = callback; } _socket.send(angular.toJson(message)); return message.transactionId; } function onMessageRecieved(evt){ var message = angular.fromJson(evt.data); $rootScope.$apply(function(){ if (message.transactionId != null && _messageHandlers[message.transactionId] != null){ _messageHandlers[message.transactionId](message); _messageHandlers[message.transactionId] = null; } $rootScope.$broadcast(message.type, message); }); } // Public API here return { isConnected: isConnected, disconnect: disconnect, connect: connect, setServerTimer: setServerTimer, sendBroadcastMessage: sendBroadcastMessage, Types: Types }; }]);
Each message to the service is stringified and parsed as JSON, but you can use pretty much any format. We used protobuf at Motorola which has an open source implementation. Each message also has a transaction Id and the ability to register a callback from it. So if you have a traditional send & receive message this will work for you. The server will send the appropriate response as necessary with the same transaction ID so it will know what callback to use.
You may notice that it also has a reference to the $rootScope. The reason why is when data comes back for the response above it is wrapped in an $apply call. The $apply call notifies angular to check the watches and to update the UI as appropriate. Most NG-events or methods such as ng-click or $http do this for you already, but there isn’t anything for web sockets.
Also we want to have a way that any $scope in the system can listen for a specific message in case there is an asynchronous message from the server or some event that is initiated from the server (such as a message from a different logged on user). To do this I an utilizing the angular $broadcast method that will notify any listeners for the new data and let them handle it as they need to.
Finally I am going to add a simple controller that has a reference to this service and some buttons to perform the appropriate actions (I won't paste that all here).
3. Test Server
So now that I have this front-end code I need something to actually hit. I recommend writing unit tests for most of your code, and the socketFactory will help you with that, but it doesn’t help you see an interactive version of the code. To get that we will need an actual server.
In my server folder that I created earlier I am now creating a socketServer.js class. I want to handle the connection/disconnecting from the socket and mock out the data being sent and any responses necessary.
Using ws I will create a socket server on the port I specified earlier. So I create message handlers for connection, close and message events.
var ws = require('ws'); var SOCKET_PORT= 55800, Types = { Connect: "CONNECT", Disconnect: "DISCONNECT", SetServerTimer: "SET_TIMER", BroadcastMessage: "BROADCAST_MESSAGE", AsyncTimer: "ASYNC_TIMER", GenericResponse: "GENERIC_RESPONSE" }; var sockets = []; exports.startServer = function(server, connect, options){ // open a websocket with said server. var commandSocket = new ws.Server({port:SOCKET_PORT}); commandSocket.on("connection", function(socket){ var _socket = socket; sockets.push(socket); console.log("Socket Connected"); _socket.on("close", function(){ console.log("Socket Closed"); //remove from the list of sockets for(var i = 0; i < sockets.length; i++){ if (_socket == sockets[i]){ sockets.splice(i, 1); break; } } _socket = null; }); _socket.on("message", function(data, flags){ var message = JSON.parse(data); console.log("Socket message recieved: " + message.type); switch(message.type){ case Types.Connect: handleConnectMessage(message); break; case Types.SetServerTimer: handleSetServerTimer(message); break; case Types.BroadcastMessage: handleBroadcastMessage(message); break; } }); }); });
In the connection event I will receive an instance of the socket object, so I will hold onto it and use closure to allow everything else have access to it. Likewise I will add it to a running list of open connections that I have going so I can send any messages of a certain type to all active sockets.
In the close event I will set the socket to null, and remove it from the active socket array. Nothing crazy here.
The message event is where we will do the bulk of the work. You can make this part as complicated or simple as possible. I recommend that you keep it really simple for a few reasons. First, you typically don’t really care about the data being sent back or any actual saving of the data. Your front end typically cares that the message got sent correctly, and any responses that happen, do happen. Second, you want to be able to change it easily. The more you add to it the more complicated it will get. You should be able to change the parameters or the message sequences easily, or it defeats the purpose of a quick and dirty server. Finally, your end product is most likely just the front end. Since that is the case you don’t want to spend more time than you need to, when you just want to test the scenarios or demo it to your client or PM.
Finally we need to actually call our mock server. That is where the specific version of gunt-contrib-connect comes in. In the options for the server there is a callback function you can hook into when the server starts called onCreateServer. That callback will pass you the server among with other information so you can hook into it. In our case we just want to call our socketServer.js class and pass in the server.
Finally we need to actually call our mock server. That is where the specific version of gunt-contrib-connect comes in. In the options for the server there is a callback function you can hook into when the server starts called onCreateServer. That callback will pass you the server among with other information so you can hook into it. In our case we just want to call our socketServer.js class and pass in the server.
connect: { options: { port: 9000, // Change this to '0.0.0.0' to access the server from outside. hostname: 'localhost', livereload: 35729, onCreateServer: function(server, connect, options){ var socketServer = require('./server/socketServer.js'); socketServer.startServer(server, connect, options); } },
Before I ended this post, I wanted to just reiterate that the point of this is not to recreate all of the logic of the server, but to make one simple enough that it is easy to change and just big enough to work.
The code is available on Github. Feel free to download it and start playing. Just npm install, bower install, and grunt server and you are good to go.
No comments:
Post a Comment