Socket.IO is a simple little library thatâs a lot like Nodeâs core net
library. Socket.IO allows you to send
messages back and forth with browser clients that connect with your Node
server, using an efficient, low-level socket mechanism. One of the nice
things about the module is that it provides a shared interface between the
browser and the server. That is, you can write the same JavaScript on both
in order to do messaging work once you have a connection
established.
Socket.IO is so named because it supports the HTML5 WebSockets standard on browsers that support it (and have it enabled). Fortunately, the library also supports a number of fallbacks:
WebSocket
WebSocket over Flash
XHR Polling
XHR Multipart Streaming
Forever Iframe
JSONP Polling
These options ensure that youâll be able to have some kind of persistent connection to the browser in almost any environment. The Socket.IO module includes the code to power these connection paths on both the browser and the server side with the same API.
Instantiating Socket.IO is as simple as including the module and creating a server. One of the things thatâs a little different about Socket.IO is that it requires an HTTP server as well; see Example 7-16.
Example 7-16. Creating a Socket.IO server
var http = require('http'), io = require('socket.io'); server = http.createServer(); server.on('request', function(req, res){ //Typical HTTP server stuff res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World'); }); server.listen(80); var socket = io.listen(server); socket.on('connection', function(client){ console.log('Client connected'); });
The HTTP server in this example could do anything. In this case, we simply return âHello World.â However, Socket.IO doesnât care what the HTTP server will do; it simply wraps its own event listener around all requests to your server. This listener will look for any requests for Socket.IOâs client libraries and service these requests. It passes on all others to be handled by the HTTP server, which will function as usual.
The example creates a socket.io
server by calling io.listen()
,
which is a factory method for the Listener
class. listen()
takes a function as an
argument, which it uses as a callback when a client connects to the
server. Because the sockets are persistent connections, you arenât dealing
with a req
and res
object as you do with an HTTP server. As
with net
, you need to use the passed
client
object to communicate with each
browser. Of course, itâs also important to have some code in the browser
(Example 7-17) to interact with the
server.
Example 7-17. A small web page to interact with a Socket.IO server
<!DOCTYPE html> <html> <body> <script src="/socket.io/socket.io.js"></script> <script> var socket = io.connect('http://localhost:8080'); socket.on('message', function(data){ console.log(data) }) </script> </body> </html>
This simple page starts by loading the necessary Socket.IO client library directly from the Node server, which is localhost on port 8080 in this case.
Note
Although port 80 is the standard HTTP port, port 8080 is more convenient during development because many developers run web servers locally for testing that would interfere with Nodeâs work. In addition, many Linux systems have built-in security policies preventing nonadministrator users from using port 80, so it is more convenient to use a higher number.
Next, we create a new Socket
object with the hostname of the Socket.IO server we are connecting to. We
ask the Socket
to connect with socket.connect()
. Then we add a listener for the message
event. Notice how the API is like a Node
API. Whenever the server sends this client a message, the client will
output it to the browserâs console window.
Now letâs modify our server to send this page to clients so we can test it (Example 7-18).
Example 7-18. A simple Socket.IO server
var http = require('http'), io = require('socket.io'), fs = require('fs'); var sockFile = fs.readFileSync('socket.html'); server = http.createServer(); server.on('request', function(req, res){ res.writeHead(200, {'content-type': 'text/html'}); res.end(sockFile); }); server.listen(8080); var socket = io.listen(server); socket.on('connection', function(client){ console.log('Client connected'); client.send('Welcome client ' + client.sessionId); });
The most striking change in this example is the addition of the
fs.readFileSync
function, which brings
the web pageâs external file into the socket server. Now instead of
responding to web browser requests with âHello World,â the Node server
will respond with the contents of socket.html.
Because readFileSync
is a
synchronous function, it will block Nodeâs event loop until the file is
read, ensuring that the file is ready to be delivered to clients
immediately when the server becomes available for connections.
Now whenever anyone requests anything from the server, unless it is
a request to the Socket.IO client library, he will get a copy of
socket.html (which might be the code in Example 7-17). The callback for connections has been
extended to send a welcome message to clients, and a client running the
code from Example 7-18 might get a message in its
console like Welcome client
17844937089830637
. Each client gets its own sessionId
. Currently, the ID is an integer
generated using Math.random()
.
Creating websockets as shown is fine when you are in full control of your application and architecture, but this will quickly lead to conflicts when you are attaching them to an existing application that uses sockets or when you are writing a service to be plugged into someone elseâs project. Example 7-19 demonstrates how namespaces avoid this problem by effectively dividing Socket.IOâs listeners into channels.
Example 7-19. A modified web page to interact with Socket.IO namespaces
<!DOCTYPE html> <html> <body> <script src="/socket.io/socket.io.js"></script> <script> var upandrunning = io.connect('http://localhost:8080/upandrunning'); var weather = io.connect('http://localhost:8080/weather'); upandrunning.on('message', function(data){ document.write('<br /><br />Node: Up and Running Update<br />'); document.write(data); }); weather.on('message', function(data){ document.write('<br /><br />Weather Update<br />'); document.write(data); }); </script> </body> </html>
This updated socket.html makes two Socket.IO
connections, one to
http://localhost:8080/upandrunning and the other to
http://localhost:8080/weather. Each connection has
its own variable and its own .on()
event listener. Apart from these differences, working with Socket.IO
remains the same. Instead of logging to the console, Example 7-20 displays its message results within the web
browser window.
Example 7-20. A namespace-enabled Socket.IO server
var sockFile = fs.readFileSync('socket.html'); server = http.createServer(); server.on('request', function(req, res){ res.writeHead(200, {'content-type': 'text/html'}); res.end(sockFile); }); server.listen(8080); var socket = io.listen(server); socket.of('/upandrunning') .on('connection', function(client){ console.log('Client connected to Up and Running namespace.'); client.send("Welcome to 'Up and Running'"); }); socket.of('/weather') .on('connection', function(client){ console.log('Client connected to Weather namespace.'); client.send("Welcome to 'Weather Updates'"); });
The function socket.of
splits
the socket object into multiple unique namespaces, each with its own
rules for handling connections. If a client were to connect to
http://localhost:8080/weather and issue an emit()
command, its results would be processed
only within that namespace, and not within the
/upandrunning namespace.
There are many cases where you would want to use Socket.IO by itself within Node as its own application or as a component of a larger website architecture that includes non-Node components. However, when itâs used as part of a full Node application using Express, you can gain an enormous amount of efficiency by writing the entire software stackâincluding the client-facing viewsâin the same language (JavaScript).
Save Example 7-21 as socket_express.html.
Example 7-21. Attaching Socket.IO to an Express application: client code
<script src="/socket.io/socket.io.js"></script> <script> var socket = io.connect('http://localhost:8080'); socket.on('news', function(data) { document.write('<h1>' + data.title + '</h1>' ); document.write('<p>' + data.contents + '</p>' ); if ( data.allowResponse ) { socket.emit('scoop', { contents: 'News data received by client.' }); } }); </script>
This example starts by connecting to the Socket.IO on port 8080. Whenever the Socket.IO server sends a ânewsâ event, the client writes the new itemâs title and contents to the browser page. If the news item allows a response, the client socket emits a âscoopâ event. The scoop wouldnât be very interesting to a real reporter; it only contains an acknowledgment that the client received the original news.
This being an example press, the news server responds to the
âscoopâ event by emitting another news story. The client will receive
this new story and print it to the screen also. To prevent this cycle
from continuing out of control, an allowResponse
parameter is sent with the news
story. If it is false or not present at all (see Example 7-22), the client will not send a scoop.
Example 7-22 shows the Express server.
Example 7-22. Attaching Socket.IO to an Express application: server code
var app = require('express').createServer(), io = require('socket.io').listen(app); app.listen(8080); app.get('/', function(req,res) { res.sendfile(__dirname + '/socket_express.html'); }); io.sockets.on('connection', function(socket) { socket.emit('news', { title: 'Welcome to World News', contents: 'This news flash was sent from Node.js!', allowResponse: true }); socket.on('scoop', function(data) { socket.emit('news', { title: 'Circular Emissions Worked', contents: 'Received this content: ' + data.contents }); }); });
The Express server is created first and then passed into Socket.IO
as a parameter. When the Express application is started with the
listen()
function, both the web
server and socket server are activated. Next, a route for the base path
(/
) is defined as a pass-through for sending the
client-side file created in Example 7-21.
The server-side code for the news broadcaster looks very similar to the client-side code for good reason. The same events (emit, on message, connection) behave similarly in Node and in the web browser, making connectivity straightforward. Because data is passed as JavaScript objects between both endpoints, no additional parsing or serialization is needed.
Clearly, we can very quickly gain a lot of power by plugging Socket.IO into Express, but astute programmers will realize that this is one-way communication of limited value, unless the connection initiated by the userâs web browser is represented in the socket stream. Any changes (logout, profile settings, etc.) should be reflected in any socket actions, and vice versa. How to accomplish that? Sessions.
To illustrate the use of a session for authentication, letâs look first at the client-side code, views/socket.html, shown in Example 7-23.
Example 7-23. Client HTML (Jade template): Socket.IO sessions
!!! 5 html(lang='en') head script(type='text/javascript', src='/socket.io/socket.io.js') script(type='text/javascript') var socket = io.connect('http://localhost:8080'); socket.on('emailchanged', function(data) { document.getElementById('email').value = data.email; }); var submitEmail = function(form) { socket.emit('emailupdate', {email: form.email.value}); return false; }; body h1 Welcome! form(onsubmit='return submitEmail(this);') input(id='email', name='email', type='text', value=locals.email) input(type='submit', value='Change Email')
When rendered in a web browser, this page will display a form text
box with a âChange Emailâ call to action whose default value comes from
Expressâs session data through the locals.email
variable. Upon user input, the
application performs these actions:
Next, have a look at the Node.js portion of Example 7-24.
Example 7-24. Sharing session data between Express and Socket.IO
var io = require('socket.io'); var express = require('express'); var app = express.createServer(); var store = new express.session.MemoryStore; var utils = require('connect').utils; var Session = require('connect').middleware.session.Session; app.configure(function() { app.use(express.cookieParser()); app.use(express.session({secret: 'secretKey', key: 'express.sid', store: store})); app.use(function(req, res) { var sess = req.session; res.render('socket.jade', { email: sess.email || '' }); }); }); // Start the app app.listen(8080); var sio = io.listen(app); sio.configure(function() { sio.set('authorization', function (data, accept ) { var cookies = utils.parseCookie(data.headers.cookie); data.sessionID = cookies['express.sid']; data.sessionStore = store; store.get(data.sessionID, function(err, session) { if ( err || !session ) { return accept("Invalid session", false); } data.session = new Session(data, session); accept(null,true); }); }); sio.sockets.on('connection', function(socket) { var session = socket.handshake.session; socket.join(socket.handshake.sessionId); socket.on('emailupdate', function(data) { session.email = data.email; session.save(); sio.sockets.in(socket.handshake.sessionId).emit('emailchanged', { email: data.email }); }); }); });
This example uses Connect, a middleware framework that simplifies
common tasks such as session management, working with cookies,
authentication, caching, performance metrics, and more. In this example,
the cookie and session tools are used to manipulate user data. Socket.IO
is not aware of Express and vice versa, so Socket.IO is not aware of
sessions when the user connects. However, both components need to
use the Session
object to share data.
This is an excellent demonstration of the Separation of Concerns (SoC) programming
paradigm.[19]
This example demonstrates using Socket.IOâs authorization, even after connection, to parse the userâs headers. Because the session ID is passed to the server as a cookie, you can use this value to read Expressâs session ID.
This time, the Express setup includes a line for session management. The arguments used to build the session manager are a secret key (used to prevent session tampering), the session key (used to store the session ID in the web browserâs cookie), and a store object (used to store session data for later retrieval). The store object is the most important. Instead of letting Express create and manage the memory store, this example creates a variable and passes it into Express. Now the session store is available to the entire application, not just Express.
Next, a route is created for the default (/
)
web page. In previous Socket.IO examples, this function was used to
output HTML directly to the web browser. This time, Express will render
the contents of views/socket.jade
to the web browser. The second variable in render()
is the email address stored in the
session, which is interpreted and used as the default text field value
in Example 7-23.
The real action happens in Socket.IOâs 'authorization'
event. When the web browser
connects to the server, Socket.IO performs an authentication routine to
determine whether the connection should proceed. The criteria in this
case is a valid session, which was provided by Express when the user
loaded the web page. Socket.IO reads the session ID from the request
headers using parseCookie
(part of
the Connect framework), loads the session from the memory store, and
creates a Session
object with the
information it receives.
The data passed to the authorization event is stored in
the socketâs handshake
property. Therefore, saving the session object into the data object
makes it available later in the socketâs lifecycle. When creating the
Session
object, use the memory store
that was created and passed into Express; now both Express and Socket.IO
are able to access the same session dataâExpress by manipulating the
req.session
object, and sockets by
manipulating the socket.handshake.session
object.
Assuming all goes well, calling accept()
authenticates the socket and allows the connection to continue.
Now suppose the user accesses your site from more than one tab in
his web browser. There would be two connections from the same session
created, so how would you handle events that need to update connected
sockets? Socket.IO provides support for rooms, or
channels if you prefer. By initiating a join()
command with sessionId
as the argument in Example 7-24, the socket transparently created a dedicated
channel you can use to send messages to all connections currently in use
by that user. Logging out is an obvious use for this technique: when the
user logs out from one tab, the logout command will instantly transmit
to all the others, leaving all of the userâs views of the application in
a consistent state.
Warning
Always remember to execute session.save()
after changing session data.
Otherwise, the changes will not be reflected on subsequent requests.
[19] SoC refers to the practice of breaking down software into
smaller single-purpose parts (concerns) that have as little
overlapping functionality as possible. Middleware enables this style
of design by allowing totally separate modules to interact in a
common environment without needing to be aware of each other.
Although, as we have seen with modules such as
bodyParser()
, it remains up to the programmer to
understand how the concerns ultimately interact and use them in the
appropriate order and context.
Get Node: Up and Running now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.