webSocketServer node.js how to differentiate clients - node.js

I am trying to use sockets with node.js, I succeded but I don't know how to differentiate clients in my code.
The part concerning sockets is this:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({port: 8080});
wss.on('connection', function(ws) {
ws.on('message', function(message) {
console.log('received: %s', message);
ws.send(message);
});
ws.send('something');
});
This code works fine with my client js.
But I would like to send a message to a particular user or all users having sockets open on my server.
In my case I send a message as a client and I receive a response but the others user show nothing.
I would like for example user1 sends a message to the server via webSocket and I send a notification to user2 who has his socket open.

In nodejs you can directly modify the ws client and add custom attributes for each client separately. Also you have a global variable wss.clients that can be used anywhere. Please try the following code with at least two clients connected:
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({
server: httpsServer
});
wss.getUniqueID = function () {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return s4() + s4() + '-' + s4();
};
wss.on('connection', function connection(ws, req) {
ws.id = wss.getUniqueID();
wss.clients.forEach(function each(client) {
console.log('Client.ID: ' + client.id);
});
});
You can also pass parameters directly in the client connection URL:
https://myhost:8080?myCustomParam=1111&myCustomID=2222
In the connection function you can get these parameters and assign them directly to your ws client:
wss.on('connection', function connection(ws, req) {
const parameters = url.parse(req.url, true);
ws.uid = wss.getUniqueID();
ws.chatRoom = {uid: parameters.query.myCustomID};
ws.hereMyCustomParameter = parameters.query.myCustomParam;
}

You can simply assign users ID to an array CLIENTS[], this will contain all users. You can directly send message to all users as given below:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({port: 8080}),
CLIENTS=[];
wss.on('connection', function(ws) {
CLIENTS.push(ws);
ws.on('message', function(message) {
console.log('received: %s', message);
sendAll(message);
});
ws.send("NEW USER JOINED");
});
function sendAll (message) {
for (var i=0; i<CLIENTS.length; i++) {
CLIENTS[i].send("Message: " + message);
}
}

you can use request header 'sec-websocket-key'
wss.on('connection', (ws, req) => {
ws.id = req.headers['sec-websocket-key'];
//statements...
});

This code snippet in Worlize server really helped me a lot. Even though you're using ws, the code should be easily adaptable. I've selected the important parts here:
// initialization
var connections = {};
var connectionIDCounter = 0;
// when handling a new connection
connection.id = connectionIDCounter ++;
connections[connection.id] = connection;
// in your case you would rewrite these 2 lines as
ws.id = connectionIDCounter ++;
connections[ws.id] = ws;
// when a connection is closed
delete connections[connection.id];
// in your case you would rewrite this line as
delete connections[ws.id];
Now you can easily create a broadcast() and sendToConnectionId() function as shown in the linked code.
Hope that helps.

It depends which websocket you are using. For example, the fastest one, found here: https://github.com/websockets/ws is able to do a broadcast via this method:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({host:'xxxx',port:xxxx}),
users = [];
wss.broadcast = function broadcast(data) {
wss.clients.forEach(function each(client) {
client.send(data);
});
};
Then later in your code you can use wss.broadcast(message) to send to all. For sending a PM to an individual user I do the following:
(1) In my message that I send to the server I include a username
(2) Then, in onMessage I save the websocket in the array with that username, then retrieve it by username later:
wss.on('connection', function(ws) {
ws.on('message', function(message) {
users[message.userName] = ws;
(3) To send to a particular user you can then do users[userName].send(message);

I'm using fd from the ws object. It should be unique per client.
var clientID = ws._socket._handle.fd;
I get a different number when I open a new browser tab.
The first ws had 11, the next had 12.

You can check the connection object. It has built-in identification for every connected client; you can find it here:
let id=ws._ultron.id;
console.log(id);

One possible solution here could be appending the deviceId in front of the user id, so we get to separate multiple users with same user id but on different devices.
ws://xxxxxxx:9000/userID/<<deviceId>>

By clients if you mean the open connections, then you can use ws.upgradeReq.headers['sec-websocket-key'] as the identifier. And keep all socket objects in an array.
But if you want to identify your user then you'll need to add user specific data to socket object.

If someone here is maybe using koa-websocket library, server instance of WebSocket is attached to ctx along side the request. That makes it really easy to manipulate the wss.clients Set (set of sessions in ws). For example pass parameters through URL and add it to Websocket instance something like this:
const wss = ctx.app.ws.server
const { userId } = ctx.request.query
try{
ctx.websocket.uid = userId
}catch(err){
console.log(err)
}

Use a global counter variable and assign its value for every new connection:
const wss = new WebSocket.Server({server});
let count_clients = 0;
wss.on('connection', function connection(ws){
ws.id=count_clients++;
console.log(`new connection, ws.id=${ws.id}, ${ws._socket.remoteAddress}:${ws._socket.remotePort} #clients=${wss.clients.size}`);
ws.on('close', req => {console.log(`disconnected, ws.id=${ws.id}, ${ws._socket.remoteAddress}:${ws._socket.remotePort} #clients=${wss.clients.size}`);});
...

Here is what I did:
* on connect, server generate an unique id (e.g uuid) for the connection,
* save it in memory, (e.g as key of map),
* send back to client in response,
*
*
* client save the id, on each request will also send the id as part of request data,
* then server identify the client by id, on receive further request,
*
* server maintain client, e.g cleanup on close/error,
*
I've impl the idea, it works well to identify the client.
And, I also achieved group/topic broadcast based on the idea, which need the server to maintain extra info.

There are a lot of interesting answers that do the job, however they mostly seem unclean, that is if you don't mind mutating the ws object. I did it this way because I'm using TypeScript and you can't arbitrarily add properties to objects.
import WebSocket from 'ws'
declare module 'ws' {
interface WebSocket {
id: any
key: string
}
}
The id doesn't have to be type any can be number or string depending on how you ID your connections. I haven't flushed out the system yet but for now when a connection is made, I just assign a random number.
const socketConnection = (socket: WebSocket.WebSocket): void => {
socket.id = Math.random()
console.log(socket.id)
const msg = JSON.stringify({ res: `[open] Welcome to the WebSocket server!` })
socket.send(msg)
}
This can be modified at any point so once I authenticate the connection I plan on assigning a relative ID here and might even add in a key property if I want to do some more fancy stuff.
How this works is explained in the Module Augmentation section of the documentation.
TypeScript: Module Augmentation
You can check that it's still assigned by looking over multiple messages in the onmessage event.
const socketMessage = (socket: WebSocket.WebSocket): void => {
socket.on('message', async (message: WebSocket.RawData) => {
console.log(socket.id)
console.log(socket.key)
})
}
Oh and a note, I made this module declaration in the document where I setup my socket. But the modification does populate across documents. For example in the AuthController I started prototyping I use it this way.
export default class AuthController {
public static connections: DLinkedList = new DLinkedList()
static async validate(request: { id: string, socket: WebSocket.WebSocket }): Promise<void> {
console.log('test', request.socket.id)
this.connections.add(request.socket, request.id)
request.socket.send(JSON.stringify({ res: true }))
console.log(this.connections.size())
}
static getSocket(id: string): WebSocket.WebSocket {
return this.connections.getAtKey(id).data
}
static removeSocket(socket: WebSocket.WebSocket) {
}
}
You can also do this in pure JS just by directly modifying the WebSocket object prototype. Some of the answers here talk about it. I haven't done it myself but the principle is similar.
Add a method to an existing class in typescript?
Hope this is useful.

Related

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.

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
}
}
});
});

socket.io+redis+expressjs cluster - get socket object in expressjs request

Question based on this answer: https://stackoverflow.com/a/18650183/4478897
I tried to find this solution but nothing seems to work in the way that I need.
Clustering expressjs and socket.io we can share sessions using redis and send io messages inside io world (io.sockets.on('connection',...). The problem is if we want to send the message (or use a simple socket.join/leave) inside the expressjs world (route.get/post).
If we are not using clusters we can atach the client socket object to the express request object (or simply export the io object) and then use it at any time on any GET/POST route.
At the other hand, if we are clustering and use the mentioned method to get the socket object inside the expressjs world, sometimes the socket object is undefined because the socket object for this client is initialized at other worker.
Some example flow:
Client connects to http://localhost and worker 1 handles this request.
After the page is loaded, the client connects to socket.io. Worker 2 handles this connection.
Client do a POST and again worker 1 or worker X handles this request.
In this case when the client do the POST, only the worker 2 knows the socket object for this client. So this will get an undefined socket object.
So, the question:
How can we get the client socket object from any worker to reuse it on expressjs request object.
Maybe my code is wrong but is almost like the link to the answer mentioned above.
NOTEs
Don't want to use some kind of proxy.
Don't want to migrate to other libraries (expressio, sockjs...)
Sorry for my English :)
Using last nodejs, socket.io, expressjs, socket.io-redis, redis... versions
Don't hesitate to ask something!
UPDATE 1
Possible solution but still need to test it. Dont know if this is a really good: solution.
UPDATE 3: Working code on my own answer
UPDATE 2
Like update 1 but using https://nodejs.org/dist/latest-v5.x/docs/api/cluster.html#cluster_event_message
remoteJoin and remoteLeave methods were added in socket.io-redis 3.0.0:
io.adapter.remoteJoin('<my-id>', 'room1', function (err) {
if (err) { /* unknown id */ }
// success
});
io.adapter.remoteLeave('<my-id>', 'room1', function (err) {
if (err) { /* unknown id */ }
// success
});
Note: The implementation looks a lot (hopefully?) like the answer above.
Well finally tried the code and it works (with some misspells modifications and other things) but i'm sure that needs to be a better code somewhere. So i'm open to more answers!
This code is part of my socket.io module when authorize the client socket and some other stuff...
var redis = require("redis");
var redisPub = redis.createClient();
var redisSub = redis.createClient();
var PubSubChannel = "clusterChannel";
// Function that checks if this worker knows the socket object of this socketId.
// If not, publish the message to all the other sockets (workers)
io.socketDo = function (type, socketId, roomName) {
if (typeof io.sockets.connected[socketId] != "undefined") {
if (type === "join") {
return io.sockets.connected[socketId].join(roomName);
}
if (type === "leave") {
return io.sockets.connected[socketId].leave(roomName);
}
} else {
redisPub.publish(
PubSubChannel,
JSON.stringify({
type: type,
socketId: '' + socketId,
roomName: roomName
})
);
}
};
// Subscribe to some channel
redisSub.subscribe(PubSubChannel);
// When this worker receive a message from channel "PubSubChannel" checks
// if it have the socket object for this socketId and do the operation
redisSub.on("message", function (channel, data) {
data = JSON.parse(data);
var type = data.type;
var socketId = data.socketId;
var roomName = data.roomName;
if ((type === "join" || type === "leave") && channel == PubSubChannel){
if (typeof io.sockets.connected[socketId] != "undefined") {
if (type === "join") {
return io.sockets.connected[socketId].join(roomName);
}
if (type === "leave") {
return io.sockets.connected[socketId].leave(roomName);
}
}
}
});
Then just simply export the module and attach it to your expressjs request => req.io = io
// req.session.socketId value is fetched on "io.sockets.on('connection', function(socket) {"
// by express to socket.io using redis shared sessions
app.get('/', function (req, res) {
req.io.socketDo('join', req.session.socketId, 'someRoomToJoin');
// IT WORKS!
req.io.sockets.in('someRoomToJoin').emit('text');
req.io.socketDo('leave', req.session.socketId, 'someRoomToLeave');
res.send('Hello World!');
});

how to change other users socket properties?

I am using nodejs + socket.io and have chat. Every user, when enters chat, get some data to his socket. For example:
module.exports = function(io) {
var chat = io.of('/chat').on('connection', function (socket) {
socket.on('start-chat', function(data) {
socket.user_name = data.name;
}
});
}
Question: How one user can change socket property of other? For example, i need to change others user socket.user_name, having his socket.id
You can get access to the connected clients and filter them for what ever criteria you need. IIRC you can also access them directly by ID if you happen to have the id with io.sockets.sockets[socket_id]
Another approach is to keep your own record of session. This means you can index using a key that you'd determine your self on each connection. An example:
var clientConnections = {};
sio.on('connection', function (socket) {
var key = <something unique, maybe based on socket.handshake data>;
clientConnections[key] = socket;
}
You can then just access the socket reference else where via the clientConnections hash: clientConnections[<some key].
Once you have that reference you should be able to manipulate the socket as if it was the subject of your event callback.

Save Data on Socket in Socket.IO

I want to save some data on the socket, server side, so whenever the client emits any data to the server, I want that data to be available!
One use case can be storing a token on the socket. When the client is connecting for the first time, it will emit the token, if it has one, or it will show the login page and then the login data will be sent to the server. Whichever one it is, I want to store the token on the server, so that every request after that doesn't need to specify the token.
Later, I'll use RedisStore, so all the data will be accessible all the servers running the app.
My only question is, where do I store the data on the socket so it's associated with that client?
on http://socket.io/#how-to-use
scroll to: Storing data associated to a client
use socket.set and socket.get to set and get data asynchronously
I'm suffering from the same question and guessing what's going on with an example code from socket.io on version 4.x
In the example, They use middleware(use function to register a middleware)
namespace.use((socket, next) => {
// get data from client
const sessionID = socket.handshake.auth.sessionID;
const {userId, username} = yourFunction();
// set socket specific data
socket.sessionID = sessionID;
socket.userID = session.userID;
socket.username = session.username;
next();
});
Middlewares are executed when a socket is connected with a server.
and you can use the data afterward
note - Socket.IO reference tells use socket.data for this purpose
namespace.on('connection', socket => {
socket.emit("join", `${socket.username} has been joined`);
})
If you use multiple servers, then you have to keep in mind that the data is only valid for the server
On multiple server environment, You need a single source of data which will be used by socket servers.
namespace.use(async (socket: Socket & { sessionID?: string, userID?: string, username?: string }, next) => {
const sessionID = socket.handshake.auth.sessionID; // [socket.handshake][4]
// or other [socket related attributes][4]
if (sessionID) {
// you have to implement a function to save and retrive session info
const session = await someFunctionToRetrieveSession(sessionID);
if (session) {
socket.sessionID = sessionID;
socket.userID = session.userID;
socket.username = session.username;
return next();
}
}
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error("invalid username"));
}
socket.sessionID = randomId();
socket.userID = randomId();
socket.username = username;
next();
});
and one more thing as I understood the namespace.use function is called only for the namespace if your client use other namespace then default then default('/') use function will not be called.
//client side
io("/chat");
...
//server side
io.use() // == io.of('/').use() will not be called
io.of('/chat').use() // only will be called
Thanksfully the author of the example implemented a sessionStorage using redis
refer to this example code
with this info, I guess socket.io server saves sockets' info in memory and set a property of a socket will be saved and when the socket comes later the server retrives the socket and it's related data. but because it happens on memory so you can't share the info among other servers that's why you have to find a way to share the data with other servers(eg. redis)
You can save the data on the global variables when you dont want to use any database
var globalVariable = {};
io.sockets.on("connection", function (socket) {
socket.on("save-client-data", function (clientData) {
var clientId = clientData.clientId;
globalVariable[clientId] = JSON.parse(clientHandshakeData);
});
socket.on("get-client-data", function (clientId) {
var clientData = globalVariable[clientId];
socket.emit("get-client-data", JSON.stringify(clientData));
});
});
This worked for my scenario, however I'm not aware of the performance implications.

Resources