Socket.IO works by wrapping itself around an HTTP Server object. Think back to Chapter 4, HTTP Servers and Clients, where we wrote a module that hooked into HTTP Server methods so that we could spy on HTTP transactions. The HTTP Sniffer attaches a listener to every HTTP event to print out the events. But what if you used that idea to do real work? Socket.IO uses a similar concept, listening to HTTP requests and responding to specific ones by using the Socket.IO protocol to communicate with client code in the browser.
To get started, let's first make a duplicate of the code from the previous chapter. If you created a directory named chap08 for that code, create a new directory named chap09 and copy the source tree there.
We won't make changes to the user authentication microservice, but we will use it for user authentication, of course.
In the Notes source directory, install these new modules:
$ npm install socket.io@2.x passport.socketio@3.7.x --save
We will incorporate user authentication with the passport module, used in Chapter 8, Multiuser Authentication the Microservice Way, into some of the real-time interactions we'll implement.
To initialize Socket.IO, we must do some major surgery on how the Notes application is started. So far, we used the bin/www.mjs script along with app.mjs, with each script hosting different steps of launching Notes. Socket.IO initialization requires that these steps occur in a different order to what we've been doing. Therefore, we must merge these two scripts into one. What we'll do is copy the content of the bin/www.mjs script into appropriate sections of app.mjs, and from there, we'll use app.mjs to launch Notes.
At the beginning of app.mjs, add this to the import statements:
import http from 'http';
import passportSocketIo from 'passport.socketio';
import session from 'express-session';
import sessionFileStore from 'session-file-store';
const FileStore = sessionFileStore(session);
export const sessionCookieName = 'notescookie.sid';
const sessionSecret = 'keyboard mouse';
const sessionStore = new FileStore({ path: "sessions" });
The passport.socketio module integrates Socket.IO with PassportJS-based user authentication. We'll configure this support shortly. The configuration for session management is now shared between Socket.IO, Express, and Passport. These lines centralize that configuration to one place in app.mjs, so we can change it once to affect every place it's needed.
Use this to initialize the HTTP Server object:
const app = express();
export default app;
const server = http.createServer(app);
import socketio from 'socket.io';
const io = socketio(server);
io.use(passportSocketIo.authorize({
cookieParser: cookieParser,
key: sessionCookieName,
secret: sessionSecret,
store: sessionStore
}));
This moves the export default app line from the bottom of the file to this location. Doesn't this location make more sense?
The io object is our entry point into the Socket.IO API. We need to pass this object to any code that needs to use that API. It won't be enough to simply require the socket.io module in other modules because the io object is what wraps the server object. Instead, we'll be passing the io object into whatever modules are to use it.
The io.use function installs in Socket.IO functions similar to Express middleware. In this case, we integrate Passport authentication into Socket.IO:
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
This code is copied from bin/www.mjs, and sets up the port to listen to. It relies on three functions that will also be copied into app.mjs from bin/www.mjs:
app.use(session({
store: sessionStore,
secret: sessionSecret,
resave: true,
saveUninitialized: true,
name: sessionCookieName
}));
initPassport(app);
This changes the configuration of Express session support to match the configuration variables we set up earlier. It's the same variables used when setting up the Socket.IO session integration, meaning they're both on the same page.
Use this to initialize Socket.IO code in the router modules:
app.use('/', index);
app.use('/users', users);
app.use('/notes', notes);
indexSocketio(io);
// notesSocketio(io);
This is where we pass the io object into modules that must use it. This is so that the Notes application can send messages to the web browsers about changes in Notes. What that means will be clearer in a second. What's required is analogous to the Express router functions, and therefore the code to send/receive messages from Socket.IO clients will also be located in the router modules.
We haven't written either of these functions yet (have patience). To support this, we need to make a change in an import statement at the top:
import { socketio as indexSocketio, router as index } from './routes/index';
Each router module will export a function named socketio, which we'll have to rename as shown here. This function is what will receive the io object, and handle any Socket.IO-based communications. We haven't written these functions yet.
Then, at the end of app.mjs, we'll copy in the remaining code from bin/www.mjs so the HTTP Server starts listening on our selected port:
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) { // named pipe
return val;
}
if (port >= 0) { // port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') { throw error; }
var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' +
port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
debug('Listening on ' + bind);
}
Then, in package.json, we must start app.mjs rather than bin/www.mjs:
"scripts": {
"start": "DEBUG=notes:* SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml NOTES_MODEL=sequelize USER_SERVICE_URL=http://localhost:3333 node --experimental-modules ./app",
"start-server1": "DEBUG=notes:* SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml NOTES_MODEL=sequelize USER_SERVICE_URL=http://localhost:3333 PORT=3000 node --experimental-modules ./app",
"start-server2": "DEBUG=notes:* SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml NOTES_MODEL=sequelize USER_SERVICE_URL=http://localhost:3333 PORT=3002 node --experimental-modules ./app",
...
},
At this point, you can delete bin/www.mjs if you like. You can also try starting the server, but it'll fail because the indexSocketio function does not exist yet.