-
Notifications
You must be signed in to change notification settings - Fork 10.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Target specific client on clustered socket.io server and using acknowledgment callback #1811
Comments
@FREEZX has been implementing the functionality to receive clients in rooms (even over multiple node instances, see: Guille: Update: we're making a minor release 1.1.1 with bugfixes, and .clients(fn) should be in the next one 1.2.0 |
Every client joins a room with his socket id as a key. You could emit to that room if you know the id of the client's socket (you do if you're using my redis adapter with the clients function). |
True, I understand this, but my main issue is that the callback mechanism won't work when you emit in rooms. |
One, quite complex workaround:
|
One thing I should clarify, I don't want to add complexity to the target client. The target client should just be able to call the callback. |
You could implement a redis pubsub to do IPC between nodes, so whichever node receives the acknowledgement can publish a message on the IPC channel and the correct node could handle it as an acknowledgement callback |
This avoids cluttering the sockets, and IPC will probably also be needed in the future. |
True, but I'm not sure if that's easier. That would work something like this I think:
When subscribing, unsubscribing is inefficient I could also store the callbacks in an object under the unique event id. |
You don't have to temporarily subscribe/unsubscribe. Just handle the events if the client is in an array or something |
Alright, that would work something like:
Of course, one day I hope to simply be able to request the clients from a room and emit to a specific client (possibly on another node.js instance) (using a callback). @FREEZX has already made step one possible. |
You can avoid targetedevent IPC calls, because you could emit that from any process (to the room with the socket's ID as a name). Socket.io automatically decides which server has the socket. So that's one less IPC event to listen for. |
I was curious how difficult it would be to emit to a specific client (possibly on another node.js instance) using a callback. So for this to happen adapters should also handle general packets (not just broadcasts). The socket module's |
@FREEZX, but then there is no way to add a callback? Or am I forgetting something? |
Add the callback to an array of your choice, where the key is the client id and then broadcast to the client's room. The client responds back to a process of your cluster that emits the ack message via redis IPC. The node that originally sent the message will have the callback stored in the callbacks array and will call it. |
Your help is greatly appreciated but I don't follow the "The client responds back to a process of your cluster" part. I think I got the basic idea working, using the following utility. It's a pub/sub channel wrapper with a callback feature. // Redis pub/sub channel wrapper
// Messages are serialized to JSON by default, so you can send regular objects across the wire.
var redis = require('redis');
var util = require("util");
var EventEmitter = require("events").EventEmitter;
var debug = require('debug')('redis-channel');
var EVENT_MSG = "event";
var ACK_MSG = "ack";
var callbackIDCounter = 0;
var callbacks = {};
module.exports = Channel;
function Channel(name,port,host) {
if (!(this instanceof Channel)) return new Channel(name,port,host);
var _self = this;
var pub = redis.createClient(port,host);
var sub = redis.createClient(port,host);
pub.on('error',onRedisError);
sub.on('error',onRedisError);
function onRedisError(err){
_self.emit("error",err);
}
sub.subscribe(name,function(err){
debug("subsribed to channel");
if(err !== null) _self.emit("error",err);
});
sub.on("message", function (channel, packet) {
if(channel !== name) return;
packet = JSON.parse(packet);
debug("received packet: ",packet);
var data = packet.data;
var callbackID = packet.id;
switch(packet.type){
case EVENT_MSG:
// ToDo: emit all arguments from data
data.unshift("message"); // add event type in front
data.push(eventCallback); // add callback to end
_self.emit.apply(_self,data);
//_self.emit("message",message,eventCallback);
function eventCallback() {
if(callbackID === undefined) {
return _self.emit("error","No callback defined");
}
var args = Array.prototype.slice.call(arguments);
var ackPacket = {type:ACK_MSG,
data:args,
id: callbackID};
debug("publishing ack packet: ",ackPacket);
pub.publish(name,JSON.stringify(ackPacket),function(err){
if(err !== null) _self.emit("error",err);
});
}
break;
case ACK_MSG:
if(typeof callbacks[callbackID] == 'function'){
//callbacks[callbackID](message);
callbacks[callbackID].apply(this, packet.data);
delete callbacks[callbackID];
}
break;
}
});
this.publish = function() {
var args = Array.prototype.slice.call(arguments);
var packet = {type:EVENT_MSG,
data:args};
// is there a callback function?
if(typeof args[args.length - 1] == 'function') {
packet.id = callbackIDCounter++;
callbacks[packet.id] = packet.data.pop();
}
debug("publishing packet: ",packet);
pub.publish(name,JSON.stringify(packet),function(err){
if(err !== null) _self.emit("error",err);
});
};
}
util.inherits(Channel, EventEmitter); Experiment server: 'use strict';
var debug = require('debug')('clustering:server');
var express = require('express');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var socketIORedis = require('socket.io-redis');
var redisHost = 'localhost';
var redisPort = 6379;
var redisChannel = require('./util/redis-channel')('pub/sub-ack experiment',redisPort,redisHost);
var PORT = process.env.PORT ? process.env.PORT : 7000;
io.adapter(socketIORedis({ key: 'socket.io-redis-experiment'}));
http.listen(PORT, function(){
debug('server listening on *:' + PORT);
});
var nsp = io.of("/");
var targetSocket = null;
nsp.on('connection', function(socket){
var query = socket.handshake.query;
debug('new connection: '+socket.id,query.target||'');
if(query.type === "target") {
targetSocket = socket;
redisChannel.on("message", function (message,callback) {
debug("received hello pub/sub message: ",message,callback);
debug("emitting hello to target");
targetSocket.emit(message,callback);
});
} else {
socket.on('hello', function(data,callback){
debug('received hello event: ',data,callback);
// can't emit to targetSocket directly so
// we use a redis pub/sub channel
debug("publishing hello pub/sub message");
redisChannel.publish("hello",callback);
});
}
socket.on('disconnect', function(){
debug('disconnect: '+socket.id);
});
}); Experiment client: 'use strict';
var debug = require('debug')('clustering:client');
var socketClient = require('socket.io-client');
var PORT = process.env.PORT ? process.env.PORT : 7000;
var TYPE = process.env.TYPE ? process.env.TYPE : '';
var nspName = "/";
var nspURL = "http://localhost:"+PORT+nspName+"?type="+TYPE;
debug("connecting to: "+nspName);
var nsp = socketClient(nspURL);
nsp.once('connect', function(){
debug("connected to: "+nspName);
if(TYPE !== "target") {
nsp.emit("hello",{},function(data) {
debug("received hello response: ",data);
});
}
nsp.on("hello",function(callback) {
debug("received hello! ",callback);
callback("client on port "+PORT);
});
});
nsp.on('error', function (err){
debug("error connecting to: "+nspName+": "+err);
//callback(err,nsp);
}); |
I published my pub/sub wrapper as an npm package. |
What i meant with "The client responds back to a process of your cluster": |
@FREEZX your solution with the array makes a ton of sense! Im wondering if this is the mechanism you were going to use behind the scenes in 1.2? Im only asking because im litterally migrating my site as we speek to a more scalable solution and need to be able to implement the callback. Would this also work with the socket.io-emitter package if the server registered to the same redis channel? Do you have an idea of timeframe? I want to determine if I should wait for your release or move forward with the above solution. Thanks! |
IPC has to be done manually at the moment, with arrays as i said. I am not an official maintainer so i cannot say whether or not IPC will get added and if so, when. |
Has this scenario been solved using socket.io, socket.io-redis, and socket.io-emitter? |
Tests will be added in the parent repository. Related: - socketio/socket.io#1811 - socketio/socket.io#4163
Syntax: ```js io.timeout(1000).emit("some-event", (err, responses) => { // ... }); ``` The adapter exposes two additional methods: - `broadcastWithAck(packets, opts, clientCountCallback, ack)` Similar to `broadcast(packets, opts)`, but: * `clientCountCallback()` is called with the number of clients that received the packet (can be called several times in a cluster) * `ack()` is called for each client response - `serverCount()` It returns the number of Socket.IO servers in the cluster (1 for the in-memory adapter). Those two methods will be implemented in the other adapters (Redis, Postgres, MongoDB, ...). Related: - #1811 - #4163 - socketio/socket.io-redis-adapter#445
This was eventually implemented in version io.timeout(1000).emit("some-event", (err, responses) => {
// ...
}); Feedback is welcome! |
Syntax: ```js io.timeout(1000).emit("some-event", (err, responses) => { // ... }); ``` The adapter exposes two additional methods: - `broadcastWithAck(packets, opts, clientCountCallback, ack)` Similar to `broadcast(packets, opts)`, but: * `clientCountCallback()` is called with the number of clients that received the packet (can be called several times in a cluster) * `ack()` is called for each client response - `serverCount()` It returns the number of Socket.IO servers in the cluster (1 for the in-memory adapter). Those two methods will be implemented in the other adapters (Redis, Postgres, MongoDB, ...). Related: - socketio#1811 - socketio#4163 - socketio/socket.io-redis-adapter#445
@FREEZX @peteruithoven @sgiachero @darrachequesne @maccman recently faced the same issue i think . like i am operating on a clustered environment using NodeJS clusters , what initially i was doing is that
b. eventually i figured out that somehow just by writing this code it would automatically search for the client with the given socketID globally ( idk how is this working if anyone could help ) still i would like to know if anyone could help me finding how is it able to globally search for the recipient how efficient is it |
I've clustered my socket.io server and I'm looking for a way to emit to a specific client and receiving a acknowledgment callback. The problem that the server doesn't have a reference to a instance of the target client, because this client can be connected to another socket.io server instance (because it's clustered).
I know I can emit to a room named after the the socket.id or a custom room where I put the client in. But the problem is that this is considered broadcasting and then I can't use a acknowledgment callback.
I don't want to add complexity to the target client. The target client should just be able to call the callback.
I see two possible solutions:
A fake socket instance wouldn't actually have a connection to a client, but I hope to use it's emit function, have it talk to it's adapter (socket.io-redis) and have it receive the acknowledgment callback.
Any tips on these solutions, maybe something I should also consider.
The text was updated successfully, but these errors were encountered: