NodeJS - Response stream - node.js

I built a simple API endpoint with NodeJS using Sails.js.
When someone access my API endpoint, the server starts to wait for data and whenever a new data appears, he broadcasts it using sockets. Each client should receive his own stream of data based on his user input.
var Cap = require('cap').Cap;
collect: function (req, res) {
var iface = req.param("ip");
var c = new Cap(),
device = Cap.findDevice(ip);
c.on('data', function(myData) {
sails.sockets.blast('message', {"host": myData});
});
});
The response do not complete (I never send a res.json() - what actually happens is that the browser keep loading - but the above functionality works).
2 Problems:
I'm trying to subscribe and unsubscribe to to this API endpoint from my client (using RxJS). When I subscribe, I start to receive data via sockets - but I can't unsubscribe to the API endpoint (the browser expect the request to be completed).
Each client should subscribe to his own socket room based on the request IP parameter ( see updated code ). Currently it blasts the message to everyone.
How I can create a stream/service-like API endpoint with Sails.js that will emit new data to each user based on his input?
My goal is to be able to subscribe / unsubscribe to this API endpoint from each client.

Revised Answer
Let's assume your API endpoint is defined in config/routes.js like this:
...
'get /collect': 'SomeController.collectSubscribe',
'delete /collect': 'SomeController.collectUnsubscribe',
Since each Cap instance is tied to one device, we need one instance for each subscription. Instead of using the sails join/leave methods, we keep track of Cap instances in memory and just broadcast to the request socket's id. This works because Sails sockets are subscribed to their own ids by default.
In api/controllers/SomeController.js:
// In order for the `Cap` instances to persist after `collectSubscribe` finishes, we store them all in an Object, associated with which socket the were created for.
var caps = {/* req.socket.id: <instance of Cap>, */};
module.exports = {
...
collectSubscribe: function(req, res) {
if (!res.isSocket) return res.badRequest("I need a websocket! Help!");
if (!!caps[req.socket.id]) return res.badRequest("Dude, you are already subscribed.");
caps[req.socket.id] = new Cap();
var c = caps[req.socket.id]; // remember that `c` is a reference to our new `Cap`, not a copy.
var device = c.findDevice(req.param('ip'));
c.open(device, ...);
c.on('data', function(myData) {
sails.sockets.broadcast(req.socket.id, 'message', {host: myData});
});
return res.ok();
},
collectUnsubscribe: function(req, res) {
if (!res.isSocket) return res.badRequest("I need a websocket! Help!");
if (!caps[req.socket.id]) return res.badRequest("I can't unsubscribe you unless you actually subscribe first.");
caps[req.socket.id].removeAllListeners('data');
delete caps[req.socket.id];
return res.ok();
}
}
Basically, it goes like this: when a browser request triggers collectSubscribe, a new Cap instance listens to the provided IP. When the browser triggers collectUnsubscribe, the server retreives that Cap instance, tells it to stop listening, and then deletes it.
Production Considerations: please be aware that the list of Caps is NOT PERSISTENT (since it is stored in memory and not a DB)! So if your server is turned off and rebooted (due to lightning storm, etc), the list will be cleared, but considering that all websocket connections will be dropped anyway, I don't see any need to worry about this.
Old Answer, Kept for Reference
You can use sails.sockets.join(req, room) and sails.sockets.leave(req, room) to manage socket rooms. Essentially you have a room called "collect", and only sockets joined in that room will receive a sails.sockets.broadcast(room, eventName, data).
More info on how to user sails.sockets here.
In api/controllers/SomeController.js:
collectSubscribe: function(req, res) {
if (!res.isSocket) return res.badRequest();
sails.sockets.join(req, 'collect');
return res.ok();
},
collectUnsubscribe: function(req, res) {
if (!res.isSocket) return res.badRequest();
sails.sockets.leave(req, 'collect');
return res.ok();
}
Finally, we need to tell the server to broadcast messages to our 'collect' room.
Note that this only need to happen once, so you can do this in a file under the config/ directory.
For this example, I'll put this in config/sockets.js
module.exports = {
// ...
};
c.on('data', function(myData) {
var eventName = 'message';
var data = {host: myData};
sails.sockets.broadcast('collect', eventName, data);
});
I am assuming that c is accessible here; If not, you could define it as sails.c = ... to make it globally accessible.

Related

How to safely get the current user id in socket.io/Node.JS?

I am developing a simple API for a chat application on Node.js Express, and by assignment, it is required to make it possible to communicate between two users using socket.іо. I am faced with the problem that I cannot "safely" transfer information about the current user to the socket in any way. Information about the user with whom the correspondence is conducted can be specified in the socket parameters when connecting, which I do, but what about the current user (me)?
For example, I can do this:
const {receiverId, myId} = socket.handshake.query;
That is, specify both ids when connecting. But this is very unsafe because anyone who will join the socket can specify any id and write anything on behalf of someone else (for example, through Postman WebSockets).
Another option I was considering is making a post request in which a connection to the socket will be created using request.user.id and the request parameter. Then the post request will look like this:
router.post('/chat/:receiver', function (req,res){
const {receiver} = req.params
const socket = io.connect('/')
socket.emit('initMyUserId', {
myId: req.user,
});
})
But this option also did not work, because the file where this function is located and the file where the io variable is initialized are different, and I am not sure that it is generally advisable to transfer and use it in this way. Moreover, this approach will not allow me to check the operation of sockets via Postman, because the connection will occur in a post request, and not manually.
Are there working options to safely transfer the current user id with the ability to test it normally in postman? Or at least just safely pass the current user id if it doesn't work with Postman.
Here is the full code snippet for the socket events handlers:
module.exports = function(io) {
io.on('connection', (socket)=>{
const {id} = socket;
console.log(Socket connected: ${id});
const {idUser} = socket.handshake.query;
console.log(Socket idUser: ${idUser});
socket.on('message-to-user', (msg) => {
msg.type = user: ${idUser};
socket.to(idUser).emit('message-to-user', msg);
socket.emit('message-to-user', msg);
});
socket.on('disconnect', () => {
console.log(Socket disconnected: ${id});
});
});
}

websocket send to specific user nodejs

I am currently creating a websocket server for a mobile front. and in some cases I need to send a json only to a specific user and after several attempts I still haven't managed to get Websocket to work so I can send my json to one client at a time.
i'm using this library : github.com/websockets/ws
To explain my problem i have several products that contain several variables that need to be refreshed in real time. when a user connects to a product he will receive only the json of that product and the other users will receive the json of the other products they are currently on. that's why i want to send a specific json to a user to enable this
I would like to know if any of you know how to fix the problem as I'm starting to block on it.
Thank you very much.
const opp = new WebSocket.Server({port: 10001});
let user = 0;
let lookup = [];
opp.on('connection', function connection(op) {
lookup.push(op.id);
let id = "";
op.on('message', function incoming(message) {
console.log('received: %s', message);
id = message;
query = {
text: "SELECT id,state,users_list_name,user_win,timer_stamp FROM products WHERE id = " + parseInt(id) + " AND circle = 1 ORDER BY CASE WHEN state = \'available\' THEN \'1\' WHEN state = \'soon\' THEN \'2\' WHEN state = \'expired\' THEN \'3\' END",
};
});
client.connect();
const interval = setInterval(function ping() {
client.query(query, (err, res) => {
if (err) {
console.log(err.toString());
console.log(query);
} else {
console.log(lookup);
for (let i = 0; i < lookup.length; i++){
console.log("########################");
lookup[i].send(JSON.stringify(res.rows));
}
}
});
}, 300);
});```
OK. Still trying to understand the actual spec you're shooting for. But, assuming the following (based on your answers to my prior questions):
A client connects using a webSocket.
When they send a message over that webSocket, that message is an id of something that can be looked up in your database and that they want regular updates for.
Those updates for that particular id should be sent only to that specific client that requested it.
If a different client connects and specifies some id, they should get updates for that id only.
When a client sends a new message that specifies a different id, their updates should now be only for that new id.
Updates for the id that one client requested are sent only to that one client (not to the other clients).
If that's what you really want, here's a way to structure that.
const wss = new WebSocket.Server({port: 10001});
// make database connection that all users share
client.connect();
wss.on('connection', function connection(ws) {
// these variables are unique to each ws connection
let interval, query;
// when webSocket closes, stop any current interval timer associated with this webSocket
ws.on('close', function() {
if (interval) {
clearInterval(interval);
}
});
// when we get an id, start querying for updates on that id
ws.on('message', function incoming(id) {
console.log(`received: ${id}`);
query = {
text: "SELECT id,state,users_list_name,user_win,timer_stamp FROM products WHERE id = " + parseInt(id) + " AND circle = 1 ORDER BY CASE WHEN state = \'available\' THEN \'1\' WHEN state = \'soon\' THEN \'2\' WHEN state = \'expired\' THEN \'3\' END",
};
// if interval is not already going, start it
if (!interval) {
interval = setInterval(function() {
client.query(query, (err, res) => {
if (err) {
console.log(err);
console.log(query);
} else {
// send data to just the one client that this timer is for
ws.send(JSON.stringify(res.rows));
}
});
}, 300);
}
});
});
Now, some comments:
Polling the database on a short time interval with a separate polling loop for every single client simply will not scale at all. You will have serious database scale issues. You really need a better design here, but since we don't know the overall requirements and architecture of your application, we don't have enough info to know what to suggest. Probably you want to leverage notifications in a database that tell you when data has changed rather than you polling it on a short interval on behalf of every single client.
I could find no reason for the lookup data structure. Your comments say that you want to send updates to ONE specific client, the one that requested that id. That can be done with ws.send().
This code assumes that the client variable represents a connection to your database that each of the setIntervals for each connected client can all share. That's why that code was moved out of the wss.on('connection', ...) event handler.
I switched to the more common terminology of wss to refer to the server instance and ws to refer to the webSocket for a particular connected client.
ws.send() is how you send to a connected client. I still don't know what you were doing with op.id. Looking at the doc for the ws library, that doesn't appear to be something you can use to send to.
Your code (and this code) creates a separate setInterval() timer for every webSocket client that connects and it uses a very short interval time. This will not scale. At worst, the interval time needs to be lengthened into multiple seconds (depending upon desired target scale). At best, you need to stop polling the database entirely and use some other mechanism in the database for getting notifications when data has been changed.

How to send new user the old sent messages with socket.io

I am using this tutorial. I need to send old messages to the new connected user. How can I do that? I mean the new users should be able to see the old messages.
for my application, i don't need to store the data in a database. Once the server is turned off then the data should be removed. but how long the server runs on each session the clients should get previous message
I have tried
const history = []
//Telling Express+Socket.io App To Listen To Port
io.sockets.on("connection",function(socket){
socket.emit("Start_Chat");
//On Event Registar_Name
socket.on("Register_Name",function(data){
console.log(data)
io.sockets.emit("r_name","<strong>"+data+"</strong> Has Joined The Chat");
//Now Listening To A Chat Message
socket.on("Send_msg",function(data){
history.push(data)
console.log(history)
io.sockets.emit("msg",data);
//Now Listening To A Chat Message
})
})
})
but how can I say when a new client enters send history data?
UPDATE: I have tried the following but it is always sending the history
const history = []
const client = []
//Telling Express+Socket.io App To Listen To Port
io.sockets.on("connection",function(socket){
client.push({id : socket.client.id})
console.log(client)
socket.emit("Start_Chat");
//On Event Registar_Name
socket.on("Register_Name",function(data){
console.log(data)
io.sockets.emit("r_name","<strong>"+data+"</strong> Has Joined The Chat");
//Now Listening To A Chat Message
socket.on("Send_msg",function(data){
history.push(data)
console.log(history)
if(client.find(e => e.id !== socket.client.id)) {
io.sockets.emit("msg",history);
} else {
io.sockets.emit("msg",data);
}
//Now Listening To A Chat Message
})
})
There are several ways depending on how complex you want to make your chat application.
Simple solution: You create some type of data structure (possibly an array) which holds the last n messages. Whenever a new message is added, you remove the first one that was added. When the onConnect event is triggered, you send this entire data structure to the client. This is more for a proof of concept solution which you can later change.
Pros: Easy to implement
Cons: All the data on the server is essentially volatile, if you were to restart the application, all data would be lost. Additionally it would not scale across multiple server instances.
A little more complex solution: Store all messages in a database and query the database to retrieve the last n messages when a user connects.
Pros: All data is persistent and if the server were to crash/be stopped, you still would be able to retrieve the data.
Cons: You need to maintain the database as messages will take up more space over time. To remedy this you can always delete all messages older than x days automatically.
Finally, Figured it out
const history = []
const client = []
//Telling Express+Socket.io App To Listen To Port
io.sockets.on("connection",function(socket){
client.push({id : socket.client.id})
console.log(client)
var getClientID = client.find(e => (e.id === socket.client.id))
console.log("the Client", getClientID)
if(getClientID){
//io.sockets.emit("msg",history);
socket.emit("msg",history);
}
socket.emit("Start_Chat");
//On Event Registar_Name
socket.on("Register_Name",function(data){
console.log(data)
io.sockets.emit("r_name","<strong>"+data+"</strong> Has Joined The Chat");
//Now Listening To A Chat Message
socket.on("Send_msg",function(data){
history.push(data)
console.log(history)
io.sockets.emit("msg",data);
})
})
})
I wouldn't suggest using socket for retrieving old messages. What you can do is that, you can use AJAX to retrieve old messages and Socket for the live chat.
you need to store the messages in a database and when a new user entered the chat, read the old messages from the db and show them to user, i don't think there is another solution for that!
Update:
so you could just make an array and store every message in the form of an js object with the sender info and when a user entered, once in the connection event send the array to client and in the client parse the array in the way you want.
for your question about how long should get, as i said you can send the prev messages to the client only once and in the first entrance.
Update 2: Send History:
you can use the acknowledgements part of socket.io:
const history = []
//Telling Express+Socket.io App To Listen To Port
io.sockets.on("connection", function (socket) {
socket.emit("Start_Chat");
//On Event Registar_Name
socket.on("Register_Name", function (data, ack) {
io.sockets.emit("r_name", "<strong>" + data + "</strong> Has Joined The Chat");
console.log(data)
//**** Make change Here
ack({ history });
});
socket.on("Send_msg", function (data) {
history.push(data)
console.log(history)
io.sockets.emit("msg", data);
//Now Listening To A Chat Message
});
});
And to get the ack data in client part:
socket.emit("Register_Name", your_data, function (ack) {
//you get the prev messages
console.log(ack);
});
and you don't need to store the clients for this!

How to get current socket object or id with in a sails controller?

I would like to access the currently connected socket id with in a sails.js (v0.12 ) controller function.
sails.sockets.getId(req.socket); is showing undefined since this is not a socket request
My objective is to set the online status of my user in the database when he logged in successfully
login: function (req, res) {
Util.login(req, function(){
var socketId = sails.sockets.getId(req.socket);
console.log('socketId ===', socketId); // return undefined
});
},
Basically i would like to access the current user's socket object in a controller or access current user's session object with in a socket on method
Also i'm not sure that how can i rewrite my old sockets.onConnect
handler
onConnect: function(session, socket) {
// Proceed only if the user is logged in
if (session.me) {
//console.log('test',session);
User.findOne({id: session.me}).exec(function(err, user) {
var socketId = sails.sockets.getId(socket);
user.status = 'online';
user.ip = socket.handshake.address;
user.save(function(err) {
// Publish this user creation event to every socket watching the User model via User.watch()
User.publishCreate(user, socket);
});
// Create the session.users hash if it doesn't exist already
session.users = session.users || {};
// Save this user in the session, indexed by their socket ID.
// This way we can look the user up by socket ID later.
session.users[socketId] = user;
// Persist the session
//session.save();
// Get updates about users being created
User.watch(socket);
// Send a message to the client with information about the new user
sails.sockets.broadcast(socketId, 'user', {
verb :'list',
data:session.users
});
});
}
},
You need to pass the req object to the method.
if (req.isSocket) {
let socketId = sails.sockets.getId(req);
sails.log('socket id: ' + socketId);
}
Since the request is not a socket request, you might need to do something like
Send back some identifier to the client once logged in.
Use the identifier to join a room. (One user per room. )
Broadcast messages to the room with the identifier whenever you need to send message to client.
https://gist.github.com/crtr0/2896891
Update:
From sails migration guide
The onConnect lifecycle callback has been deprecated. Instead, if you need to do something when a new socket is connected, send a request from the newly-connected client to do so. The purpose of onConnect was always for optimizing performance (eliminating the need to do this initial extra round-trip with the server), yet its use can lead to confusion and race conditions. If you desperately need to eliminate the server roundtrip, you can bind a handler directly on sails.io.on('connect', function (newlyConnectedSocket){}) in your bootstrap function (config/bootstrap.js). However, note that this is discouraged. Unless you're facing true production performance issues, you should use the strategy mentioned above for your "on connection" logic (i.e. send an initial request from the client after the socket connects). Socket requests are lightweight, so this doesn't add any tangible overhead to your application, and it will help make your code more predictable.
// in some controller
if (req.isSocket) {
let handshake = req.socket.manager.handshaken[sails.sockets.getId(req)];
if (handshake) {
session = handshake.session;
}
}

How to organise multiple Redis clients

I'm using the redis-sentinel-client library to manage a connection to a Redis sentinel group. The issue I have is that upon connecting I need to process records which may or may not already be present in the Redis store.
As I have two clients (due to the fact that one is a subscriber) I am not sure the best way to organise my event listeners so that I guarantee that both clients are ready prior to attempting any operations.
At the moment I have the following:
var sentinelSubscriberClient = RedisSentinel.createClient(opts);
var sentinelPublisherClient = RedisSentinel.createClient(opts);
sentinelSubscriberClient.on('ready', function redisSubscriberClientReady() {
sentinelPublisherClient.removeAllListeners('ready');
sentinelPublisherClient.on('ready', function () {
supportedChannels.forEach(function (channel) {
sentinelSubscriberClient.subscribe(channel);
});
// Includes reading + publishing via `sentinelPublisherClient`
processUnprocessed();
});
});
(there are also error listeners but I've removed them to make the code easier to read)
This current approach falls over if the publisher client emits ready before the subscriber client. My question is how can I organise the event listeners so that I can safely call .subscribe() on the subscriber client and various methods (.lrange(), .publish() etc.) of the publisher listener?
Thanks!
Simply move client creation into the ready callback function.
var sentinelSubscriberClient = RedisSentinel.createClient(opts);
var sentinelPublisherClient = null;
sentinelSubscriberClient.on('ready', function redisSubscriberClientReady() {
sentinelPublisherClient = RedisSentinel.createClient(opts);
sentinelPublisherClient.on('ready', function () {
supportedChannels.forEach(function (channel) {
sentinelSubscriberClient.subscribe(channel);
});
// Includes reading + publishing via `sentinelPublisherClient`
processUnprocessed();
});
});

Resources