6

I am using Node.js to create a media upload microservice. This service works by taking in the binary data of the upload to a buffer, and then using the S3 npm package to upload to an S3 bucket. I am trying to use the eventEmitter present in that package which shows the amount of data uploaded to S3, and send that back to the client which is doing the uploading (so that they can see upload progress). I am using socket.io for this sending of progress data back to the client.

The problem I am having is that the .emit event in socket.io will send the upload progress data to all connected clients, not just the client which initiated the upload. As I understand it, a socket connects to a default room on 'connection', which is mirrored by the 'id' on the client side. According to the official docs, using socket.to(id).emit() should send the data scoped only to that client, but this is not working for me.

UPDATED Example code:

server.js:

var http = require('http'),
users = require('./data'),
app = require('./app')(users);

var server = http.createServer(app);

server.listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

var io = require('./socket.js').listen(server);

socket.js:

var socketio = require('socket.io');

var socketConnection = exports = module.exports = {};

socketConnection.listen = function listen(app) {
    io = socketio.listen(app);
    exports.sockets = io.sockets;

    io.sockets.on('connection', function (socket) {
        socket.join(socket.id);
        socket.on('disconnect', function(){
            console.log("device "+socket.id+" disconnected");
        });
        socketConnection.upload = function upload (data) {
        socket.to(socket.id).emit('progress', {progress:(data.progressAmount/data.progressTotal)*100});
    };
});
return io;   
};

s3upload.js:

var config = require('../config/aws.json');
var s3 = require('s3');
var path = require('path');
var fs = require('fs');
var Busboy = require('busboy');
var inspect = require('util').inspect;

var io = require('../socket.js');
...
var S3Upload = exports = module.exports = {};
....
S3Upload.upload = function upload(params) {
// start uploading to uploader
var uploader = client.uploadFile(params);

uploader.on('error', function(err) {
    console.error("There was a problem uploading the file to bucket, either the params are incorrect or there is an issue with the connection: ", err.stack);
    res.json({responseHTML: "<span>There was a problem uploading the file to bucket, either the params are incorrect or there is an issue with the connection. Please refresh and try again.</span>"});
    throw new Error(err);
}),

uploader.on('progress', function() {
    io.upload(uploader);
}),

uploader.on('end', function(){
    S3Upload.deleteFile(params.localFile);
});
};

When using DEBUG=* node myapp.js, I see the socket.io-parser taking in this information, but it isn't emitting it to the client:

socket.io-parser encoding packet {"type":2,"data":["progress",{"progress":95.79422221709825}],"nsp":"/"} +0ms


socket.io-parser encoded {"type":2,"data":["progress",{"progress":95.79422221709825}],"nsp":"/"} as 2["progress",{"progress":95.79422221709825}] +0ms

However, if I remove the .to portion of this code, it sends the data to the client (albeit to all clients, which will not help at all):

io.sockets.on('connection', function(socket) {
    socket.join(socket.id);
    socket.emit('progress', {progress: (data.progressAmount/data.progressTotal)*100});
});

DEBUG=* node myapp.js:

socket.io:client writing packet {"type":2,"data":["progress",{"progress":99.93823786632886}],"nsp":"/"} +1ms
  socket.io-parser encoding packet {"type":2,"data":["progress",{"progress":99.93823786632886}],"nsp":"/"} +1ms
  socket.io-parser encoded {"type":2,"data":["progress",{"progress":99.93823786632886}],"nsp":"/"} as 2["progress",{"progress":99.93823786632886}] +0ms
  engine:socket sending packet "message" (2["progress",{"progress":99.93823786632886}]) +0ms
  engine:socket flushing buffer to transport +0ms
  engine:ws writing "42["progress",{"progress":99.84186540937002}]" +0ms
  engine:ws writing "42["progress",{"progress":99.93823786632886}]" +0ms

What am I doing wrong here? Is there a different way to emit events from the server to only specific clients that I am missing?

Jonathan Kempf
  • 699
  • 1
  • 13
  • 27

3 Answers3

4

The second example of code you posted should work and if it does not, you should post more code.

As I understand it, a socket connects to a default room on 'connection', which is mirrored by the 'id' on the client side. According to the official docs, using socket.to(id).emit() should send the data scoped only to that client, but this is not working for me.

Socket.io is pretty much easier than that. The code below will send a 'hello' message to each client when they connect:

io.sockets.on('connection', function (socket) {
  socket.emit('hello');
});

Everytime a new client connects to the socket.io server, it will run the specified callback using that particular socket as a parameter. socket.id is just an unique code to identify that socket but you don't really need that variable for anything, the code above shows you how to send a message through a particular socket.

Socket.io also provides you some functions to create namespaces/rooms so you can group connections under some identifier (room name) and be able to broadcast messages to all of them:

io.sockets.on('connection', function (socket) {
    // This will be triggered after the client does socket.emit('join','myRoom')
    socket.on('join', function (room) {
        socket.join(room); // Now this socket will receive all the messages broadcast to 'myRoom'
    });
...

Now you should understand socket.join(socket.id) just does not make sense because no socket will be sharing socket id.

Edit to answer the question with the new code:

You have two problems here, first:

    socketConnection.upload = function upload (data) {
        socket.to(socket.id).emit('progress', {progress:(data.progressAmount/data.progressTotal)*100});
    };

Note in the code above that everything inside io.sockets.on('connection',function (socket) { will be run each time a clients connect to the server. You are overwriting the function to point it to the socket of the latest user.

The other problem is that you are not linking sockets and s3 operations. Here is a solution merging socket.js and s3upload.js in the same file. If you really need to keep them separated you will need to find a different way to link the socket connection to the s3 operation:

var config = require('../config/aws.json');
var s3 = require('s3');
var path = require('path');
var fs = require('fs');
var Busboy = require('busboy');
var inspect = require('util').inspect;
var io = require('socket.io');

var socketConnection = exports = module.exports = {};
var S3Upload = exports = module.exports = {};

io = socketio.listen(app);
exports.sockets = io.sockets;

io.sockets.on('connection', function (socket) {

    socket.on('disconnect', function(){
        console.log("device "+socket.id+" disconnected");
    });

    socket.on('upload', function (data) { //The client will trigger the upload sending the data
        /*
            some code creating the bucket params using data
        */
        S3Upload.upload(params,this);
    });
});

S3Upload.upload = function upload(params,socket) { // Here we pass the socket so we can answer him back
    // start uploading to uploader
    var uploader = client.uploadFile(params);

    uploader.on('error', function(err) {
        console.error("There was a problem uploading the file to bucket, either the params are incorrect or there is an issue with the connection: ", err.stack);
        res.json({responseHTML: "<span>There was a problem uploading the file to bucket, either the params are incorrect or there is an issue with the connection. Please refresh and try again.</span>"});
        throw new Error(err);
    }),

    uploader.on('progress', function() {
        socket.emit('progress', {progress:(uploader.progressAmount/uploader.progressTotal)*100});
    }),

    uploader.on('end', function(){
        S3Upload.deleteFile(params.localFile);
    });
};
Javier Conde
  • 2,553
  • 17
  • 25
  • I have edited my question to show more of the underlying code architecture. I cannot simply emit on connection, as I am passing information from another route into the socket. – Jonathan Kempf Nov 24 '15 at 15:49
  • I have edited my answer according to your code, I have merged `socket.js` and `s3upload.js` in the same file to make it easier. If you really need to keep them separated, I'd recommend you to toggle the dependency having socket.js importing S3 and not the other way around. You will need to call functions based on socket events so it will need to access the `s3upload.js` functions. – Javier Conde Nov 24 '15 at 19:12
  • I will try to re-architecture my app with your suggestions, thanks for the help. Related question based on your suggestion to move my code around: Is it true that when using socket, apps should really be designed around socket.io, so that all logic is passed through the socket handler? If that is true, wouldn't that make dedicated routes impossible to write while using socket? – Jonathan Kempf Nov 25 '15 at 17:17
  • 1
    I've done a few socket.io applications and tried the same you were doing above without much success. I was able to push it down the dependency hierarchy and configure some socket events at upper levels, but at the end I realized that it was getting much more complicated and redundant from a design point of view. HTTP server is fine above and at the same module than socket.io (in my experience) because it is a service but if you are going to use backend functions triggered by socket events, you better have them below or at the same module. – Javier Conde Nov 25 '15 at 17:31
  • Again, thanks for the explanation. Is there something in socket.io that causes the socket methods to be scoped only to the module it is called in? It seems like a common use-case where people would want to progressively enhance a node app with socket, rather than build the entire backend logic based on socket.io. I am aware of socket-emitter module, but that also requires some dependencies, and since socket is in the overall node object when defined, I can't think of a good reason why these methods can't be global. – Jonathan Kempf Nov 25 '15 at 17:54
  • I have accepted your answer as it appears there isn't a way to separate out socket.io logic and emitters from the file it is defined in. A major limitation for sure, but perhaps my design isn't compatible with socket. I will work on publishing a module to alleviate this problem. – Jonathan Kempf Nov 27 '15 at 19:24
  • @JavierConde please take a look at my [question](https://stackoverflow.com/questions/54229400/socket-io-emit-requested-data-to-each-client-continuously) – node_man Jan 17 '19 at 05:17
1

According to the documentation all the users join the default room identified by the socket id, so no need for you to join in on connection. Still according to that, if you want to emit to a room in a namespace from a specific socket you should use socket.broadcast.to(room).emit('my message', msg), given that you want to broadcast the message to all the clients connected to that specific room.

João Antunes
  • 521
  • 4
  • 9
  • updating my code with that is giving me the same problem. It is running the parser, but not the engine or writestream. I am running socket.io 1.3.6, so these functions are definitely defined. Let me know if seeing more of my code would help, or if there is a way to further debug this that I am not trying. – Jonathan Kempf Nov 19 '15 at 16:59
1

All new connections are automatically joined to room having the name that is equal to their socket.id. You can use it to send messages to specific user, but you have to know socket.id associated with connection initialized by this user. You have to decide how you will manage this association (via databases, or in memory by having an array for it), but once you have it, just send progress percentage via:

socket.broadcast.to( user_socket_id ).emit( "progress", number_or_percent );
kaytrance
  • 2,657
  • 4
  • 30
  • 49