DvD DvD - 4 months ago 26
Node.js Question

Node.js & Socket.io with multiple tabs

From my understanding, a socket.io server manages N connections between itself and N clients via N separate sockets, each with its own ID.

It therefore makes sense that you get two different ID's when opening two tabs.

Consider, however, this minimal example:

index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"></html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<title>Stuffy Stuff</title>
</head>

<body>
<button id="btnA">A</button>

<script src="/socket.io/socket.io.js"></script>
<script src="app.js"></script>
</body>


index.js

var express = require('express');
var path = require('path');
var app = express();
var servercode = require('./server');

app.use(express.static(path.join(__dirname,'public')));

var server = require('http').createServer(app).listen(process.env.PORT || 8080);
var io = require('socket.io').listen(server);

io.on('connection', function (socket)
{
console.log('client ' + socket.id + ' connected');
servercode.init(io, socket);
});


app.js

;
jQuery(function($) // client-side code
{
socket = io.connect();

$(document).on('click', '#btnA', stuff);

function stuff()
{
socket.emit('dostuff', socket.id);
socket.on('answerstuff', function()
{
console.log("Answered");
});
}
}($));


server.js

// Server-side

var io;
var socket;

exports.init = function(sio, sock)
{
io = sio;
socket = sock;

socket.on('dostuff', doStuff);
};

function doStuff(idd)
{
console.log('who is asking: ' + idd);
console.log('who am I answering to: ' + socket.id);
console.log(socket.id);
socket.emit('answerstuff');
}


Now say I fire up
node index.js
and then open two instances of
localhost:8080
; let's call them A and B. If I press the button on A's instance, the 'dostuff' message is emitted, it's caught by the server, but the socket it answers to is the one relative to B's instance, not A's. In general, it answers to the most recent instance. Why is it so?

EDIT: explanation
As I've finally grasped the nature of my mistake I thought I could help whoever runs into a similar issue.

Let's take our sample workflow: A connects, B connects, A presses button.


  1. server.init(io, socket A)
    gets called

  2. init(socket A)
    attaches the
    dostuff
    event, with callback function
    doStuff
    , to socket A

  3. B connects...
    init
    attaches
    dostuff
    to socket B

  4. Client A presses button, thus A emits
    dostuff
    passing socket A's id

  5. doStuff(idd)
    is executed, but its definition contains a reference to a variable (
    socket
    ) not defined within the function's scope, therefore it has to navigate its parent scopes

  6. Such reference is found: it's
    socket = sock
    (if we omit
    var
    , the declaration 'bubbles up' in scope until it finds a variable with such a name. Had we written
    var socket = sock
    INSIDE
    init
    , the function couldn't have found any such variable)


    1. But
      socket
      points to socket B thanks to B connecting and
      init
      being executed again. Therefore, the function only knows socket B.




In order to solve this issue we need to employ closures: functions that encapsulate the state of the world at the moment of their definition. In other words, a closure is an object containing the function definition AND the value of the variables not in the function scope but referenced by it.

As illustrated in the solution given by JagsSparrow, there are two ways of dealing with this issue and both involve creating a closure:

1. Use the
function.bind(thisArg, args)
function


The
bind()
function returns the function 'A' it's called upon where
this
is bound to
thisArg
(where it would normally point to the object calling A) and instantiates whatever arguments are specified in
args
.

In our case we don't need
this
as it's never mentioned in the
doStuff()
function, but we do need it to remember whose socket is the one mentioned in the arguments. We can do this by writing

doStuff.bind(null, socket)


This expression returns an object that is the
doStuff()
function where, in its context,
this
equals
null
(we could've written
doStuff.bind(this, socket)
: in that case,
this
equals
init
) and its first argument,
socket
, is bound to
socket
. Therefore,

socket.on('dostuff', doStuff.bind(this,socket));


tells socket A to fire up
doStuff
(which contains a reference to socket A) when
dostuff
happens. Same with socket B and any other socket.

2. Use a nested function

We simply move the definition of
function doStuff(idd)
inside
socket.on()
:

socket.on('dostuff', function doStuff(idd)
{
console.log('who is asking: ' + idd);
console.log('who am I answering to: ' + socket.id);
console.log(socket.id);
socket.emit('answerstuff');
});


This way, again, the socket is bound to a function that contains (by virtue of the closure) the definition of whichever socket is performing this operation.

Answer

In server.js you declared var socket; which is local of the server.js module (module scope),

So this variable gets assigned to different client each time its get connected

  1. When client A get connected socket is of client A.
  2. When client B get connected socket is of client B.

So, Don't make socket variable local to server.js module

Solution 1 : server.js

var io;
//var socket;

exports.init = function(sio, sock)
{
    io = sio;
    socket = sock;

    socket.on('dostuff', doStuff.bind(this,socket));
};

function doStuff(socket,idd)
{
    console.log('who is asking: ' + idd);
    console.log('who am I answering to: ' + socket.id);  
    console.log(socket.id);
    socket.emit('answerstuff');
}

Solution 2 : server.js

var io;
//var socket;

exports.init = function(sio, sock)
{
    io = sio;
    var socket = sock;

    socket.on('dostuff', function doStuff(idd)
    {
        console.log('who is asking: ' + idd);
        console.log('who am I answering to: ' + socket.id);  
        console.log(socket.id);
        socket.emit('answerstuff');
    });
};