- Building Scalable Apps with Redis and Node.js
- Joshua Johanan
- 1588字
- 2025-02-28 04:11:29
Creating namespaces
Socket.IO has another trick up its sleeve. We have been working in only one area so far, but Socket.IO has methods to connect to multiple areas. The first is namespaces. The other is rooms. Both of these ideas are very similar, and we will set both of them up.
We will use the same idea of users being able to ping, but we will add onto this idea. First of all, we will allow users to enter different areas and ping inside those areas. Secondly, we will allow users to send private pings that will only go to one specific user. For this, we will create a new project and start from scratch. We will use namespaces for the first project.
Here is all the boilerplate code that we need to do. Create a directory named second app
, files named namespace.js
and namespace.html
, and a symlink to the socket.io.js
client library (remember, it's in the node_modules
directory after you install Socket.IO).
We can now build our little app. We will start on the backend. Open up namespace.js
and add Socket.IO to our app:
var io = require('socket.io').listen(4000);
Now, add all your listeners. We will have three: join
, ping
, and privatePing
. Our connection listener should have all three of these functions in it, as shown in the following code:
io.sockets.on('connection', function(socket){ socket.on('join', function(data){ socket.username = data.username; socket.broadcast.emit('join', {username: data.username, socket: socket.id}); }); socket.on('ping', function(){ socket.broadcast.emit('ping', {username: socket.username}); }); socket.on('privatePing', function(data){ io.sockets.connected[data.socket].emit('ping', {username: socket.username, priv: true}); }); });
The join
and ping
events are very similar to the functions we built for our first app. The join
event adds the username to the socket and then does a broadcast emit back. It also broadcasts the client's socket ID to everyone else. We will use this later. The ping
event does almost the same, except it gets the username and sends that back.
This brings us to our new listener, privatePing
. It starts off by getting the username, but now it uses io.sockets.connected[data.socket]
. The data.socket
JavaScript object contains the socket ID and io.sockets.connected
has all the connected sockets. Put these together and we get a specific client connection. This is essentially a hash or dictionary of every connection with the socket ID as the key. Earlier, we sent the socket ID to the client, and this is the client sending it back to ping that user. We have a flag that shows this ping event is sent to only one client. So far, we have not really done anything too new and nothing that involves namespaces.
Well, let's add namespaces then. We will need to add another connection listener. The following code is what the file will look like:
io.of('/vip').on('connection', function(socket){ socket.on('join', function(data){ socket.username = data.username; socket.broadcast.emit('join', {username: data.username, socket: socket.id}); }); socket.on('ping', function(){ socket.broadcast.emit('ping', {username: socket.username});}); socket.on('privatePing', function(data){ io.of('/vip').connected[data.socket].emit('ping', {username: socket.username, priv: true}); }); });
The first thing you should notice is that it is really similar to the code that doesn't use namespaces. In fact, there are only two lines of code that are different. The first line is where we tie to a namespace. This is done using the of
method, as follows:
io.of('/vip').on('connection', function(socket){});
We just pass a string of the namespace. Inside the connection listener, we have the same object and can set up the same events. The socket variable here will only refer to clients that have connected to the '/vip'
namespace.
The other changed line of code is in the privatePing
listener. Here, we use the of
method again. Anytime we use io.of('/namespace')
, all the methods we use after it will be in the context of that namespace. If we used it the other way (io.sockets.socket()
), then the response would have been sent back in the default namespace instead of the '/vip'
namespace. Go ahead, switch them and see what happens.
We have our server side built; now let's build our client side.
Building our namespace client
We know what events the server is listening for, so now we have to create a client that will send them. Open up your namespace.html
and add the following code:
<!DOCTYPE html> <html> <head> <title>Ping Pong</title> <script type="text/javascript" src="socket.io.js"></script> <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.0/jquery.js"></script> <style> .areas { float: left; width: 50%;} </style> </head> <body> <div> <input type="text" id="username"> </div> <div class="areas default"> Default <button class="join">Join</button> <button class="ping">Ping</button> <div> Users <ul class="users"> </ul> </div> <div> Events <ul class="events"> </ul> </div> </div> <div class="areas vip"> VIP <button class="join">Join</button> <button class="ping">Ping</button> <div> Users <ul class="users"> </ul> </div> <div> Events <ul class="events"> </ul> </div> </div> <script> </script> </body> </html>
This is a simple HTML structure and div.areas
are identical. They both have two buttons, Join
and Ping
, and two lists, users
and events
. This is our skeleton we will use to wire up all our JavaScript muscles. We are using the socket.io
client (you did remember to link to it from node_modules
, right?) and jQuery 2.1 from a content delivery network (we will talk about these in Chapter 8, Javascript Best Practices for Application Development). All the JavaScript code will go into our script tag at the end of the body (is HTML just one big biology metaphor?).
The first thing to do is to connect to our server. We will create two variables to hold our connections:
var socket = io.connect('http://localhost:4000'), vip = io.connect('http://localhost:4000/vip');
The first connection we have is done already. The second is how we use namespaces. To connect to a namespace, just connect to the server and append the namespace you created. This object is now in the context of a namespace. Any methods we call or any event listeners we attach will only be for events from the '/vip'
namespace. Let's finish up this app and then test it.
Note
We will create two connections, but Socket.IO doesn't actually connect twice. It is able to just use one connection for both, including any and all namespace connections.
The next thing we will do is grab our key elements from the page, as follows:
var defaultArea = $('.default'), vipArea = $('.vip'), $username = $('#username');
We will create jQuery objects that are tied to div.default
, div.vip
, and input#username
. If you are not familiar with jQuery, it gives us some cross-browser methods and easy selectors. We will cover more jQuery when we build out the frontend.
We now will create a simple utility function, as shown in the following code:
function createButton(user){ return '<li>' + user.username + '<button class="private_ping" data-socket="' + user.socket + '">Ping Me</button></li>'; };
We pass in a user object (which we will get from our socket events), and it will return an HTML string for a button with a private_ping
class.
Finally, we will create the function that will wire everything up:
function wireEvents(area, socketio){ var users = area.find('.users'), events = area.find('.events'); area.on('click', function(e){ if (e.target.className === 'join') { socketio.emit('join', {username: $username.val()}); }else if (e.target.className === 'ping') { socketio.emit('ping'); }else if (e.target.className === 'private_ping') { socketio.emit('privatePing', {socket: e.target.getAttribute('data-socket')}); } }); socketio.on('join', function(user){ users.append(createButton(user)); }); socketio.on('ping', function(user){ if (user.priv === undefined){ events.append('<li>Ping from ' + user.username + '</li>'); }else{ events.append('<li>Ping from ' + user.username + ' sent directly to you!</li>'); } }); };
The wireEvents
function takes an area and a Socket.IO connection and will attach all the listeners for us.
The first thing we do is find the users list and events list. We will do this with the find
function of jQuery. It will look in the object we pass in (which will be either div.default
or div.vip
) and find all the elements that have users or events in their class list. This gives us the reference to each list.
Next, we add a click listener on the entire area. This is better than adding multiple click handlers for each element that is clicked. We then check the event that is passed in for what element was clicked and what class the element has. With this info, we know what button was clicked, as each button has a specific class attached to it. From there, we pass the event to the server. The join
and ping
events are very straightforward. join
uses the username as input and ping
just sends the event. privatePing
uses an attribute attached to the button. This attribute is added from the createButton
function that uses the socket ID from the join
event sent from the server. This function will create a button element, as shown in the following screenshot:

This is where the socket ID is stored when the server broadcasts the join
event. We send it back so the server can find that specific socket and send it to only that client.
Next, we add a listener for the join
events from the server. We just add a list item with the button we just discussed.
Finally, we listen for ping events. If the event does not have the priv
attribute set, then we know it was broadcast to everyone. If it is set, then we know it was sent to only us. We then append either of these to our events list.
We now have all our events covered. We are listening for clicks, joins, pings, and private pings.
We built this function to be reusable, so we can easily do this for both the default
and vip
areas, as shown in the following code:
wireEvents(defaultArea, socket); wireEvents(vipArea, vip);
At this point, we can launch Node and our Python HTTP server. Load http://localhost:8000/namespace.html
in a couple of tabs. Join a couple of areas, ping a few times, and send some private pings. We can see that everything works how we expect it to. All the events will be tied to a specific room. The following screenshot will show the output:

This is not a full-featured app. It is just an example on how to use namespaces in Socket.IO. Let's now modify our little ping app to use rooms instead of namespaces, so we can see what the differences are.