Is there a way to determine a user from a socket.io connection event?
The following code is used to get a user object from the front end currently:
io.on("connection", async (socket: Socket) => {
socket.on("user-joined", (user) => {
console.log("front end says hi", user);
connectedUsers.push(user);
});
});
What I would like to know is if there's a way to pass the user right away in the connection event, so I don't have to listen for a follow up 'user-joined' message?
On the client side it seems we can pass auth along with the connection.
From https://socket.io/docs/v4/client-initialization/#auth
import { io } from "socket.io-client";
const socket = io({
auth: {
user: "abcd"
}
});
The solution would look something like:
io.on("connection", async (socket: Socket) => {
console.log("front end says hi", socket.handshake.auth); // prints { user: "abcd" }
});
Can I pass data with along with the "connection" message to identify the user that is connecting?
Thanks
Yes, it is possible to identify the user in the initial connection event.
To start: the client will have to initialize a socket connection with an options param. The options should include an auth property that specifies the user.
For example on the client side:
import { io } from "socket.io-client";
const socket = io({
auth: {
user: "abcd"
}
});
On the server side we can read the auth data in the the connection event handler:
io.on("connection", async (socket: Socket) => {
console.log("front end says hi", socket.handshake.auth);
// prints "front end says hi", { user: "abcd" }
});
You may also find this link to the official docs helpful: https://socket.io/docs/v4/client-initialization/#auth
Related
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!
I am currently trying to emit an event across namespaces, yet I can't seem to do that. If I set up a listener outside on io.on('connection', async function (socket) {}, it does not work. Here is my code:
[Server]
const io = socket(server); // binds http server
// accounts route to handle CRUD operations for accounts
const accountsHandler = io
.of('/api/accounts')
.on('connection', async function (socket) {
socket.emit('received accounts', { accounts: await accountsAPI.getAccounts() });
socket.on('add account', async function({ username, password }) {
let accounts = await accountsAPI.addAccount({ username, password });
console.log("Adding account...");
console.log("Username: " + username);
gramHandler.emit('new account', accounts[accounts.length-1]);
socket.emit('received accounts', { accounts });
});
socket.on('remove account', async function({ key }) {
let accounts = await accountsAPI.removeAccount(key);
socket.emit('received accounts', { accounts });
});
});
If imove the event add account outside to
accountsHandler.on('add account', async function({ username, password }) {
let accounts = await accountsAPI.addAccount({ username, password });
console.log("Adding account...");
console.log("Username: " + username);
gramHandler.emit('new account', accounts[accounts.length-1]);
socket.emit('received accounts', { accounts });
});
It doesn't work. I do not think I have to do it this way. The current goal is to emit an even in gramHandler when accountHandler receives the add account event.
const gramHandler = io
.of('/api/gram')
.on('connection', async function (socket) {
socket.on('new account', async function({ username, password, key }) {
console.log("Adding new account " + username);
let proxy = await get_proxy(proxies);
let instance = new api(username, password, proxy['http']);
instance.login();
instances[key] = instance;
});
}
How can I fix this?
Think about it this way, whenever there is a new connection, socketio creates a socket object for that connection, and it's provided in a callback hence
io.on('connection', async functoin(socket) {
})
AND IT IS VERY IMPORTANT TO PAY ATTENTION THAT HERE WE ARE USING THE OBJECt io AND NOT SOCKET
However, whenever you want to capture an even that's coming from a socket then you do socket.on() and socket only exists in the scope under io.on hence extracting the function wont work.
Moreover, in your add account function you are using socket.emit to respond, but socket doesn't even exist. If you want to modularize, you can use something like redis and cache all connected sockets and then you loop through them and use each socket.on, but I don't recommend this method because it can be very resources intensive.
Your next bet is to separate the add account function and whenever a socket has an event just call the function and pass the socket as a parameter
For example:
io.on('connection', async function(socket) {
socket.on('add account', async function({ username, password }) {
addAccount(username, password, socket)
})
})
function addAccount(username, password, socket) {
// Your addAccount logic goes here
}
You can modularize this as you wish and separate functions into different files.
Goodluck
I am using mqttjs and socketio on my nodejs backend.
I am using angular as my frontend framework.
On my frontend there are 3 routes.
All requires socket connection for real time data.
So on ngOnInit i run client side socket io connection code and on ngOnDestroy I will run socket disconnect as well.
And in my server side code (index.js) there are mainly 3 actions that is happening.
const io = require('socket.io')(server)
mqtt.createConnection();
mqtt.mqttSubscriptions(io);
mqtt.mqttMessages(io);
These are the mqtt methods:
const createConnection = () => {
let options = {
protocol: 'mqtt',
clientId: process.env.MQTT_CLIENT_ID,
username: process.env.MQTT_USERNAME,
password: process.env.MQTT_PASSWORD,
};
client = mqtt.connect(process.env.MQTT_HOST, options);
client.on('connect', function() {
winston.info('MQTT connected');
});
client.on('error', function(err) {
winston.error(err);
});
};
const mqttSubscriptions = io => {
winston.info(`Socket connected.`);
client.subscribe([TOPICS.DATA], function(error, granted) {
if (error) {
winston.error(error);
}
winston.info('Topics: ', granted);
});
};
const mqttMessages = io => {
io.sockets.on('connection', socket => {
winston.info(`Socket connected.`);
client.on('message', function(topic, message) {
let payload = JSON.parse(message.toString());
winston.info(topic);
winston.info(payload.id);
switch (topic) {
case TOPICS.DATA:
dataController.storeData(payload, io);
break;
default:
winston.error('Wrong topic');
break;
}
});
});
};
And on the datacontroller I am running
socket.emit()
My problem is everytime I navigate to a route and come back the dataController.storeData is called multiple times.
That is when I am at route A, and then navigate to route B and then back to A and then to C, the data is multiplied that many times of my route navigation. (In this case 4 times.)
I found that it is socket io and mqtt connection problem, but I don't know how to solve, since I am new to both of these.
Any help?
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
I work with a setup created by create-react-app and use flux for data management and the application needs to implement socket on the client side (I use socket.io for this purpose).
Currently the socket is initialised in a Socket.js file the following way:
import io from 'socket.io-client';
import { remoteUrl } from './constants/RemoteUrl';
import SocketWorker from './utilities/SocketWorker';
let socket = io.connect(remoteUrl + '?role=user');
socket.on('statusChange', (data) => {
return SocketWorker.receiveOrderStatusChange(data);
})
export { socket };
It does work, however the problem is that it only tries to connect to the server once, when the site is loaded. When the user opens the site unauthenticated it does not connect and misses to reconnect, thus the connection is not established and socket events are not received
I have tried to create a class instead and react an API for reconnect on the object, like:
import io from 'socket.io-client';
import { remoteUrl } from './constants/RemoteUrl';
import SocketWorker from './utilities/SocketWorker';
function Socket() {
this.socket = io.connect(remoteUrl + '?role=user');
this.reconnect = () => {
this.socket = io.connect(remoteUrl + '?role=user');
}
}
let socket = new Socket();
socket.socket.on('statusChange', (data) => {
return SocketWorker.receiveOrderStatusChange(data);
})
export { socket };
I tried to call the Socket.reconnect() method, although it did not work and connection was not established either. Any idea or alternative solution?
The way I managed to solve this if anyone face the same problem with the Socket.io API:
First, you should encapsulate your Socket into an object created by the constructor, but there is no need to create a reconnect method as the connection is present already (and the auth can be handled through emitted events I will describe below) :
import io from 'socket.io-client';
import { remoteUrl } from './constants/RemoteUrl';
import SocketWorker from './utilities/SocketWorker';
function Socket() {
this.socket = io.connect(remoteUrl + '?role=user');
this.socket.on('statusChange', (data) => {
return SocketWorker.receiveOrderStatusChange(data);
})
};
const socket = new Socket();
export { socket };
You can import the socket anywhere within your project:
import {socket} from './Socket';
And you can call:
socket.socket.emit('joinRoleRoom','user');
socket.socket.emit('joinIdRoom', _user._id);
On the server side, you just need to handled these events as follow:
socket.on('joinRoleRoom', (role) => {
socket.join(role)
console.log('Client joined to: ' + role);
});
socket.on('joinIdRoom', (id) => {
console.log('Client joined to: ' + id);
socket.join(id)
});
The socket will join the necessary rooms based on their auth info obtained during the auth process.
The original accepted answer from sznrbrt would work, but beware it has a serious security flaw.
If you do the following an attacker could join a room by just passing the desired user_id and start to receive sensitive user information. It could be private messages between two individual.
socket.socket.emit('joinRoleRoom','user');
socket.socket.emit('joinIdRoom', user_id);
Socket.io has an option to pass extraHeaders. One can use that to pass a token from the client. The server would use the desired authentication algorithm to decrypt the token and get the user_id.
Example:
socket.js
import io from 'socket.io-client';
import { remoteUrl } from './constants/RemoteUrl';
import SocketWorker from './utilities/SocketWorker';
const socket = io.connect(remoteUrl + '?role=user');
const socketAuth = () => {
socket.io.opts.extraHeaders = {
'x-auth-token': 'SET_TOKEN',
};
socket.io.opts.transportOptions = {
polling: {
extraHeaders: {
'x-auth-token': 'SET_TOKEN',
},
},
};
socket.io.disconnect();
socket.io.open();
};
export { socket, socketAuth };
client.js
import { socket, socketAuth } from './socket';
//After user logs in
socketAuth();
server.js, using a package socketio-jwt-auth
io.use(jwtAuth.authenticate({
secret: 'SECRET',
succeedWithoutToken: true
}, (payload, done) => {
if (payload && payload.id) {
return done(null, payload.id);
}
return done();
}));