├── .gitignore ├── Procfile ├── README.md ├── index.html ├── loadtest.js ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | .DS_Store 18 | 19 | strongloop.json 20 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Websocket Server 2 | 3 | This Node application perioducally receives JSON data from another web application, and serves it to clients connected to it. 4 | 5 | # Running Locally 6 | 7 | Install ZMQ first 8 | 9 | ```bash 10 | brew install zeromq 11 | ``` 12 | Alternatively: http://zeromq.org/intro:get-the-software 13 | 14 | ``` bash 15 | sudo npm install 16 | node --harmony server.js 17 | ``` 18 | 19 | Module versions might be old when you install this application, so especially if you get node-gyp compilation errors after installing modules, try updating module versions of related packages on package.json 20 | 21 | ## How it works 22 | 23 | Please see [node-fetcher](https://github.com/denizozger/node-fetcher) and [node-dataprovider](https://github.com/denizozger/node-dataprovider) implementations too, all three applications work together - although not necessarily. 24 | 25 | 1. A client connects to the application. ie. ws://node-websocket/some-key 26 | 2. App checks if it has some-key's data on memory 27 | 3. If some-key's data is on memory already, it serves it to connected client 28 | 4. If some-key's data is not found, then requests it with via a socket from a specific server, ie. [node-fetcher](https://github.com/denizozger/node-fetcher) 29 | 5. Waits to receive data for some-key, over a socket. When data is received, transmits it to clients who are connected via some-key. 30 | 31 | Go to [localhost:5000/?test1](localhost:5000/?test1) for a demo 32 | 33 | When you have all three applications, you should start node-socketio as: 34 | 35 | ``` bash 36 | PORT=5000 node --harmony server.js 37 | ``` 38 | 39 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/denizozger/node-websocket/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 40 | 41 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 39 | 40 | 41 |

Node Websocket Server

42 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /loadtest.js: -------------------------------------------------------------------------------- 1 | // npm install 2 | var WebSocket = require('ws'); 3 | var log = require('npmlog'); 4 | 5 | log.level = 'verbose'; 6 | 7 | var sockets = []; 8 | var maxSockets = 415; // max 400 9 | var connectionAttempts = 0; 10 | 11 | function connectToWebSocket() { 12 | connectionAttempts++; 13 | 14 | var socket = {}; 15 | 16 | var ws; 17 | 18 | (function() { 19 | ws = new WebSocket('http://localhost:5000/matchesfeed/1/matchcentre'); 20 | })(); 21 | 22 | ws.on('open', function() { 23 | log.info('Connected'); 24 | }); 25 | 26 | ws.on('error', function() { 27 | log.error('Error'); 28 | }); 29 | 30 | ws.on('close', function() { 31 | log.warn('Closed'); 32 | }); 33 | 34 | sockets.push(ws); 35 | 36 | if (connectionAttempts < maxSockets) { 37 | setTimeout(connectToWebSocket, 500); 38 | } 39 | 40 | }; 41 | 42 | connectToWebSocket(); 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-websocket", 3 | "subdomain": "node-websocket", 4 | "version": "0.0.1-1", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/denizozger/node-websocket.git" 8 | }, 9 | "dependencies": { 10 | "ws": "0.4.x", 11 | "express": "3.x", 12 | "request": "2.27.x", 13 | "async": "0.2.x", 14 | "zmq": "~2.8.0", 15 | "npmlog": "0.0.6", 16 | "strong-agent": "~1.0.3" 17 | }, 18 | "engines": { 19 | "node": "0.10.x", 20 | "npm": "1.2.x" 21 | }, 22 | "scripts": { 23 | "start": "node server.js" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'), 4 | app = express(), 5 | server = require('http').createServer(app), 6 | WebSocketServer = require('ws').Server, 7 | request = require('request'), 8 | async = require('async'), 9 | zmq = require('zmq'), 10 | log = require('npmlog'); 11 | // strongloop = require('strong-agent').profile(); 12 | 13 | app.use(express.static(__dirname + '/')); 14 | app.use(express.json()); 15 | app.use(express.urlencoded()); 16 | app.enable('trust proxy'); 17 | 18 | log.level = process.env.LOGGING_LEVEL || 'verbose'; 19 | 20 | const port = process.env.PORT || 5000 21 | 22 | server.listen(port, function() { 23 | log.info('Server ' + process.pid + ' listening on', port); 24 | }); 25 | 26 | var webSocketServer = new WebSocketServer({ 27 | server: server 28 | }); 29 | 30 | /** 31 | * Infrastructure settings and data models 32 | */ 33 | var resourceData = {}; // key = resourceId, value = data 34 | var resourceObservers = {}; // key = resourceId, value = [connection1, conn2, ..] 35 | 36 | /** 37 | * Public Endpoints 38 | */ 39 | webSocketServer.on('connection', function (webSocketClient) { 40 | handleClientConnected(webSocketClient); 41 | }); 42 | 43 | function handleClientConnected(clientConnection) { 44 | if (!isValidConnection(clientConnection)) { 45 | clientConnection.disconnect(); 46 | } 47 | 48 | var resourceId = getResourceId(clientConnection); 49 | observeResource(clientConnection, resourceId); 50 | 51 | var existingResourceData = resourceData.resourceId; 52 | 53 | if (existingResourceData) { 54 | sendResourceDataToObserver(clientConnection, resourceId); 55 | } else { 56 | requestResource(resourceId); 57 | } 58 | } 59 | 60 | function observeResource(clientConnection, resourceId) { 61 | var currentResourceObservers = resourceObservers[resourceId] || []; 62 | 63 | currentResourceObservers.push(clientConnection); 64 | resourceObservers[resourceId] = currentResourceObservers; 65 | 66 | logNewObserver(clientConnection, resourceId); 67 | } 68 | 69 | // Publish a resource request for a resrouce that we don't have in memory (ie. in resourceData) 70 | const resourceRequiredPusher = zmq.socket('push').bind('tcp://*:5432'); 71 | // Receive new resource data 72 | const resourceUpdatedPuller = zmq.socket('pull').connect('tcp://localhost:5433'); 73 | 74 | resourceUpdatedPuller.on('message', function (data) { 75 | 76 | handleResourceDataReceived(data); 77 | }); 78 | 79 | function handleResourceDataReceived(data) { 80 | var resource = JSON.parse(data); 81 | log.verbose('Received resource data for resource id (' + resource.id + ')'); 82 | 83 | storeResourceData(resource); 84 | 85 | notifyObservers(resource.id); 86 | } 87 | 88 | 89 | /** 90 | * Implementation of public endpoints 91 | */ 92 | 93 | function sendResourceDataToObserver(clientConnection, resourceData) { 94 | clientConnection.send(resourceData); 95 | } 96 | 97 | function requestResource(resourceId) { 98 | log.silly('Requested resource (id: ' + resourceId + ') does not exist, sending a resource request'); 99 | 100 | resourceRequiredPusher.send(JSON.stringify({id: resourceId})); 101 | } 102 | 103 | function storeResourceData(resource) { 104 | resourceData[resource.id] = resource.data; 105 | 106 | logAllResources(); 107 | } 108 | 109 | function notifyObservers(resourceId) { 110 | var currentResourceObservers = resourceObservers[resourceId]; 111 | 112 | var data = resourceData[resourceId]; 113 | 114 | if (currentResourceObservers) { 115 | 116 | async.forEach(currentResourceObservers, function(thisObserver){ 117 | 118 | if (thisObserver.readyState !== 3) { 119 | sendResourceDataToObserver(thisObserver, data); 120 | } else { 121 | // We need to find the index ourselves, see https://github.com/caolan/async/issues/144 122 | // Discussion: When a resource terminates, and all observers disconnect but 123 | // currentResourceObservers will still be full. 124 | var indexOfTheObserver = getIndexOfTheObserver(currentResourceObservers, thisObserver); 125 | 126 | unobserveResource(currentResourceObservers, resourceId, indexOfTheObserver); 127 | } 128 | }, 129 | function(err){ 130 | log.error('Cant broadcast resource data to watching observer:', err); 131 | }); 132 | } else { 133 | log.verbose('No observers watching this resource: ' + resourceId); 134 | } 135 | } 136 | 137 | function getIndexOfTheObserver(observersWatchingThisResource, observerToFind) { 138 | for (var i = 0; i < observersWatchingThisResource.length; i++) { 139 | var observer = observersWatchingThisResource[i]; 140 | 141 | if (observer === observerToFind) { 142 | return i; 143 | } 144 | } 145 | } 146 | 147 | function unobserveResource(observersWatchingThisResource, resourceId, indexOfTheObserver) { 148 | observersWatchingThisResource.splice(indexOfTheObserver, 1); 149 | 150 | if (observersWatchingThisResource.length === 0) { 151 | removeResource(resourceId); 152 | } 153 | 154 | logRemovedObserver(); 155 | } 156 | 157 | function removeResource(resourceId) { 158 | log.verbose('Removing resource ( ' + resourceId + ') from memory'); 159 | 160 | delete resourceObservers[resourceId]; 161 | delete resourceData[resourceId]; 162 | } 163 | 164 | function getResourceId(clientConnection) { 165 | return clientConnection.upgradeReq.url.substring(1); 166 | } 167 | 168 | function isValidConnection(clientConnection) { 169 | var resourceId = getResourceId(clientConnection); 170 | 171 | if (!resourceId) { 172 | log.warn('Bad resource id (' + resourceId + ') is requested, closing the socket connection'); 173 | return false; 174 | } 175 | 176 | return true; 177 | } 178 | 179 | function closeAllConnections() { 180 | resourceRequiredPusher.close(); 181 | resourceUpdatedPuller.close(); 182 | webSocketServer.close(); 183 | } 184 | 185 | process.on('uncaughtException', function (err) { 186 | log.error('Caught exception: ' + err.stack); 187 | closeAllConnections(); 188 | process.exit(1); 189 | }); 190 | 191 | process.on('SIGINT', function() { 192 | closeAllConnections(); 193 | process.exit(); 194 | }); 195 | 196 | /** 197 | * Logging 198 | */ 199 | 200 | function logNewObserver(clientConnection, resourceId) { 201 | log.info('New connection for ' + resourceId + '. This resource\'s observers: ' + 202 | resourceObservers[resourceId].length + ', Total observers : ', webSocketServer.clients.length); 203 | } 204 | 205 | function logRemovedObserver() { 206 | log.verbose('Connection closed. Total connections: ', webSocketServer.clients.length); 207 | logResourceObservers(); 208 | } 209 | 210 | function logResourceObservers() { 211 | for (var resourceId in resourceObservers) { 212 | if (resourceObservers.hasOwnProperty(resourceId)) { 213 | log.verbose(resourceObservers[resourceId].length + ' observers are watching ' + resourceId ); 214 | } 215 | } 216 | } 217 | 218 | function logAllResources() { 219 | log.silly('Total resources in memory: ' + Object.keys(resourceData).length); 220 | // log.silly(JSON.stringify(resourceData, null, 4)); 221 | } 222 | --------------------------------------------------------------------------------