6

I'm trying to setup my server with websockets so that when I update something via my routes I can also emit a websocket message when something on that route is updated.

The idea is to save something to my Mongo db when someone hits the route /add-team-member for example then emit a message to everyone who is connected via websocket and is a part of whatever websocket room that corresponds with that team.

I've followed the documentation for socket.io to setup my app in the following way:

App.js

// there's a lot of code in here which sets what to use on my app but here's the important lines

const app = express();
const routes = require('./routes/index');

const sessionObj = {
    secret: process.env.SECRET,
    key: process.env.KEY,
    resave: false,
    saveUninitialized: false,
    store: new MongoStore({ mongooseConnection: mongoose.connection }),
             secret : 'test',
             cookie:{_expires : Number(process.env.COOKIETIME)}, // time im ms    
}

app.use(session(sessionObj));
app.use(passport.initialize());
app.use(passport.session());

module.exports = {app,sessionObj};

start.js

const mongoose = require('mongoose');
const passportSocketIo = require("passport.socketio");
const cookieParser = require('cookie-parser');

// import environmental variables from our variables.env file
require('dotenv').config({ path: 'variables.env' });

// Connect to our Database and handle an bad connections
mongoose.connect(process.env.DATABASE);

// import mongo db models
require('./models/user');
require('./models/team');

// Start our app!
const app = require('./app');
app.app.set('port', process.env.PORT || 7777);

const server = app.app.listen(app.app.get('port'), () => {
  console.log(`Express running → PORT ${server.address().port}`);
});

const io = require('socket.io')(server);

io.set('authorization', passportSocketIo.authorize({
  cookieParser: cookieParser,
  key:         app.sessionObj.key,       // the name of the cookie where express/connect stores its session_id 
  secret:      app.sessionObj.secret,    // the session_secret to parse the cookie 
  store:       app.sessionObj.store,        // we NEED to use a sessionstore. no memorystore please 
  success:     onAuthorizeSuccess,  // *optional* callback on success - read more below 
  fail:        onAuthorizeFail,     // *optional* callback on fail/error - read more below 
}));


function onAuthorizeSuccess(data, accept){}

function onAuthorizeFail(data, message, error, accept){}

io.on('connection', function(client) {  
  client.on('join', function(data) {
      client.emit('messages',"server socket response!!");
  });

  client.on('getmessage', function(data) {
    client.emit('messages',data);
});  

});

My problem is that I have a lot of mongo DB save actions that are going on in my ./routes/index file and I would like to be able to emit message from my routes rather than from the end of start.js where socket.io is connected.

Is there any way that I could emit a websocket message from my ./routes/index file even though IO is setup further down the line in start.js?

for example something like this:

router.get('/add-team-member', (req, res) => {
  // some io.emit action here
});

Maybe I need to move where i'm initializing the socket.io stuff but haven't been able to find any documentation on this or perhaps I can access socket.io from routes already somehow?

Thanks and appreciate the help, let me know if anything is unclear!

red house 87
  • 1,837
  • 9
  • 50
  • 99
  • 1
    You should be able to broadcast a message to all clients using the `io` instance. https://stackoverflow.com/questions/7352164/update-all-clients-using-socket-io – WoodyWoodsta Jan 16 '18 at 13:32

5 Answers5

2

As mentioned above, io is in your global scope. If you do

router.get('/add-team-member', (req, res) => {
    io.sockets.emit('AddTeamMember');
});

Then every client connected, if listening to that event AddTeamMember, will run it's associated .on function on their respective clients. This is probably the easiest solution, and unless you're expecting a huge wave of users without any plans of load balancing, this should be suitable for the time being.

Another alternative you can go: socket.io lib has a rooms functionality where you can join and emit using the io object itself https://socket.io/docs/rooms-and-namespaces/ if you have a knack for this, it'd look something like this:

io.sockets.in('yourroom').broadcast('AddTeamMember');

This would essentially do the same thing as the top, only instead of broadcasting to every client, it'd only broadcast to those that are exclusive to that room. You'd have to basically figure out a way to get that users socket into the room //before// they made the get request, or in other words, make them exclusive. That way you can reduce the amount of load your server has to push out whenever that route request is made.

Lastly, if neither of the above options work for you, and you just absolutely have to send to that singular client when they initiate it, then it's going to get messy, because you have to have some sort of id to that person, and since you have no reference, you'd have to store all your sockets upon connection, and then make a comparison. I do not fully recommend something like this, because well, I haven't ever tested it, and don't know what type of repercussions could happen, but here is a jist of an idea I had:

app.set('trust proxy', true)
var SOCKETS = []
io.on('connection', function(client) {
  SOCKETS.push(client);
  client.on('join', function(data) {
    client.emit('messages',"server socket response!!");
  });

  client.on('getmessage', function(data) {
    client.emit('messages',data);
  });
});

router.get('/add-team-member', (req, res) => {
    for (let i=0; i< SOCKETS.length; i++){
        if(SOCKETS[i].request.connection.remoteAddress == req.ip)
          SOCKETS[i].emit('AddTeamMember');
    }
});

Keep in mind, if you do go down this route, you're gonna need to maintain that array when users disconnect, and if you're doing session management, that's gonna get hairy really really quick.

Good luck, let us know your results.

simon
  • 854
  • 1
  • 9
  • 23
  • The issue with this answer is that you're defining io at runtime in start.js and router is defined in the routes/Index file which is required in app.js so router will be undefined at that point no? – red house 87 Jan 27 '18 at 11:53
  • If routes/index is defined in app.js and app.js is defined before io, there is no problem – simon Jan 27 '18 at 20:25
  • Basically the flow would go like this: app and io are defined on server start. user connects, thus defining router. Router.route is called based upon user behavior, io should still be defined on server even if that is the case. – simon Jan 27 '18 at 20:27
  • io is defined on my server and i've managed to get it to my routes, but it doesn't go to my routes with any socket sessions attached when I call io.sockets.sockets. This is the issue – red house 87 Jan 29 '18 at 13:32
2

Yes, it is possible, you just have to attach the instance of socket.io as long as you get a request on your server. Looking to your file start.js you just have to replace your functions as:

// Start our app!
const app = require('./app');
app.app.set('port', process.env.PORT || 7777);
const io = require('socket.io')(app.app);

const server = app.app.listen(app.app.get('port'), () => {
server.on('request', function(request, response){
    request.io = io;
}
console.log(`Express running → PORT ${server.address().port}`);
});

now when you receive an event that you want to emit some message to the clients you can use your io instance from the request object.

router.get('/add-team-member', (req, res) => {
    req.io.sockets.emit('addteammember', {member: 6});
    //as you are doing a broadcast you just need broadcast msg
    ....
    res.status(200)
    res.end()
});

Doing that i also were able to integrate with test framework like mocha, and test the events emited too...

I did some integrations like that, and in my experience the last thing to do was emit the msg to instances in the socket.

As a good practice the very begining of middleware functions i had were doing data validation, data sanitization and cleaning data. Here is my working example:

var app = require('../app');
var server = require('http').Server(app);
var io = require('socket.io')(server);

io.on('connection', function(client) {
        client.emit('connected');
        client.on('disconnect', function() {
            console.log('disconnected', client.id);
        });
});

server.on('request', function(request, response) {
    request.io = io;
});

pg.initialize(app.config.DATABASEURL, function(err){
  if(err){
    throw err;
  }

  app.set('port', process.env.PORT || 3000);

    var server1 = server.listen(app.get('port'), function(){
    var host = 'localhost';
    var port = server1.address().port;

    console.log('Example app listening at http://%s:%s', host, port);
  });
});
Danizavtz
  • 3,166
  • 4
  • 24
  • 25
  • the solution doesn't seem to work! if I attach the io instance to the request object and try calling req.io.sockets.sockets it should return me all live sockets instead it returns me a blank object... – red house 87 Jan 29 '18 at 13:29
  • can you put the server.on('request', function(request, response){ request.io = io; } before the const server and see if it works? I have a working example and i can show you it working – Danizavtz Jan 29 '18 at 13:55
  • yea that doesn't solve it i'm afraid, do you have a link to the a repo you can show me? – red house 87 Jan 29 '18 at 20:46
  • I just updated my answers, i do not have a working example in github. – Danizavtz Jan 29 '18 at 21:49
  • Here is the repository which i based my solution: https://gist.github.com/patrickbrandt/1cd98a02c42e9e22a5a9 – Danizavtz Feb 06 '18 at 12:09
1

You can use emiter-adapter to emit data to client in other process/server. It use redis DB as backend for emitting messages.

Oleksandr Kyrpa
  • 638
  • 10
  • 17
1

I did something similar in the past, using namespaces.

Let's say your client connect to your server using "Frontend" as the namespace. My solution was to create the instance of socket.io as a class in a separate file:

websockets/index.js

const socket = require('socket.io');

class websockets {
  constructor(server) {
    this.io = socket(server);
    this.frontend = new Frontend(this.io);

    this.io.use((socket, next) => {
      // put here the logic to authorize your users..
      // even better in a separate file :-)
      next();
    });
  }
}

class Frontend {
  constructor(io) {
    this.nsp = io.of('/Frontend');

    [ ... ]
  }
}

module.exports = websockets;

Then in App.js

const app = require('express')();
const server = require('http').createServer(app);
const websockets = require('./websockets/index');
const WS = new websockets(server);

app.use('/', (req, res, next) => {
  req.websocket = WS;
  next();
}, require('./routes/index'));

[ ... ]

Finally, your routes can do:

routes/index.js

router.get('/add-team-member', (req, res) => {
  req.websocket.frontend.nsp.emit('whatever', { ... });

  [ ... ]
});
menestrello
  • 181
  • 1
  • 7
1

Your io is actually the socket object, you can emit events from this object to any specific user by -

io.to(userSocketId).emit('eventName', data);

Or you can broadcast by -

io.emit('eventName', data);

Just create require socket.io before using it :)

Rahul
  • 863
  • 6
  • 23