July 27, 2018

Mixed Signals with Socket.IO and WebRTC

by Jscrambler

mixed-signals-webrtc-socketio

History of WebRTC

If you’ve played any web game or have used chat online, you’ve most likely used an application utilizing WebRTC.

WebRTC is a Real-Time Communications protocol with origins going as far back as 2011. Google, Mozilla, Opera and even Ericsson among others have contributed to shape WebRTC into what it is now: an open standard for plugin-free duplex video, audio and data communication.

In other words, WebRTC is the reason you can download a random chat extension for Chrome or Firefox and begin a full HD video conference call in seconds with no additional plugins or software. Better yet, this protocol allows for other types of metadata and peer-to-peer communication to occur.

Previous implementations of WebRTC, including Skype, Facebook (built on Skype), and Google Hangouts (Google Talk plugin) required the presence of proprietary plugins or native apps. For obvious reasons, these implementations fell out of fashion in favor of API’s and protocols aligning with the open-source, free, and standardized guiding principles of WebRTC. These API’s are now being built into modern browsers and run more efficiently than existing proprietary technologies.

WebRTC’s Mixed Signals

The WebRTC API still needs servers to be able to communicate between peers, since it coordinates and exchanges metadata between clients. This is “Signaling”.

While WebRTC provides the architecture and API for communication, Signaling is the “operator” that coordinates and establishes a session with the appropriate information. Session information related to media types, codecs, network settings and security are all examples of metadata exchanged with signaling.

The WebRTC signaling process is based on JSEP: JavaScript Session Establishment Protocol. JSEP is a collection of interfaces for signaling identification - more specifically, to identify negotiation of local and remote addresses.

Signaling is used to detect peers and exchange prerequisites to set up media connections which are at the discretion of the application. This process can be done using a gateway like WebSockets, XMPP (i.e. AIM/Pidgin), ICE (interactive connectivity establishment) or - more commonly - session initiation protocol (SIP).

In previous generations of asynchronous communication, JavaScript’s API was limited to either AJAX or XHR - in essence, not at all asynchronous. As JavaScript has matured in functionality, so has WebRTC with WebSockets.

WebSockets provides a standardized way for the server to send content to the client without it being first requested by the client and allows messages to be passed back and forth while keeping the connection open. Where HTTP/XHR is limited by domain, the WebSockets API provides cross-domain messaging, as it was created expressly for that purpose.

Chat Room App with Socket.IO and WebRTC

Socket.IO

Socket.IO is a robust library for real-time communication created by Guillermo Rauch, CTO of LearnBoost.

Socket.IO also provides an API for Node.js which parallels the client-side API. Socket.IO simplifies signaling with WebSockets usage but, more importantly, it provides fallbacks to other protocols (AJAX/JSONP polling, iframe, Flash, etc.) in the event that WebSockets are not supported on the browser or server, granting it a wide range of browser compatibility.

Another benefit worth noting is that Socket.IO adds room name spacing, connection, and logging details, along with plenty of integration with libraries like Angular, Vue, React, among others. For a neat example of WebSockets (WebRTC) in action, try out this fun little distraction built by the team at Mozilla.

NOTE: Socket.IO is not a WebSocket implementation. Although Socket.IO uses WebSocket as a transport mechanism when possible, it adds some metadata to each packet: the packet type, the namespace, and the acknowledgment id when a message acknowledgment is needed. That is why a pure WebSocket client will not be able to successfully connect to a Socket.IO server, and vice-versa.

Socket.IO Chat Tutorial

Luckily, Socket.IO is so simple to get up and running that you’ll be creating your own chat room app by the end of this article.

Our main objectives will be to build a chat application that should have the following functionality:

  • a group chat room using express and Socket.IO
  • let the user correspond with other users (sending and receiving messages)
  • broadcast messages to all other connected users in the room
  • display the status of users (user is typing...)

We start off by creating scaffolding for our app, which will include the HTML/CSS, our server, and our client files.

index.html  
styles.css  
index.js  
chat.js  

The basic HTML boilerplate should be added first, as it’s the easiest. Be sure to include a head and body to house our chat app. To save time, you can simply copy and paste from below. Don’t forget to add the vital styles for our application, by copying over the provided CSS as well. Copy this HTML and CSS.

Once you’ve copied the HTML and CSS, set up the foundation of your project with npm install express. Then comes the star of the show: Socket.IO. Install it with npm install socket.io or download the library from Socket.IO directly and load from within our HTML. You can also use the CDN hosted version.

After we’ve installed express, Socket.IO, and we've created our files, we can begin to develop our chat app.

Once we’ve saved Socket.IO as a dependency, we can begin our application development with the server-side JavaScript by creating our variables - specifically, one requiring our express dependency and those variables necessary for our server to run, including the ability to serve our static application files to the client on port 4000. Be sure to house all static files (chat.js, HTML/CSS) within a folder called ‘public’ as seen below in the corresponding app.use(express.static('public')); line of code

Index.js file:

var express = require('express');

// Express app setup
var app = express();  
//listens for any requests to our port 
var server = app.listen(4000, function(){  
    console.log('listening for requests on port 4000,');
});

// Static files
app.use(express.static('public'));  

Once we’ve done completed these initial steps, we can set up our client-side JavaScript. Do so by connecting to port 4000, which we’ve declared for our app in our corresponding server file with our Socket.IO method io.connect. Next up, we tie our message, handle, btn, output, and feedback variables to their respective id's in the DOM.

Chat.js client:

// Make a socket.io connection
var socket = io.connect('http://localhost:4000');

// Assign variables to query the DOM
var message = document.getElementById('message'),  
      handle = document.getElementById('handle'),
      btn = document.getElementById('send'),
      output = document.getElementById('output'),
      feedback = document.getElementById('feedback');
...

The socket.emit method sends the handle and message without acknowledgment. For more info on other socket methods take a look at "emit cheatsheet" in the Socket.IO documentation.

The event emitters on "click" will send the chat message text and display it according to the handle that sent it, while “keypress” is used for displaying "user is typing..." in our feedback DIV. Socket.on fires a callback function with the data object and sends it all of the connected sockets viewing the chat, outputting the message and any subsequent messages to the DOM (output.innerHTML).

// Emit events
btn.addEventListener('click', function(){  
    socket.emit('chat', {
        message: message.value,
        handle: handle.value
    });
    message.value = "";
});

message.addEventListener('keypress', function(){  
    socket.emit('typing', handle.value);
})

// Listen for events
socket.on('chat', function(data){  
    feedback.innerHTML = '';
    output.innerHTML += '<p><strong>' + data.handle + ': </strong>' + data.message + '</p>';
});
socket.on('typing', function(data){  
    feedback.innerHTML = '<p><em>' + data + ' is typing a message...</em></p>';
});

In our Index.js, the following snippet is coded to listen for an open connection sent from chat.js and any message events, then sends a message back to the server, which is, in turn, to broadcast to everyone else connected in the chat room.

After we establish the connection, the event listener will fire when it connects a socket and confirms a connection in the console, along with the socket id.

// Socket connection setup & pass chat data
var io = socket(server);  
io.on('connection', (socket) => {

    console.log('made socket connection', socket.id);

    // Handle chat message event
    socket.on('chat', function(data){
        // console.log(data);
        io.sockets.emit('chat', data);
    });

    // Handle "user is typing" event
    socket.on('typing', function(data){
        socket.broadcast.emit('typing', data);
    });

});

Our completed server side index.js file should look like:

var express = require('express');  
var socket = require('socket.io');

// App setup
var app = express();  
var server = app.listen(4000, function(){  
    console.log('listening for requests on port 4000,');
});

// Static files
app.use(express.static('public'));

// Socket setup & pass server
var io = socket(server);  
io.on('connection', (socket) => {

    console.log('made socket connection', socket.id);

    // Handle chat event
    socket.on('chat', function(data){
        // console.log(data);
        io.sockets.emit('chat', data);
    });

    // Handle typing event
    socket.on('typing', function(data){
        socket.broadcast.emit('typing', data);
    });

});

The completed chat.js file should look like the following:

// Make connection
var socket = io.connect('http://localhost:4000');

// Query DOM
var message = document.getElementById('message'),  
      handle = document.getElementById('handle'),
      btn = document.getElementById('send'),
      output = document.getElementById('output'),
      feedback = document.getElementById('feedback');

// Emit events
btn.addEventListener('click', function(){  
    socket.emit('chat', {
        message: message.value,
        handle: handle.value
    });
    message.value = "";
});

message.addEventListener('keypress', function(){  
    socket.emit('typing', handle.value);
})

// Listen for events
socket.on('chat', function(data){  
    feedback.innerHTML = '';
    output.innerHTML += '<p><strong>' + data.handle + ': </strong>' + data.message + '</p>';
});

socket.on('typing', function(data){  
    feedback.innerHTML = '<p><em>' + data + ' is typing a message...</em></p>';
});

Our entire completed chat room application should look something like this:

Chat Room App with Socket.IO and WebRTC

Thanks to its abstraction from WebRTC, WebSockets (and many complex features, like named room creation and intelligent reconnection) alleviate countless headaches, making it the perfect choice for any real-time app.

Any enterprise engineering efforts prioritizing production timelines should consider Socket.IO. While you can save some kb’s by using pure WebSockets alone, Socket.IO supports more browsers and has more overall functionality.

For these reasons, numerous indie games and web applications have had success developing with Socket.IO, leading to its proliferation as the library of choice when deploying WebRTC technologies.

Socket.IO’s ubiquity beyond Node.js means that there is a strong developer community support and that it can be implemented using Ruby, C++, and Python, to name a few.

By utilizing WebRTC in any form, you'd be helping support technologies built in the spirit of a free and open-source web. Take it from a seasoned developer:

"WebRTC is a new front in the long war for an open and unencumbered web." Brendan Eich, inventor of JavaScript