reuse socket id on reconnect, socket.io, node.js - node.js

is it possible to reuse a socket.id or use it multiple times?
Let's assume a user views multiple pages of the same site in different browser tabs. I want to use a single socket.id, socket to handle them all.
If a user receives a notification it should popup on all tabs with a single socket.emit.

It is possible
From the dates of previous responses I assume it might not have been possible in previous versions of socket.io, but I can confirm that I'm successfully reusing socket ids on reconnections with socket.io 2.3.0.
You just need to override io.engine.generateId. Whatever that method returns will be the id assigned to the socket. Here are the docs about generateId.
As far as I've experimented myself, there are two situations when that method is called. During connections and reconnections.
The method io.engine.generateId receives the originating request object as the argument, so we can use it to figure out if we want to reuse the id or get a fresh new one.
Example
As an example, I'll show how you would reuse an id sent from the client, or create a new one when the client doesn't send it. The id will be sent on the handshake request as the query param socketId.
1. Override io.engine.generateId
First you need to override io.engine.generateId, which is the method that assigns IDs. On the server you need to do something like this.
const url = require('url')
const base64id = require('base64id')
io.engine.generateId = req => {
const parsedUrl = new url.parse(req.url)
const prevId = parsedUrl.searchParams.get('socketId')
// prevId is either a valid id or an empty string
if (prevId) {
return prevId
}
return base64id.generateId()
}
That way, whenever you send the query param socketId in the handshake request, it will be set as the socket id. If you don't send it you'll generate a new one using base64id. The reason to use that library in particular is because that's what the original method does. Here you can find the source code.
2. Send the information on the connection request
Once you have that, you need to send the socketId param from the client. This is described in the docs.
const socket = io.connect(process.env.WEBSOCKET_URL, {
query: {
socketId: existingSocketId || ''
}
})
process.env.WEBSOCKET_URL would be the URL where your web socket is listening.
Note that this will work when connecting, but you might want to update the query on reconnection.
3. Send the information on the reconnection request
On the same section of the docs it explains how to update the query params before reconnection. You just need to do something like this.
socket.on('reconnect_attempt', () => {
socket.io.opts.query = {
socketId: existingSocketId || ''
}
});
Just like that you'll be reusing the same socket id as long as it is sent from the client.
Security concerns
It's probably a bad idea to trust information sent from the client to assign the socket id. I'd recommend sending a cryptographically signed payload, storing that payload in the client, and send it back to the server when connecting and reconnecting. That way the server can check that the payload can be trusted by verifying the signature.
Using the same example above, we would send something like this to the client, maybe .on('connect'):
{
socketId: 'foo',
signature: SHA_256('foo' + VERY_SECRET_PASSWORD)
}
The client would store that payload and send it back on connecting or reconnecting, in the same way we were sending socketId before.
Once the server receives the signed payload, inside io.engine.generateId we could check that the signature in the payload matches the hash we produce using the ID and the VERY_SECRET_PASSWORD.

You can't reuse Socket.IO connection IDs since they are created during the client-server handshake, but there are alternative methods. I don't have any examples, but you can modify the Socket.IO client to pass along a query string when the handshake is being performed. Then you can tell the server to handle the client based on the query string, and later fetch all client IDs with a certain query string.
Another method you could use would be to use namespaces. Assuming you have some type of session system, you could create a session-specific namespace, and connect clients with that session ID straight to that namespace.

Multiple sites?
No, that's not possible. It would be possible if you open those sites into iframes in your webapp, I guess.
Another option would be to build a browser plugin that opens a socket connection.

Related

socket.io broadcast on api call to everyone except "sender"

I'm making an API call from a client to the server, and I don't want to send the message in the websocket as well.
What's the best way for the server to broadcast to all other clients when this happens?
I was thinking maybe to attach the client socketId in the headers, and reading that on the server, and looping over all the connected clients and send them, something like:
Object.keys(io.sockets.sockets).forEach((socketId) => {
if (socketId === socketIdFromHeader) return
socket.broadcast.to(socketId).emit('message', 'my message');
});
I see a couple of problems here:
it feels hacky
If I have a large scale of connected clients, running a separate function for each of them will be expensive.
Any ideas how'd you go about it?
Thanks!
PS. I'm new to Socket.io and websockets
You need to know that your API connection is completely different from your web socket connection. So if you want to send data to all users but the sender, you should store a map from usersId (any identification you have of users which you have access in both socket.io connection and your API) to users socket id.
for example store the data in redis. to get user in io connection your client can send a token in query string, and then you find the user in your database using this token or decode the user If you are using JWT.
io.on('connection', socket => {
let user = await getUserUsingToken(socket.handshake.query.token);
await redis.set(user._id, socket.id);
}
and in your api get socket id from Redis
let sid = await redis.get(user._id);
Object.keys(io.sockets.sockets).forEach((socketId) => {
if (socketId === sid) return
socket.broadcast.to(socketId).emit('message', 'my message');
});
You can store this mapping anywhere you like. I just showed you Redis as an example.

What is the proper way to emit an event with socket.io?

I want to emit an event to the client when a long fucntion comes to an end.
This will show a hidden div with a link - on the client side.
This is the approach i tested:
//server.js
var express = require('express');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);
require('./app/routes.js')(app, io);
//routes.js
app.post('/pst', function(req, res) {
var url = req.body.convo;
res.render('processing.ejs');
myAsyncFunction(url).then(result => {
console.log('Async Function completed');
socket.emit('dlReady', { description: 'Your file is ready!'});
//do some other stuff here
}).catch(err => {
console.log(err);
res.render('error.ejs');
})
});
I get this
ERROR: ReferenceError: socket is not defined
If i change the socket.emit() line to this:
io.on('connection', function(socket) {
socket.emit('dlReady', { description: 'Your file is ready!'});
});
Then i don't receive an error, but nothing happens at the client.
This is the client code:
<script>
document.querySelector('.container2').style.display = "none";
var socket = io();
socket.on('dlReady', function(data) { //When you receive dlReady event from socket.io, show the link part
document.querySelector('.container1').style.display = "none";
document.querySelector('.container2').style.display = "block";
});
</script>
This whole concept is likely a bit flawed. Let me state some facts about this environment that you must fully understand before you can follow what needs to happen:
When the browser does a POST, there's an existing page in the browser that issues the post.
If that POST is issued from a form post (not a post from Javascript in the page), then when you send back the response with res.render(), the browser will close down the previous page and render the new page.
Any socket.io connection from the previous page will be closed. If the new page from the res.render() has Javascript in it, when that Javascript runs, it may or may not create a new socket.io connection to your server. In any case, that won't happen until some time AFTER the res.render() is called as the browser has to receive the new page, parse it, then run the Javascript in it which has to then connect socket.io to your server again.
Remember that servers handle lots of clients. They are a one-to-many environment. So, you could easily have hundreds or thousands of clients that all have a socket.io connection to your server. So, your server can never assume there is ONE socket.io connection and sending to that one connection will go to a particular page. The server must keep track of N socket.io connections.
If the server ever wants to emit to a particular page, it has to create a means of figuring out which exact socket.io connect belongs to the page that it is trying to emit to, get that particular socket and call socket.emit() only on that particular socket. The server can never do this by creating some server-wide variable named socket and using that. A multi-user server can never do that.
The usual way to "track" a given client as it returns time after time to a server is by setting a unique cookie when the client first connects to your server. From then on, every connection from that client to your server (until the cookie expires or is somehow deleted by the browser) whether the client is connection for an http request or is making a socket.io connection (which also starts with an http request) will present the cookie and you can then tell which client it is from that cookie.
So, my understanding of your problem is that you'd like to get a form POST from the client, return back to the client a rendered processing.ejs and then sometime later, you'd like to communicate with that rendered page in the client via socket.io. To do that, the following steps must occur.
Whenever the client makes the POST to your server, you must make sure there is a unique cookie sent back to that client. If the cookie already exists, you can leave it. If it does not exist, you must create a new one. This can be done manually, or you can use express-session to do it for you. I'd suggest using express-session because it will make the following steps easier and I will outline steps assuming you are using express-session.
Your processing.ejs page must have Javascript in it that makes a socket.io connection to your server and registers a message listener for your "dlready" message that your server will emit.
You will need a top-level io.on('connection', ...) on your server that puts the socket into the session object. Because the client can connect from multiple tabs, if you don't want that to cause trouble, you probably have to maintain an array of sockets in the session object.
You will need a socket.on('disconnect', ...) handler on your server that can remove a socket from the session object it's been stored in when it disconnects.
In your app.post() handler, when you are ready to send the dlready message, you will have to find the appropriate socket for that browser in the session object for that page and emit to that socket(s). If there are none because the page you rendered has not yet connected, you will have to wait for it to connect (this is tricky to do efficiently).
If the POST request comes in from Javascript in the page rather than from a form post, then things are slightly simpler because the browser won't close the current page and start a new page and thus the current socket.io connection will stay connected. You could still completely change the page visuals using client-side Javascript if you wanted. I would recommend this option.

Is there an alternate way of sending a private message with Socket.io (1.0+)?

Im working on a simple session based app shared by a session code in the URL. I decided to generate and assign a shorter user friendly unique ID for each client who connects to a socket, and the client who creates a session causes a socket.io room to be created with his ID.
I didnt realize until later that the private messaging mechanism in socket.io relied on each client being assigned to a room named by their ID. This means that because my room for a session is named after the creator's socket ID, using .to() will not message that client, but rather all of the clients now assigned to that room.
I could remedy this in ways that would require some re-design, but first I wanted to ask if there is an alternate way of sending a message to a specific client via his/her ID.
/*create an array of clients, where key is the name of user and value is its unique socket id(generated by socket only, you do not have to generate it) during connection.*/
var clients = {};
clients[data.username] = {
"socket": socket.id
};
//on server side
socket.on('private-message', function(data){
io.sockets.connected[clients[data.username].socket].emit("add- message", data);
});
//on client side
socket.emit("private-message", {
"username": userName,
"content": $(this).find("textarea").val()
});
socket.on("add-message", function(data){
notifyMe(data.content,data.username);
});

ss.api.publish.user('someUser Id', content) is not working in socket stream

I'm using socket stream to send the data to the logged in user by using the following code
var ss = require('socketstream');
....
....
ss.api.publish.user('userId', content);
but the ss.api.publish is undefined is what the error i'm receiving.
Where am i going wrong.
Please advice.
The API for a publish to a user is:
Sending to Users
Once a user has been authenticated (which basically means their session now includes a value for req.session.userId), you can message the user directly by passing the userId (or an array of IDs) to the first argument of ss.publish.user as so:
// in a /server/rpc file
ss.publish.user('fred', 'specialOffer', 'Here is a special offer just for you!');
Important: When a user signs out of your app, you should call req.session.setUserId(null, cb) to prevent the browser from receiving future events addressed to that userId. Note: This command only affects the current session. If the user is logged in via other devices/sessions these will be unaffected.
The above is taken from the original document describing the socketstream pub/sub api.
as you can see, you need to supply one more argument than you thought. That is, becasue on the client side you need to subscribe to a message channel in order to get the message. In the above example, you need to do this in your client side code:
ss.event.on('specialOffer', function(message){
alert(message);
});

Save socket.io ID for all open tabs in browser

Can you please tell how to solve the problem of preserving the identity socket.io, for all open tabs in the browser.
Example:
I have a page open (/index, for example), I set it to connect to socket.io and get a unique session ID. But when I open the same page on another tab - ID creates another, that is, there are already two compounds with two different ID. That is right, it is logical. But, I need to send messages to these ID, simultaneously in all the tabs (including binding to user_id), and because the session ID are different - it is impossible.
If you don't want to (or can't) use some kind of session store, and want to send messages to multiple sockets with the same user_id, you can store a map with user_id as key and an array of socket as value
var socketMap = {};
And store a reference to the socket with your handshake data
io.sockets.on('connection', function (socket) {
var userId = socket.handshake.userId;
if(!socketMap[userId]) socketMap[userId] = [];
socketMap[userId].push(socket);
});
Or with some normal event data
socket.on('auth', function(data) {
var userId = data.userId;
var authToken = data.authToken;
... // verify the data...
if(!socketMap[userId]) socketMap[userId] = [];
socketMap[userId].push(socket);
});
When you want a list of socket to send message to
var sockets = socketMap['some_user_id'];
ID of the socket will be different in any cases, and should be not used to identify user in any case. It is only used to identify Socket it self.
What you are looking for is session. Ability to relate socket connection to session - is what you are looking for.
In order to do so, you need to have sessions created for new http connections as well as restore this session during handshake process of socket.io.
That way you will be able to identify that specific socket belongs to specific session.
And then user data and other stuff you keep to session. This helps in many cases - restore session on page refresh or have single session in multiple tabs as you need it.
Read this answer of how to restore socket.io session id from handshake process: http://www.danielbaulig.de/socket-ioexpress/

Resources