socket.io and node cluster: dispatch events to sockets - node.js

I have a nodejs cluster server that is using mongo changestream listener, to emit data to clients over socket.io. I'm using Redis to store a userId and the socketId of all the connected users in a hash.
{ userId: 'aaa', socketId: 'bbb' }
The redis clients for storing this data is initialized in the master process.
The mongo changestream is created in the master process.
When the changestream sees a new document, it will send the document to a child process as a message. When the child process receives the message, it can retrieve the userId from the document. With the userId, the socketId for the client connection can be retrieved from redis.
The issue I am having is in trying to emit a message using the socketId after it is retrieved from redis.
I am creating a sockethandler object that contains the socketId. When I use this socketId to emit a socket message, like so:
io.sockets.to(userSocketId)
.emit("confirmOrder", "Your order is being processed!")
I receive an error:
(node:31804) UnhandledPromiseRejectionWarning: Error: The client is closed
at new ClientClosedError (/Users/a999999999/code/*/node_modules/#node-redis/client/dist/lib/errors.js:24:9)
The error is from redis, and originated on the socket emit line written above. ^^
Here is more code from the worker process:
const pubClient = createClient({ host: "127.0.0.1", port: 6379 }),
subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
setupWorker(io);
io.on("connection", (socket) => {
const socketId = socket.id;
socket.emit("connection", "SERVER: you are connected");
socket.on("userConnect", (user) => {
let { userId } = user;
userConnectClient
.HSET(userId, { userId, socketId })
.catch((err) => console.log("ERROR: ", err));
});
});
process.on("message", async ({ type, data }) => {
switch (type) {
case "dispatch:order":
let { order } = JSON.parse(data);
const socketsHandler = await createSocketsHandler(order);
const userSocketId = socketsHandler.user.socketId;
io.sockets
.to(userSocketId)
.emit("confirmOrder", "Your order is being processed!");
break;
}
});
async function createSocketsHandler(order) {
let { userId } = order;
let user = await userConnectClient
.HGETALL(userId)
.catch((err) => console.log(err));
return {
user: user,
};
}
I am temporarily stumped at this point. Currently experimenting with the io object, and trying to find better tools to monitor redis. Any help/questions is appreciated! Thank you!

I've since realized why the redis client was not working properly. I'm making use of publisher and subscriber clients with redis. The problem was, I was creating the redis clients in the worker process of the server. So, whenever the server is making a command to redis, it is not able to do it properly because there is a pair of clients for each worker process, which is not the proper implementation, I believe.
This was solved by creating the redisClient outside of my cluster server code. ;P My server can now properly subscribe to redis client in master and worker process!

Related

Remembering a client even after they disconnect nodeJS socket.io

I have a chat app and currently, the client is saving its name in a text file, this works fine for windows but mac has some weird directory settings so it makes it harder to read the text file. I'm wondering if it's possible that when a client connects my server saves their IP or some sort of constant data about the client so when the client connects again I can know who it is and assign the name accordingly.
I'm using nodeJS socket.io
First thing that comes to my mind is to make the client responsible for identifying itself by sending a generated UUID to server after connecting
socket.on("connect", () => {
const CLIENT_UUID = 'uuid';
// Get client UUID from local storage
let clientUuid = localStorage.getItem(CLIENT_UUID);
// Check whether if this is a new client and it doesn't have UUID
if (!clientUuid) {
// Then generate random UUID, you'll need to implement `generateRandomUuid`
clientUuid = generateRandomUuid();
// Then save it to local storage
localStorage.setItem(CLIENT_UUID, clientUuid);
}
// Then just emit the connected event
// This will be the actual connection event as once this is emitted and received
socket.emit('connected', {
uuid: clientUuid
});
});
Now on server you can handle the client based on it's UUID
io.on('connection', (socket) => {
// Here you handle the `connected` event
socket.on('connected', (clientUuid) => {
// And now you can handle this clientUuid
const chats = getCurrentClientChats(clientUuid);
const groups = getCurrentClientGroups(clientUuid);
socket.emit('current-chats', { chats });
socket.emit('current-groups', { groups });
});
});
Notice that way, you'r client is now actually known on the connected event
Same approach but probably cleaner is to send the client UUID upon connecting to the server, since it's really easy to do that with socket IO
const socket = io("ws://example.com/my-namespace", {
query: { clientUuid }
});
And just use it upon connection
io.on('connection', (socket) => {
const clientUuid = socket.handshake.query.name;
const chats = getCurrentClientChats(clientUuid);
const groups = getCurrentClientGroups(clientUuid);
socket.emit('current-chats', { chats });
socket.emit('current-groups', { groups });
});

Sending a message to a user that is not connected?

I'm trying to create a user to user chat application - no group chat or anything.
I'm using NodeJS and Socket.io on the backend and React Native on the frontend.
I ended up having a Map that stores a user id and it's corresponding socket id, my problem is that only when a user connects to the server, he will get a socket id.
But what if User A is connect and is trying to send a message to User B, and User B is not connected, so it does not have a socket id, I don't really know what to do then.
This is what I got so far:
io.on("connection", (socket: Socket) => {
//Whenever a user will connect, the user will emit 'initiate' to register itself and it's socket id to the server.
//We will be using this userSocketMap to send messages between users.
socket.on(SocketEvents.INITIATE, (data) => {
const uid = data.uid;
const socketID = socket.id;
userSocketMap.set(uid, socketID);
});
//This function will get called when a user is sending message to another user.
socket.on(SocketEvents.SEND, (data) => {
const to = data.to;
const from = data.from;
const content = data.content;
const payload: MessagePayload = {
to: to,
from: from,
content: content,
};
const dstUser = userSocketMap.get(to); // getting the socket id of the receiver.
// '/#' is the prefix for the socketID, if the socketID already has this prefix, this should be deleted - need to check this.
//MessageEvent.RECEIVE is used to signal the dstUser to receive a message.
io.to("/#" + dstUser).emit(SocketEvents.RECEIVE, { payload: payload });
});
socket.on(SocketEvents.DISCONNECT, (socket: Socket) => {
const userID = getByValue(socket.id);
if (!userID) return;
userSocketMap.delete(userID);
});
});
You should do two things when working with react-native and socket.io in case user lost internet connection. Use socket.io heartbeat mechanism inorder to get the users that lost connection and are not responding and user NetInfo package to inform the mobile user that he has lost internet connection.
Socket.io
var server = app.listen(80);
var io = socketio(server,{'pingInterval': 2000});
io.on("connection", (socket: Socket) => {
socket.on(SocketEvents.INITIATE, (data) => {
const uid = data.uid;
const socketID = socket.id;
userSocketMap.set(uid, socketID);
})
socket.on('heartbeat', (socket: Socket) => {
const userID = getByValue(socket.id)
userSocketMap.MARK_USER_AS_INACTIVE(userID)
})
});
React-Native - use NetInfo - it used to be part of the core but got separated to a community module
import NetInfo from "#react-native-community/netinfo";
NetInfo.fetch().then(state => {
console.log("Connection type", state.type);
console.log("Is connected?", state.isConnected);
});
const unsubscribe = NetInfo.addEventListener(state => {
console.log("Connection type", state.type);
console.log("Is connected?", state.isConnected);
});
// Unsubscribe
unsubscribe();

How to trigger websocket send from another function in Node?

It's been a while since I've worked with Node and Websockets. Basically how do I get socket.send() to work from another function is what I'm stuck on.
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', socket => {
socket.on('message', message => {
console.log(`received from a client: ${message}`);
});
socket.send('yo world!');
});
function onMessageHandler (target, context, msg, self) {
client.say(target, response);
server.socket.send(response);
console.log(response);
}
}
How do I get my onMessageHandler to trigger a socket send, this is fail... server.socket.send(response);
Seeing your question i think there is a lack of understanding on how Websockets work. I am assuming you're using https://github.com/websockets/ws
There are two things. First is the WebSocketerver which you've named as server and then an Individual Socket which you've named as socket
Now the thing to understand is socket is not accessible outside server.on() callback The reason for this is there could be 1000 of sockets connected at a given instance and there would be no way to uniquely identify a particular socket you want to send message to.
So ask yourself the question that your application wants to send message to an individual socket to send to everyone who is connected to your server (basically broadcast)
If you want to send to an individual, you will have to uniquely identify the user
this._wss = new WebSocket.Server({
port: ENV_APP_PORT_WS
});
this._wss.on("connection", async (ws: AppWebSocket, req: IncomingMessage) => {
// const ipAddress = req.connection.remoteAddress; // IP Address of User
logger.info(req);
const queryParams = url.parse(req.url, true).query;
let authUser: User;
try {
authUser = await this._authenticateWebSocket(queryParams);
} catch (e) {
// Terminate connection and return...
}
// WS User INIT
ws.isAlive = true;
ws.userId = authUser.id;
ws.uuid = Helpers.generateUUIDV4();
ws.send(JSON.stringify({
type: "connected",
env: ENV
}));
});
The above code will add a property to each socket object that will enable it to uniquely identify a particular user/socket.
While sending =>
onMessageHandler(targetUserId: number, message: string) {
const allSockets = <AppWebSocket[]>Array.from(this._wss.clients.values());
const targetSocket = allSockets.find(w => w.userId === targetUserId);
targetSocket.send(message);
}
If You want to send to all connect users, it's quite easy:
https://github.com/websockets/ws#server-broadcast

Socket.io keeps multiple connected sockets alive for 1 client

I am using Socket.io to connect a React client to a Node.js server and the query option in socket.io to identify uniquely every new client. However, the server creates multiple sockets for every client and, when I need to send something from the server, I don't know which socket use, because I have more than one, and all of them are connected.
The client code:
import io from "socket.io-client";
...
const socket = io(process.env.REACT_APP_API_URL + '?userID=' + userID, { forceNew: true });
socket.on('connect', () => {
socket.on('new-order', data => {
const { add_notification } = this.props;
add_notification(data);
});
The server code:
....
server = http
.createServer(app)
.listen(8080, () => console.log(env + ' Server listening on port 8080'));
io = socketIo(server);
io.on('connection', socket => {
const userID = socket.handshake.query.userID;
socket.on('disconnect', () => {
socket.removeAllListeners();
});
});
And here the server-side that emits events to the client:
for (const socketID in io.sockets.connected) {
const socket = io.sockets.connected[socketID];
if (socket.handshake.query.userID === userID) {
// Here, I find more than one socket for the same condition, always connected.
socket.emit(event, data)
}
}
Here, it is possible to see all these socket for the same client:
I tried to send events for all socket from a given userID, however, multiple events are triggered to the client, showing duplicated data to the user. I also tried to send events to the last socket, but, sometimes it works, sometimes doesn't.
Someone have a clue how to uniquely identify a socket when there are several clients?

Send a message from client to server on connection node.js

I want my client-side code to send the server the user's userid when establishing the connection, then i want the server to check the database for new messages for each user that is connecting, and send the user the number of new messages it has when new messages are available.
My client-side code:
var socket = io.connect('http://localhost:8000');
socket.on('connect', function () {
socket.emit('userid', '1');
});
socket.on('new_message', function (data) {
var number_of_messages= "<p>"+data.number+"</p>";
$('#container').html(number_of_messages);
});
My server-side code:
io.sockets.on( 'userid', function (data) {
console.log('userid: '+data);
});
My problem is that the above code is not working: the userid is never received by the serverside and the on('userid') is never called.
My question is how to know which socket sent this user id and how to send to only this specific socket a certain message.
I have solved the problem by saving the clients socket and their id into a global array. this is not a good solution but it works; I know there are rooms and namespaces but I never used it..
socket.io namespaces and rooms
however,
(I used express)
client:
var socket = io.connect('http://localhost:3000',{reconnection:false});
socket.once('connect', function() {
socket.emit('join', '#{id}');
};
server:
var clients = [];
app.io.on('connection', function(socket) {
socket.on('join', function(data) {
clients.push({
ws: socket,
id: data
});
//retrive the messages from db and loop the clients array
//and socket.send(things)
}
}

Resources