Need help to verify if this socket.io code will work on multiple server instances - node.js

Imagine there is a Node.js chatting app.
It is going to be hosted on AWS that will scale across multiple instances.
The chat server app has this piece of codes that tracks who is online or offline using a variable called "sockets".
This code works on single instance. But will this code fail when the server is scaled across multiple instances?
var sockets = {};
io.configure(function () {
...
});
io.sockets.on('connection', function(socket) {
socket.on('online', function(data){
if(data.userId){
sockets[userId] = socket;
console.log("user " + userId + " connected.");
}
});
socket.on('disconnect', function () {
for(var key in sockets){
if(sockets[key] == socket)
{
delete sockets[key];
console.log("user " + key + " disconnected.");
break;
}
}
});
});
// check if a userId is connected
// will this still work when multiple server instances
function isUserIdOnline(userId)
{
return (sockets[userId] != null);
}

No, it will not work with multiple instances. sockets represent only the users who connected to this particular server, not all the other servers.
The likely answer to this question involves storing who is connected and which server they are connected to in a central database server so any socket server can find out if someone is connected and which server they are connected to by contacting the central database. Since the data is temporal, an in-memory database like redis could be used.
A realated discussion of various architectural options for using multiple servers to handle connections while solving the "who is online problem and how do I contact them" is in this answer.

Related

Adding a user to a room connected to a different server with Node, SocketIO and Redis

I am working on writing server-side code in node.js for a swift based iOS application. Currently, the code works when running it on one EC2 instance, but I am working on setting up a network load balancer so that it can more appropriately scale with incoming user traffic. I decided that the easiest way to achieve this is to use the redis adapter. So now, my server.js file includes:
const app = new Koa();
const server = http.createServer(app.callback));
const io = require('socket.io')(server);
const redisAdapter = require('socket.io-redis');
io.adapter({ host: 'my-elasticache-redis-endpoint', port: 6379 })
Based on the documentation, this seemed like the only step that was necessary from a code standpoint to get redis actually working in the project, but I may be missing something. From an architecture perspective, I enabled sticky sessions on the target group and set up two servers, both running this code. In addition, if I print out the socket io information, I can see that it has adequately connected to the redis endpoint.
The issue is as follows. Lets say I have two people, Person A and Person B, each connected to different servers. The application is supposed to function like so:
Person A adds person B to a socket room. Then the server emits an event to everyone in that room saying that person B has joined, so the front end can respond accordingly.
This is done through the following function:
protected async r_joinRoom(game: GameEntity, player: PlayerEntity): Promise<void> {
return new Promise((res, rej) => {
let socket: any;
socket = this._io.sockets.connected[player.socket_id];
if (!socket) {
socket = this._socket;
}
socket.join(`game/${game.id}`, (err: any) => {
if (err) {
return rej(new GameError(`Unable to join the room=${game.id}.\n${err}`));
}
res();
});
});
}
The premise here is that Person B is a player, and as a player, he has an associated socket id that the server is keeping track of. I believe the issue, however, is that socket = this._io.sockets.connected[player.socket_id]; Does not find the connected player, because he is technically connected to a different server. Printing out the socket shows it as null, and if I subsequently have that exact same function run on the server player B is connected to, he joins the room no problem. Therefore, when the emitted events takes place following 'adding' person B to the room, only person A's phone gets the event, and not B. So is this an issue with my Redis setup? Or is there a way to get all the connected clients to any of the servers running the node.js app?
I ended up answering my own question. When you add to the room, you have to do it directly from the adapter. From the documentation, that means I would switch socket.join... to
io.of('/').adapter.remoteJoin('<my-id>', 'room1', (err) => {
if (err) { /* unknown id */ }
// success
});
using that remoteJoin function worked off the bat

Persiste NodeJS Sockets

Hello community,
Since the morning I am faced with an idea not to say a problem I want to store clients (sockets) in order to re-invoke them later, I know that it is not too clear I will explain in detail:
First I have a server interface (ServerSocket) net.Server() which will receive clients (sockets) net.Socket() and this one will be stored in a Map() with a unique ID for each client, and each time I want to communicate with one of them I call map.get(id).write ... or other function,
Everything works fine until I close the server, automatically the sockets will be killed ... saying that I have found a solution to store the clients for my case (vuex) or localStorage to simplify what I want is when I restart the server and I invoke one of the client it will always be active.
So my main questions:
How can I keep clients still active after the server is closed?
How can I Store sockets and check if they are active after restarting server?
var net = require("net");
var server = new net.Server();
var sockets = new Map();
/**
* This events is called when server is successfully listened.
*/
server.on("listening", () => {
console.log("Server Listen in port 4444");
});
/**
* This events is called when error occur in server
*/
server.on("error", (e) => {
if (e.code === "EADDRINUSE") {
console.log("Address in use, retrying...");
}
});
/**
* This events is called when a new client is connected to server.
*/
server.on("connection", (socket) => {
var alreadyExist = false;
sockets.forEach((soc) => {
if (soc.remoteAddress === socket.remoteAddress) {
alreadyExist = true;
}
});
if (alreadyExist) {
socket.end();
} else {
socket.setKeepAlive(true, Infinity);
socket.setDefaultEncoding("utf8");
socket.id = nanoid(10);
sockets.set(socket.id, socket);
socket.on("error", (e) => {
console.log(e);
if (e.code === "ECONNRESET") {
console.log("Socket end shell with CTRL+C");
console.log("DEL[ERROR]: " + socket.id);
}
});
socket.on("close", () => {
console.log("DEL[CLOSE]: " + socket.id);
});
socket.on("end", () => {
console.log("DEL[END]: " + socket.id);
});
socket.on("timeout", () => {
console.log("timeout !");
});
var child = sockets.get(res.id);
child.write(/* HERE I SEND COMMAND NOT IMPORTANT ! */);
socket.on("data", (data) => {
console.log("Received data from socket " + data);
});
}
});
How can I keep clients still active after the server is closed?
A client can't maintain a connection to a server that is not running. The client is free to do whatever it wants on its own when the server shuts down, but it cannot maintain a connection to that server that is down. The whole definition of a "connection" is between two live endpoints.
How can I Store sockets and check if they are active after restarting server?
You can't store sockets when the server goes down. A socket is an OS representation of a live connection, TCP state, etc... When the server goes down and then restarts, that previous socket is gone. If it wasn't closed by the server before it shut-down, then it was cleaned up by the OS when the server process closed. It's gone.
I would make a suggestion that you're asking for the wrong thing here. Sockets don't outlive their process and don't stay alive when one end of the connection goes down.
Instead, the usual architecture for this is automatic reconnection. When the client gets notified that the server is no longer there, the client attempts to reconnect on some time interval. When, at some future time, the server starts up again, the client can then connect back to it and re-establish the connection.
If part of your connection initiation is an exchange of some sort of clientID, then a client can reconnect, present it's identifier and the server can know exactly which client it is and things can then continue as before with a new socket connection, but everything else proceeding as if the previous socket connection shut-down never happened. You just rebuild your Map object on the server as the clients reconnect.
For scalability reasons, your clients would generally implement some sort of back-off (often with some random jitter added) so that when your server comes back online, it doesn't get immediately hammered by hundreds of clients all trying to connect at the exact same moment.

Socket.io 'Handshake' failing with cluster and sticky-session

I am having problems getting the sticky-sessions socket.io module to work properly with even a simple example. Following the very minimal example given in the readme (https://github.com/indutny/sticky-session), I am just trying to get this example to work:
var cluster = require('cluster');
var sticky = require('sticky-session');
var http = require('http');
if (cluster.isMaster) {
for (var i = 0; i < 4; i++) {
cluster.fork();
}
Object.keys(cluster.workers).forEach(function(id) {
console.log("Worker running with ID : " +
cluster.workers[id].process.pid);
});
}
if (cluster.isWorker) {
var anotherServer = http.createServer(function(req, res) {
res.end('hello world!');
});
anotherServer.listen(3000);
console.log('http server on 3000');
}
sticky(function() {
var io = require('socket.io')();
var server = http.createServer(function(req, res) {
res.end('socket.io');
});
io.listen(server);
io.on('connection', function onConnect(socket) {
console.log('someone connected.');
socket.on('sync', sync);
socket.on('send', send);
function sync(id) {
socket.join(id);
console.log('someone joined ' + id);
}
function send(id, msg) {
io.sockets.in(id).emit(msg);
console.log('someone sent ' + msg + ' to ' + id);
}
});
return server;
}).listen(3001, function() {
console.log('socket.io server on 3001')
});
and a simple client:
var socket = require('socket.io-client')('http://localhost:3001');
socket.on('connect', function() {
console.log('connected')
socket.emit('sync', 'secret')
});
The workers start up fine. The http servers work fine. But when the client connects, the console logs 'someone connected' and nothing more. The client never fires the on connect event, so I think the upgrade/handshake is failing or something. If anyone can spot what I am doing wrong that would help alot.
Thanks!
#jordyyy : I was facing same issue after googling I have fond answer.
Socket.Io handshaking task complete in more than one request and when you will run on sticky session it means you are using multiple process according to your core.
So handshaking request will distribute on different different process and they can't talk.(not IPC) (They are child process) and most of time connection will be failed/lost.(connection-disconnect event occurs frequently )
So what is solution ? Solution is socketio-sticky-session
Socketio-sticky-session, manage connection on IP based. So when you will request by any client then it will maintain ip address with respect process/worker. So further request will be forward to same process/worker and your connection properly stabilized.
And When you will use redies adapter then you can actually maintain socket
connection data b/w all processes/workers.
For more information
https://github.com/elad/node-cluster-socket.io
(you need some patch on worker_index method, if your server is supporting IPv6)
Just knowledge bytes. :) :)
One more thing, you don't need to fork process. It will be done by sticky session.
This was super old and wasn't really answered when i needed it, but my solution was to drop this bad module and any other super confusing module and just use pub/sub with redis adapter. The only other step was to force transports to websockets, and if that bothers anyone then use something else. For my purposes my solution was simple, readable, didn't mess with the 'typical' socket.io api, and best of all it worked extremely well.

Security on Socket.io

Using Socket.io what is the best way to require some sort of authentication for pushing to the socket. For example I have one group listeners who should only be allowed to received updates and I have admins who are allowed to push updates (and listen).
I could use some sort of passphrase that only the "admin" knows.
I could use separate sockets for pushing vs listening and block all listeners to the pushing port
?
Otherwise the listeners would just need to come up with some creative javascript in order to do the correct push. We can also assume that the server doesn't know if someone is an admin or a listner.
For example:
//Server
socket.on('foo', function (data) {
io.sockets.emit('message', data);
}
//listner client
socket.on('message', function (data) {
alert("admin sent new update");
});
//admin client
someButton.onclick = sendMessage = function() {
socket.emit('foo', {'update'});
};

Hosting multiple instances of a node.js server

I'm new to node.js and I'm working on learning how to use Socket.io to create multiple chat servers on my domain.
Here's the scenario:
you log onto the site
you pick a chat room or create a chat room
you join that individual chat room while other chat rooms are going on at the same time
Pretty standard operation on the web but I have yet to find a way to do it. Specifically, how to host it on your domain.
When creating and testing I always just use my localhost and tell the server to listen(8000) . However, how do write a script that:
A) creates a new listening port dynamically for each new chat sever?
B) how do I host it (I use Hostmonster)?
Instead of creating a separate server for each chat room, you could run all of them from the same server and just maintain a map of chat room name to the sockets involved in it.
For example,
//store a map of chat room name to sockets here
var chatRooms = {};
io.sockets.on('connection', function (socket) {
//when someone wants to join a chat room, check to see if the chat room name already exists, create it if it doesn't, and add the socket to the chat room
socket.on('joinChatRoom', function (data.message) {
var chatRoomName = data.message;
chatRooms[chatRoomName] = chatRooms[chatRoomName] || [];
chatRooms[chatRoomName].push(socket);
//set the chatRoomName into the socket so we can access it later
socket.set( "chatRoomName", chatRoomName, function() {
//when we receive a message
socket.on( "chatMessage", function(data) {
var chatMessage = data.message;
//figure out what chat room this socket belongs to
socket.get( "chatRoomName", function(err,chatRoomName) {
//iterate over the sockets in the chat room and send the message
chatRooms[chatRoomName].each(function( chatRoomSocket ) {
chatRoomSocket.emit("chatMessage", { message : chatMessage } );
});
});
});
});
});
});
Note, this code is untested and is just an idea (you should probably treat it more like pseudocode). There are a bunch of things it doesn't handle like cleanup upon disconnects, errors, etc. There are probably lots of other (and better) ways to accomplish this too but hopefully it'll give you some more ideas.

Resources