Wednesday, March 25, 2015

How Do I Simultaneously Navigate Multiple Web Browsers?

With some JavaScript and Socket.IO!

However, before getting into that, here's a little background on why I needed to do this (feel free to scroll down if you want to jump to the code). Working as a developer, the majority of your code will be aimed at being deployed in production. By production, I mean an environment where it will be used to provide value to the organization employing you, be this a customer facing web application or a test harness that's used as part of the build process. When you are developing for a production environment, it makes sense to stay within the confines of the current tech stack that is being employed, unless it really makes sense to introduce a new element. For example, if your organization develops their web applications using only ASP.NET, it wouldn't make sense for one developer to start developing a couple new pages in PHP. Sticking to a tech stack improves maintainability tremendously, but it can also limit our ability to see beyond that technology stack. This is why I savor the opportunities to code something that won't go to production and won't need to be maintained by anyone. This gives me the freedom to choose whatever technologies I feel I can be most productive using to solve the problem at hand. It's great not to be confined to your production stack and have the opportunity to step outside of the box and try something new.

One of these opportunities presented itself when I ran into an issue where it appeared either sessions were getting mixed up or the wrong value was being used for the Set-Cookie HTTP header entry. From examining the webserver's logs and the database's audit log, it was clear that the affected parties' requests reached the webserver at exactly the same instance. This meant, that in order to attempt to reproduce this issue, I needed a way to trigger multiple browsers to navigate to the same URL at exactly the same time in order to increase the likelihood of causing an occurrence as this issue did not raise itself every time two or more requests were received simultaneously. This was the moment I had been waiting for, the moment to try something new!

I decided to use node.js as I felt that I would be able to get something up and running with minimal friction. The first solution I considered was where the browsers would repeatedly poll the server and ask if they should navigate or not. When the server said yes, each of the browsers would then navigate to the website I wanted to test. In my mind's eye, the implementation of this would be very easy for both the client and server, but something didn’t feel right about it. I ended up dismissing this approach as the accuracy of the synchronized navigation would rely solely on the shortness of the poll duration. I felt that the command to navigate should be sent from the server to all the browsers in the form of a broadcast. This led me to web sockets and ultimately Socket.IO.

The solution that I created has an index page and an admin page. When browsers load the index page, a connection is established with the web server and a message is broadcasted to the other connected browsers that one more has connected. The admin page allows messages to be sent to the connected clients and to command them to navigate to a url.

Server side

app.js:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
(function () {
 'use strict';
 
 var http = require("http");
 var express = require("express");
 var app = express();
 var router = express.Router();
 
 app.use(express.static(__dirname + '/public'));
 
 router.use(function (req, res) {

  res.status(404).send("Did you get lost?");
 });

 router.use(function (err, req, res, next) {

  console.log("ERROR:");
  console.dir(err);
  
  if (req.xhr) {
   res.set('Content-Type','application/json');
   res.status(500).send({ message: "Oops, something went wrong." });
  } else {
   res.status(500).send("Oops, something went wrong.");
  }
 });

 app.use('/', router);

 var port = process.env.PORT || 3000;
 var server = http.createServer(app);
 server.listen(port);
 require('./sockets.js')(server);
 console.log("Listening on port %d", port);
 
}());

The majority of this is plumping for Express.js. The most significant part is line 34 as this is where the call to configure Socket.IO is being made. I've taken a liking to my modules returning a function when they need to perform some initialization upon import. Once you invoke the initialization function, this can then return the API for the module. This construct mirrors the behavior of a constructor by helping to ensure that the module is in a valid state before you start to use it. This can be seen at line 34 where I am invoking the imported sockets.js module and passing in the server variable. In this case, there is no API returned as once I've setup Socket.IO, I do not need to interact with it again.

sockets.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
(function () {
 'use strict';
 var socketio = require("socket.io");
 
 module.exports = function (server) {
  
  var io = socketio.listen(server);
  
  io.on('connection', function (socket) {
   
   socket.on('join', function (data) {
    
    var address = socket.handshake.address;
    console.log("Joined: " + address);
    
    socket.emit('joined', { message: "You are connected!" });
   });
   
   socket.on('send message', function (data) {
    
    console.log("Message: " + data.message);
    
    io.emit('client message', { message: data.message });
   });
   
   socket.on('navigate', function (data) {
    console.log("Navigate: " + data.url);
    
    io.emit('navigate browser', { url: data.url });
   });
  });
 };
        
}());

Socket.IO revolves around emitting events and responding to them. Line 9 is an example of how you would setup a callback to respond to a "connection" event that is received by the server though the use of the on function. Responding to the "connection" event is the standard pattern for setting up the behavior for when a new connection is established. In lines 11 through 30, I am creating three listeners that will react to the connected socket emitting "join", "send message" and "navigate" events. The "send message" and "navigate" listeners both emit events of their own. As there are broadcasted on the io channel, they will be sent to all the other connected sockets. However, the "join" listener is only emitting the "joined" event on the connected socket. When you emit an event, you have the option to send data with it and this can be seen as the second parameter of the emit function.

Client side

index.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(function () {
 'use strict';
 
 var messages = document.getElementById('messages');
 var addMessage = function (message) {

  messages.innerHTML = messages.innerHTML + message + '<br />';
 };
 
 var socket = io.connect(location.host);
 
 socket.on('navigate browser', function (data) {
  
  location.href = data.url;
 });
 
 socket.on('client message', function (data) {
  
  addMessage(data.message);
 });
 
 socket.on('joined', function (data) {
  
  addMessage(data.message);
 });
 
 socket.emit('join');

}());

admin.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(function () {
 'use strict';

 var socket = io.connect(location.host);
 
 var message = document.getElementById('message');
 document.getElementById('message-button').addEventListener('click', function () {

  socket.emit('send message', { message: message.value });
 });
 
 document.getElementById('navigate-button').addEventListener('click', function () {
  
  var url = document.getElementById('url').value;
  socket.emit('navigate', { url: url });
 });

}());

With Socket.IO, the client side of the wire follows a very similar pattern as on the server side. After connecting to the server you are returned a socket upon which you can emit events or listen for events. Like on the server, these are done using functions called on and emit. For an example of listening for an event, look at line 13 in index.js and for an example of emitting a client side event, look at line 9 in admin.js. The entire solution can be found on GitHub.

I realize that I could have stuck with the .NET stack and used ASP.NET SignalR to achieve the same results, but I feel that trying something new in a different stack can bring new insights that can help to make you a better developer. Each stack has its good points and its bad, however, if you only stick with the same stack, you will only ever experience the same pluses and the same minuses. By trying different programming languages, web-frameworks, databases, and so on, you can expand your horizons to new and different ways of doing something. You may even find a new solution for a pain-point that's been nagging you since you can’t remember when. I plan to step outside of the box whenever the opportunity presents itself and try something new. And maybe you should too!

No comments:

Post a Comment