Socket.io opening a second connection - node.js

Requirements
I have the following user requirements:
Document A can be edited in real-time by n users
User A can edit the document A and everyone connected to document A should see changes in real time
The same goes for documents B, C, D, ..., n
I'm currently using socket.io in an express server to handle the real-time communication. The logic (reduced to a minimal reproducible example) is as follow:
As soon as user opens document A, he joins a room by emitting an event join:room to the websocket server
websocket server attaches the user to room document A
user A writes something on document A, user B will receive the updates in real-time
user B leaves document A, he emits an event leave:room to websocket server
Minimum reproducible example
const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server, {
cors: {
origin: "*"
}
});
io.on('connection', (socket) => {
socket.on('join:room', (data) => {
console.log(`Joined room: ${data.roomId}`);
socket.join(data.roomId);
});
socket.on('text:insert', (data) => {
console.log(`Text inserted: ${data.roomId}`);
socket.to(data.roomId).emit('text:inserted', data);
});
socket.on('leave:room' , (data) => {
console.log('left room: ' + data.roomId);
socket.leave(data.roomId);
});
})
The code on the client is as follow (reproduced to a minimal reproducible example):
// main.js
import { io } from "socket.io-client";
window.io = io;
console.log('opened connection');
window.monacoSocket = io(process.env.MIX_WEBSOCKET_SERVER_URL);
// custom.js
function onTextInserted({ user, event }) {
console.log('Text have been inserted: ' + event);
}
document.querySelectorAll('.files')
.forEach((item) => {
item.dblclick(() => {
window.monacoSocket.removeAllListeners();
window.monacoSocket.on('text:inserted', ({ event, user }) => onTextInserted({ user, event }));
});
});
This codes works great when we are working in the same document
The problem
The problem starts when I move from document A to document B. Whenever I open my page and visit only document A, I can see that only a single websocket connection was opened and everything works as expected (from both user A and user B perspective):
Now, if I follow the following steps:
as user A I open document A
as user B I open document A
as user A I make a change in document A
as user A I switch to document B
as user A I make changes to document B
as user B I see a new connection opening in my network tab
As soon as this new connection is opened, user A and user B will not be receiving changes anymore
Questions
I have a couple of questions to help me understand what exactly is going on here.
When I add new listeners to a socket.io-client instance, should the socket.io library open a new connection as it is doing right now?
What exactly is causing the new connection to be opened? Is it safe to assume that if no new connection would be open, the code should work as intended - the console.log in onTextInserted function should print?
On my anonymous function on dbclick, should I see remove all listeners?
I highly appreciate anyone taking the time to answer these questions.

Related

Socket.Io not emitting immediately after first emit (order important)

Environment:
Backend
node:latest
socket.io | 4.5.2
Frontend
React Native | 0.70.4
socket.io-client | 4.6.0
both Android and iOS
Here is my NodeJs entry file:
const numCPUs = cpus().length
if (cluster.isPrimary) {
const app = express()
const httpServer = http.createServer(app)
setupMaster(httpServer, { loadBalancingMethod: 'least-connection' })
setupPrimary()
for (let i = 0; i < numCPUs; i++) {
cluster.fork()
}
cluster.on('exit', (worker) => {
cluster.fork()
})
} else {
const app = express()
const httpServer = http.createServer(app)
const io = new Server(httpServer, { maxHttpBufferSize: 1e8 })
io.adapter(createAdapter())
setupWorker(io)
API.Socket.init(io, process.pid)
middlewares.forEach((middleware: any) => app.use(middleware))
routes.forEach((route) => app.use(route.path, route.handler))
httpServer.listen(CONFIG.PORT, () => {})
}
I have a simple chat application.
When user A sends message to user B, new chat message and notification is recorded in database. Now that chat message and notification* should be sent to the B user. There are 2 socket emit-functions for that:
sendNewNotification(
notification: BE.Entities.TNotification,
toUser: string,
) {
this.io
?.to(toUser)
.volatile.emit(ECustomEvents.NewNotification, notification)
}
sendPrivateMessage(
toUser: string | Array<string>,
chatMessage: BE.Entities.TChatMessage,
sourceUser: BE.Entities.TUser,
) {
this.io
?.to(toUser)
.volatile.emit(ECustomEvents.PrivateMessage, chatMessage, sourceUser)
}
If I do it like this, the targetUser is not going to receive the event with the newChatMessage however he will receive the savedNotification
API.Socket.sendPrivateMessage(targetUserId, newChatMessage, userToPass)
API.Socket.sendNewNotification(savedNotification, targetUserId)
Now, if I switch these lines:
API.Socket.sendNewNotification(savedNotification, targetUserId)
API.Socket.sendPrivateMessage(targetUserId, newChatMessage, userToPass)
the behavior would be as expected: the target user B will receive both saved notification and new chat message
How is that possible? What could be wrong?
Thank you mates in advance!
With the current information, I'm not so sure the order matters but perhaps that it's a side-effect / coincidence. Are you checking anywhere to make sure the server-side socket is ready before the client emits?
Consider this super simple WebSocket chat sandbox:
One of the issues I noticed when writing this is when the server WebSocket is not ready, I could not emit from the client to the server. To make sure the server is ready, I sent a ping from the server to the client notifying the client that the server is ready:
wss.on("connection", async function connection(client, request) {
console.log("user connected", Date.now());
client.send(JSON.stringify({ ready: true }));
...
});
I also notice you are usingg the volatile.emit which according to the documentation:
Volatile events
Volatile events are events that will not be sent if the underlying connection is not ready (a bit like UDP, in terms of reliability).
This can be interesting for example if you need to send the position of the characters in an online game (as only the latest values are useful).
socket.volatile.emit("hello", "might or might not be received");
The Socket.IO docs have a similar listener which lets you know when the server is ready.
If you prevent the client from emitting until the server is ready, you can avoid this issue. You also should not need to use the volatile.emit for something that must be delivered.

Websocket + Redis: multiple channels, specific subscriptions/publishing

I'm new to websockets, and am wondering how best to go about this.
My scenario: I have a server that handles different classes of users. For this example, let's say the classes are "mice", "cats", and "dogs"
Each of those classes should have their own channels to listen to for changes e.g. "mice-feed", "cat-feed", and "dog-feed"
My question is: after the server authenticates and determines the class of the current user, what's the best way to have them subscribed to a specific channel, or channel(s), so that when I broadcast messages to said channel(s), I can make sure that only members of particular classes get them (as against everyone currently connected to that server)?
My current code setup looks like this:
var ws = require('ws');
var redis = require('redis');
/* LOCATION 1 */
// prep redis, for websocket channels
var pub = redis.createClient();
var sub = redis.createClient();
// subscribe to our channels
sub.subscribe('mice-feed');
sub.subscribe('cat-feed');
sub.subscribe('dog-feed');
// declare the server
const wsServer = new ws.Server({
noServer: true,
path: "/",
});
/* ... removing some code for brevity... */
wsServer.on("connection", function connection(websocketConnection, connectionRequest) {
/* LOCATION 2 */
});
Do I put the redis declarations in LOCATION 1 (where it currently is), or in LOCATION 2 (when a successful connection is established)? Or neither of the above?
(also: I know it's possible to do this on the websocket end directly i.e. iterate through every client and ws.send if some criterion is matched, but iteration can become costly, and I'm wondering if I can do it on a redis-channel wide operation instead)
If I were building this, my first approach would be this:
// connect to Redis
const client = createClient();
client.on('error', (err) => console.log('Redis Client Error', err));
await client.connect();
// declare the server
const wsServer = new ws.Server(...elided...);
// handle connection
wsServer.on('connection', async (websocketConnection, connectionRequest) => {
const sub = client.duplicate()
// figure out the feed
const feed = 'animal-feed';
await sub.subscribe(feed, message => {
...do stuff...
});
});
It's pretty straightforward but would result in ever user having a dedicated connect to Redis. That may or may not matter depending on how many users you anticipate having.

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

Websockets & NodeJS - Changing Browser Tabs & Sessions

I've started writing a node.js websocket solution using socket.io.
The browsers connects to the node server successfully and I get see the socket.id and all config associated with console.log(socket). I also pass a userid back with the initial connection and can see this on the server side to.
Question: I'm not sure the best way to associate a user with a connection. I can see the socket.id changes every page change and when a tab is opened up. How can I track a user and send 'a message' to all required sockets. (Could be one page or could be 3 tabs etc).
I tried to have a look at 'express-socket.io-session' but I'm unsure how to code for it and this situation.
Question: I have 'io' and 'app' variables below. Is it possible to use the 2 together? app.use(io);
Essentially I want to be able to track users (I guess by session - but unsure of how to handle different socket id's for tabs etc) and know how to reply to user or one or more sockets.
thankyou
The best way to handle the situation is rely on SocketIO's rooms. Name the room after the user's unique ID. This will support multiple connections out of the box. Then, whenever you need to communicate with a particular user, simply call the message function and pass in their id, the event, and any relevant data. You don't need to worry about explicitly leaving a room, SocketIO does that for you whenever their session times out or they close their browser tab. (We do explicitly leave a room whenever they log out though obviously)
On the server:
var express = require('express');
var socketio = require('socket.io');
var app = express();
var server = http.createServer(app);
var io = socketio(server);
io.on('connect', function (socket) {
socket.on('userConnected', socket.join); // Client sends userId
socket.on('userDisconnected', socket.leave); // Cliend sends userId
});
// Export this function to be used throughout the server
function message (userId, event, data) {
io.sockets.to(userId).emit(event, data);
}
On the client:
var socket = io('http://localhost:9000'); // Server endpoint
socket.on('connect', connectUser);
socket.on('message', function (data) {
console.log(data);
});
// Call whenever a user logs in or is already authenticated
function connectUser () {
var userId = ... // Retrieve userId somehow
if (!userId) return;
socket.emit('userConnected', userId);
}
// Call whenever a user disconnects
function disconnectUser () {
var userId = ... // Retrieve userId somehow
if (!userId) return;
socket.emit('userDisconnected', userId);
}

How do I determine when a browser completely disconnects from my site using socket.io?

Because people can open many tabs and use many browsers, I have some troubles of determining when user close all tabs of each browser.
If all tabs are closed, the user is no longer connected so I assume you want to know on the server if he is completely disconnected?
You should hold a list of sockets against a user identifier (login name or similar) on the server, when a new tab is opened it will have a new socket connection so add it to the list.
When a socket connection is closed, remove it from the socket collection for that user.
When the user's last socket connection is closed, you know that the user has completely disconnected.
EDIT with example
something like this (untested and hastily written!)
'use strict';
var userConnections = [];
io.on('connection', function (socket) {
var username = socket.request.user.username;
var existingUser = userConnections.find(function(userConnection){
return userConnection.username === username;
})
if (!existingUser){
existingUser = {
username: username,
sockets: []
}
}
existingUser.sockets.push(socket);
socket.on('disconnect', function () {
var socketIndex = existingUser.indexOf(socket);
existingUser.sockets.splice(socketIndex, 1);
if (existingUser.sockets.length === 0){
//user has completely disconnected
}
});
});
EDIT - after clarification (see comments)
OP has indicated he wishes to know when all connections for a particular browser instance have disconnected.
Since you cannot access any system information about the browser process from javascript I don't see any way of achieving this.
It is possible to detect the browser type (Chrome/IE/Edge etc) on the client and send this information on socket connection. You could then store your socket information referencing this information. However I don't think this is what the OP wants.
Here is my solution, it depends on #Banners's one.
"socket.cookies" stores the browser's cookies
Please let me now if I was missing something.
'use strict';
var userConnections = {};
io.on('connection', function (socket) {
var username = socket.request.user.username;
var visit_id = (socket.cookies.vid) ? socket.cookies.vid : random_unique_id();
//set cookie here
setCookie('vid', visit_id, expire);
if (!userConnections[visit_id])
userConnections[visit_id] = [];
userConnections[visit_id].push(socket.id);
socket.on('disconnect', function () {
var vid = socket.cookies.vid;
if (userConnections[vid]) {
var index = userConnections[vid].indexOf(socket.id);
if (index != -1)
userConnections[vid].splice(index, 1);
if (userConnections[vid].length === 0) {
delete userConnections[vid];
//All tabs have been closed
}
}
});
});

Resources