├── .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 | [](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 |
--------------------------------------------------------------------------------