Dad72

Create a Cluster Server with Node.js and socket.IO

Recommended Posts

It is regularly ask so I suggest that I use as server NodeJS + socket.IO. this therefore create a master server and workers according to the number of heart of the machine.
This solution allows to distribute the load.
 
Here is the Serveur.js
 

var cluster  = require('cluster'), _portSocket  = 8080, _portRedis   = 6379, _HostRedis   = 'localhost';

if (cluster.isMaster) {	
	var server = require('http').createServer(), socketIO = require('socket.io').listen(server), redis = require('socket.io-redis');	
	socketIO.adapter(redis({ host: _HostRedis, port: _portRedis }));
	
	var numberOfCPUs = require('os').cpus().length;
	for (var i = 0; i < numberOfCPUs; i++) {
		cluster.fork();		
	}
	
	cluster.on('fork', function(worker) {
        console.log('Travailleur %s créer', worker.id);
    });
    cluster.on('online', function(worker) {
         console.log('Travailleur %s en ligne', worker.id);
    });
    cluster.on('listening', function(worker, addr) {
        console.log('Travailleur %s écoute sur %s:%d', worker.id, addr.address, addr.port);
    });
    cluster.on('disconnect', function(worker) {
        console.log('Travailleur %s déconnecter', worker.id);
    });
    cluster.on('exit', function(worker, code, signal) {
        console.log('Travailleur %s mort (%s)', worker.id, signal || code);
        if (!worker.suicide) {
            console.log('Nouveau travailleur %s créer', worker.id);
            cluster.fork();
        }
    });
}

if (cluster.isWorker) {	

	var http = require('http');
	
	http.globalAgent.maxSockets = Infinity;	
	
	var app = require('express')(), ent = require('ent'), fs  = require('fs'), server = http.createServer(app).listen(_portSocket), socketIO = require('socket.io').listen(server), redis = require('socket.io-redis');
	
	socketIO.adapter(redis({ host: _HostRedis, port: _portRedis }));
	
	app.get('/', function (req, res) { res.emitfile(__dirname + '/interface.php');});
	
	socketIO.sockets.on('connection', function(socket, pseudo) {

		socket.setNoDelay(true);
		
		socket.on('nouveau_client', function(pseudo) {
			pseudo = ent.encode(pseudo);			
			socket.pseudo = pseudo;
			try {
				socket.broadcast.to(socket.room).emit('nouveau_client', pseudo);
			} catch(e) {
				socket.to(socket.room).emit('nouveau_client', pseudo);
			}
			console.log('L\'utilisateur : '+socket.pseudo+' s\'est connecter');
		});	

		socket.on('message', function(data) {
			socket.broadcast.to(socket.room).emit('dispatch', data);
		});	

		socket.on('exit', function(data) { socket.close();});
		
		socket.on('room', function(newroom) {
			socket.room = newroom;
			socket.join(newroom);	
			console.log('Le membre '+socket.pseudo+' a rejoint le domaine '+socket.room);
			socket.broadcast.to(socket.room).emit('dispatch', 'L\'utilisateur : '+socket.pseudo+' a rejoint le domaine : '+socket.room);
		});
	});
	
}

And to install Node:
 

Quote

sudo apt-get install wget  or apt-get install wget
wget http://nodejs.org/dist/v0.10.22/node-v0.10.22.tar.gz
tar xfz node-v0.10.22.tar.gz
cd node-v0.10.22
./configure
make
make install

 and install Redis server:
 

Quote

sudo apt-get install redis-server

 
and modules:
 

Quote

npm install cluster
npm install express
npm install ent
npm install fs
npm install socket.IO
npm install socket.io-redis
npm install forever -g (forever start server.js) = Run as a service nodejs

 The server runs.  :D   ;)
 
------------------------
Client:

<head>
  <script type="text/javascript" src="http://localhost:8080/socket.io/socket.io.js"></script>
</head>
  <body>
    <script>
    var socket = null;
try {	
  socket = io.connect();
     console.log("socket: Ok!");
    }
catch(err) {
  console.error("Socket is out of service!");
}	
if(socket != null) {
  socket.emit('new_client', 'admin');
  // use variable php (COOKIES, SESSION...)	
  socket.on('message', function(data) {	
    $('#zone_chat').prepend('' + data.pseudo + ': ' + data.message + '<br />');
  });	
  socket.on('new_client', function(pseudo) {	
    $('#zone_chat').prepend('<em>' + pseudo + ' a rejoint le chat !</em><br />');
  });	
  socket.on('moveObjet', function(data) {
    mesh = scene.getMeshByName(data.name);
    mesh.position = new BABYLON.Vector3(data.position);		
    mesh.rotation = new BABYLON.Vector3(data.rotation);	
  });
}
</script>
</body>

The client runs.   :D    ;)

Share this post


Link to post
Share on other sites

Waaow nice  :)  :)  :)

Socket.io is a really a good websocket library especialy server side.

As it can manage xhr fallback if WS can't connect, it is very versatile.

Sometimes too much as you would prefer never to fallback to xhr polling when coding a game and force WS connection : xhr are just http, so too much overhead for often no useful data transfered, browser Same Origin Policy limited (whereas you can have your web server and your websocket server under different domain names with WS), etc

So Socket.io client xhr features are unnecessery imo as the genuine HTML5 WS API is enough (only my opinion)

Not that important if you really force Socket.io to use WS connexions only.

 

I can't understand in your server side code where you implemented the game session notion.

In other terms, if I code a script (not a part of your game, just a baddy script :angry: ) connecting your WS server and if I start to emit (after having sent a 'pseudo' message) many many 'moveObject messages, I will probably spam every connected player and make the game unplayable.  :ph34r:

 

Imho, the server code should check if any incoming message is to be treated or dropped.

A kind of unpredictable pre-shared  game session token should be given to each authorized client (players only), and they should sent it back to the server in each emited message. Then the server would filter : messages containing the token are legitimate, others are dropped (this token can be set in WS sub-protocol header, for instance)

This is a http-session-like mechanism.

Unless Socket.io abstracts this behavior and I don't know it.  ;)

 

Other mechanisms like public/private keys with live encryption/decryption client and server sides are possible too, but so complex to handle they aren't probably worth it for just to protect you game for illegitimate incoming messages.

Share this post


Link to post
Share on other sites

The client-side communication to send and receive :

<script type="text/javascript" src="http://localhost:8080/socket.io/socket.io.js"></script>var socket = io.connect('http://localhost:8080'); // créer la session du joueur sur le serveur par un COOKIE créer et récupérer avec PHP et la créer ici sur le serveur : socket.set('pseudo', pseudo);socket.emit('nouveau_client', '<?php echo $_COOKIE['pseudo'];?>'); // Quand on reçoit un message, on l'insère dans la pagesocket.on('message', function(data) {     $('#zone_chat').prepend('' + data.pseudo + ': ' + data.message + '<br />');}); // Quand un nouveau joueur entre sur le jeusocket.on('nouveau_client', function(pseudo) {    $('#zone_chat').prepend('<em>' + pseudo + ' a rejoint le jeu !</em><br />');}); socket.on('moveObjet', function(data) {    data.objet.position = new BABYLON.Vector3(data.position);    data.objet.rotation = new BABYLON.Vector3(data.rotation);});

for the secutity of message: ent.encode(message);

 

socket.emit  to send to the server
and socket.on to receive from the server to each client

Share this post


Link to post
Share on other sites

Cool !

I can see you send some cookie to the server indeed. Right.  :)

 

But I still think your server code should filter messages and accept only authorized clients.

Maybe is it already the case and I can't understand it reading the server side code  :( : does the encode function manage the cookie value and drop the incoming message if the cookie is unknown ?

BTW, your implementation is really nice imho because it's light, KISS and scalable.  ;)

 

heuu don't we drift far away from BJS here ? ;)

 

shameless references, in french, about websockets if you need :

http://jerome.bousquie.fr/ws_slides.pdf

http://jerome.bousquie.fr/ws_article.pdf

Share this post


Link to post
Share on other sites

In fact I manage the client connection via PHP and SQL querying a database. if it connects it is a client authorized for me.

 

I think Babylon should be able to handle the network. what would be the icing on the cake and would this engine (babylon) the best engine in the world WebGL.  :D Maybe one day

Share this post


Link to post
Share on other sites

In fact I manage the client connection via PHP and SQL querying a database. if it connects it is a client authorized for me.

 

But what if I don't authenticate via PHP ?

 

Imagine I am a baddy just wanting to have your online game down because I'm jealous of your success  ;) :

I just could code a tiny script on my laptop to connect directly to your websocket server. This script could then emit infinitly 'moveObject' messages to your game server program which will handle them as I were a legitimate authenticated player, nope ?

 

This is the point I wanted to draw your attention to. :huh:

I think your WS server lacks of some kind of game session filtering.

Maybe am I wrong ? I just can see where you reject/drop illegitimate incoming messages.

 

Except this small but important lack imho, your implementation seems really good and smart to me anyway :) .

It seems to be designed to only one big simultaneous game session, doesn't it ?

Share this post


Link to post
Share on other sites
You have reasons, it may be missing the listed security in case the person arrives on the play. But to reach the game requires a connection, so I did not worry about that side secutity. but it can be done later if necessary.

 


Basically I have an index page that allows connection and redirects to the game. at that time I record the client to the server. Then I can add later the player of banishment in case of bad user, but it is not yet integrated into the server. maybe I would edit this later here when I should be there.


 

Yes, it's a pretty big game. An adventure MMORPG with a publisher to integrate that each player can create his own world and a social network integrated into the game world to share their creations with quests ... ambitious project, but I like challenges.

 

 

Share this post


Link to post
Share on other sites

Nice, really nice  :)

I strongly recommend you to have this incoming message filtering if you want your game to have a commercial use... or simply to avoid attacker pollution during game sessions.

 

WS protocol does know nothing about application level session : the WS server will accept by default any incoming connection.

 

Arrf, my job is to deploy server services and to set up their security. Internet is not a quite place : run a public server, it will be attacked in the first minute !

That's why I focus on this, just to alert you on this very risk.  ;)

Share this post


Link to post
Share on other sites
Updating the server. it seems that there have been changes since the update to 1.2 socket.IO

 

set() and get() are now depreciate. before we could create a session variable on the server like this:

socket.set ('pseudo', 'dad72');

 

but now it suffice to simply:

socket.pseudo = 'dad72'

 

Same for get () is now made directly

if (socket.pseudo)

Share this post


Link to post
Share on other sites

I have a problem:
 

RedisStore = require('socket.io/lib/stores/redis'),
redis      = require('socket.io/node_modules/redis');
 
RedisStore does not exist.
 
I installed redis but the path is "root/node_modules/redis".
 
Why use the path socket.io?
 
 
 
--------------------
Returns many times this error "WebSocket connection to 'ws://MyIp:8520/socket.io/?EIO=3&transport=websocket&sid=HsbslLeePELkFMehAAAA' failed: Connection closed before receiving a handshake response"
 
(8520 is the port that i choose)
 
Why?

Share this post


Link to post
Share on other sites

Yes i installed redis (sudo apt-get install redis-server).

 

If i write redis-server in command line this is the response (http://s15.postimg.org/kjrj6zosr/image.png).

 

 

The path "RedisStore = require('socket.io/lib/stores/redis')," gives me an error (http://s9.postimg.org/d9lam45n3/image.png).

 

Therefore i installed "npm install socket.io-redis --save" and changed this code:
 

RedisStore = require('socket.io/lib/stores/redis'),

 

redis      = require('socket.io/node_modules/redis');
 
in this:
RedisStore = require('socket.io-redis'),
  redis      = require('redis');
 
but gives me this error: " WebSocket connection to 'ws://MyIp:8520/socket.io/?EIO=3&transport=websocket&sid=raU53papomMA0BuCAAAA' failed: Connection closed before receiving a handshake response"
 
MyIp isn't localhost but the ip of server to external access.
 
 

Share this post


Link to post
Share on other sites

"Connection closed before receiving a handshake response"

Seems your websocket server isn't running or isn't accessible from your client (proxies ? firewalls ?). The handshake is the first stage of the connection before upgrading http protocol to ws protocol.

please have a look at this tl;dr academic article (french) : http://jerome.bousquie.fr/ws_article.pdf

just read the very last annex page (english) to check if you are in the case of traversing proxies or firewalls and how to workaround

Share this post


Link to post
Share on other sites

For example, even if I do not open the web page where run the ws command sometimes appears to me also "Connected to worker: 2".

 

I believe that it should not do it if I do not open the page, right?

 

 

---------------------------

 

Yes, but i always use not localhost but the ip of the server and the port like 40.50.60.20:8520

is right?

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.