The Need for a Signaling Channel - Real-Time Communication with WebRTC (2014)

Real-Time Communication with WebRTC (2014)

Chapter 4. The Need for a Signaling Channel

As we anticipated in Chapter 3, a signaling channel is needed in a WebRTC-enabled application in order to allow for the exchanging of both session descriptions and network reachability information. Up until now, we have disregarded this specific aspect by sticking to a local perspective. This turned out to be useful, since it allowed us to just focus on the details of the WebRTC APIs, while leaving aside all networking-related aspects. The time is now ripe to also tackle these last issues. In this chapter, we will describe how we can create a proper signaling channel between any pair of peers that are interested in successfully setting up a WebRTC-enabled communication session.

The material presented in this chapter is only loosely related to the main topic of the book. More precisely, we will herein just focus on the creation of the above-mentioned signaling channel by describing the design and implementation of a very simple JavaScript application involving two clients and a server. The example itself should provide the reader with a set of tools that can be easily reused in a wide set of application scenarios. In the following chapter we will finally put all pieces together in order to complete the 10-step WebRTC recipe in a distributed setting.

Building Up a Simple Call Flow

As usual, we will continue to embrace the learn-by-example approach in order to let you figure out how to build a server-assisted signaling channel between two remote peers. In this chapter, we will focus on the realization of a simple interaction scenario, as formally depicted in the sequence diagram in Figure 4-1.

Signaling channel example: sequence diagram

Figure 4-1. Signaling channel example: Sequence diagram

The diagram in the picture involves three different actors:

§ A channel initiator, such as the peer that first takes the initiative of creating a dedicated communication channel with a remote party

§ A signaling server, managing channel creation and acting as a message relaying node

§ A channel joiner, for instance, a remote party joining an already existing channel

The idea is that the channel is created on demand by the server after receiving a specific request issued by the initiator. As soon as the second peer joins the channel, conversation can start. Message exchanging always happens through the server, which basically acts as a transparent relay node. When one of the peers decides to quit an ongoing conversation, it issues an ad hoc message (called Bye in the figure) towards the server, before disconnecting. This message is dispatched by the server to the remote party, which also disconnects, after having sent an acknowledgment back to the server. The receipt of the acknowledgment eventually triggers the channel reset procedure on the server’s side, thus bringing the overall scenario back to its original configuration.

Let’s start by building a simple HTML5 page (see Example 4-1), containing an initially empty <div> element which will be used to track down the evolution of the communication between two remote peers interacting through the signaling server.

Example 4-1. Simple signaling channel

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"

"http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<title>WebRTC client</title>

</head>

<body>

<script src='/socket.io/socket.io.js'></script>

<div id="scratchPad"></div>

<script type="text/javascript" src="js/simpleNodeClient.js"></script>

</body>

</html>

As you can see from the HTML code, the page includes two JavaScript files. The former (socket.io.js) refers to the well-known socket.io library for real-time web applications.

THE SOCKET.IO JAVASCRIPT LIBRARY

socket.io is a JavaScript library for real-time web applications. It has two parts: a client-side library that runs in the browser, and a server-side library for Node.js (see The Node.js Software Platform.)

The client-side part of socket.io is an event-driven library that primarily uses the WebSocket protocol, but if needed can fall back onto multiple other methods, such as Adobe Flash sockets, AJAX long polling, and others, while providing the same interface. It provides many advanced features, like associating multiple sockets with a server-side room, broadcasting to multiple sockets, storing data associated with specific clients, and managing asynchronous I/O.

socket.io can be easily installed with the node packaged modules (npm) tool:

npm install socket.io

Once installed, the socket.io.js file has to be copied to a folder where it can be found by the web server.

The demo application also requires the node-static module, which needs to be installed as well:

npm install node-static

The latter file (simpleNodeClient.js) is presented in the following:

// Get <div> placeholder element from DOM

div = document.getElementById('scratchPad');

// Connect to server

var socket = io.connect('http://localhost:8181');

// Ask channel name from user

channel = prompt("Enter signaling channel name:");

if (channel !== "") {

console.log('Trying to create or join channel: ', channel);

// Send 'create or join' to the server

socket.emit('create or join', channel);

}

// Handle 'created' message

socket.on('created', function (channel){

console.log('channel ' + channel + ' has been created!');

console.log('This peer is the initiator...');

// Dynamically modify the HTML5 page

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) + ' --> Channel '

+ channel + ' has been created! </p>');

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> This peer is the initiator...</p>');

});

// Handle 'full' message

socket.on('full', function (channel){

console.log('channel ' + channel + ' is too crowded! \

Cannot allow you to enter, sorry :-(');

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) + ' --> \

channel ' + channel + ' is too crowded! \

Cannot allow you to enter, sorry :-( </p>');

});

// Handle 'remotePeerJoining' message

socket.on('remotePeerJoining', function (channel){

console.log('Request to join ' + channel);

console.log('You are the initiator!');

div.insertAdjacentHTML( 'beforeEnd', '<p style="color:red">Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Message from server: request to join channel ' +

channel + '</p>');

});

// Handle 'joined' message

socket.on('joined', function (msg){

console.log('Message from server: ' + msg);

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Message from server: </p>');

div.insertAdjacentHTML( 'beforeEnd', '<p style="color:blue">' +

msg + '</p>');

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Message from server: </p>');

div.insertAdjacentHTML( 'beforeEnd', '<p style="color:blue">' +

msg + '</p>');

});

// Handle 'broadcast: joined' message

socket.on('broadcast: joined', function (msg){

div.insertAdjacentHTML( 'beforeEnd', '<p style="color:red">Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Broadcast message from server: </p>');

div.insertAdjacentHTML( 'beforeEnd', '<p style="color:red">' +

msg + '</p>');

console.log('Broadcast message from server: ' + msg);

// Start chatting with remote peer:

// 1. Get user's message

var myMessage = prompt('Insert message to be sent to your peer:', "");

// 2. Send to remote peer (through server)

socket.emit('message', {

channel: channel,

message: myMessage});

});

// Handle remote logging message from server

socket.on('log', function (array){

console.log.apply(console, array);

});

// Handle 'message' message

socket.on('message', function (message){

console.log('Got message from other peer: ' + message);

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Got message from other peer: </p>');

div.insertAdjacentHTML( 'beforeEnd', '<p style="color:blue">' +

message + '</p>');

// Send back response message:

// 1. Get response from user

var myResponse = prompt('Send response to other peer:', "");

// 2. Send it to remote peer (through server)

socket.emit('response', {

channel: channel,

message: myResponse});

});

// Handle 'response' message

socket.on('response', function (response){

console.log('Got response from other peer: ' + response);

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Got response from other peer: </p>');

div.insertAdjacentHTML( 'beforeEnd', '<p style="color:blue">' +

response + '</p>');

// Keep on chatting

var chatMessage = prompt('Keep on chatting. \

Write "Bye" to quit conversation', "");

// User wants to quit conversation: send 'Bye' to remote party

if(chatMessage == "Bye"){

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Sending "Bye" to server...</p>');

console.log('Sending "Bye" to server');

socket.emit('Bye', channel);

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Going to disconnect...</p>');

console.log('Going to disconnect...');

// Disconnect from server

socket.disconnect();

}else{

// Keep on going: send response back

// to remote party (through server)

socket.emit('response', {

channel: channel,

message: chatMessage});

}

});

// Handle 'Bye' message

socket.on('Bye', function (){

console.log('Got "Bye" from other peer! Going to disconnect...');

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Got "Bye" from other peer!</p>');

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Sending "Ack" to server</p>');

// Send 'Ack' back to remote party (through server)

console.log('Sending "Ack" to server');

socket.emit('Ack');

// Disconnect from server

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Going to disconnect...</p>');

console.log('Going to disconnect...');

socket.disconnect();

});

The code performs the following actions:

1. Allows the client to connect to the server (through the socket.io library)

2. Prompts the user for the name of the channel she wants to join

3. Sends a create or join request to the server

4. Starts to asynchronously handle server-sent events.

In the remainder of this chapter, we will follow a complete call flow in a step-by-step fashion. Before doing this, though, we will take a look at the server-side behavior. The server has been written by leveraging the Node.js JavaScript library.

THE NODE.JS SOFTWARE PLATFORM

Node.js is an extremely powerful software platform that allows users to easily build scalable server-side applications with JavaScript. It is based on a single-threaded event loop management process making use of nonblocking I/O.

The library provides a built-in HTTP server implementation, making it independent from third-party software components. With Node.js, it is really easy for the programmer to implement a high-performance HTTP server with customized behavior with just a few lines of code.

Let’s go over the server-side code. It basically looks after the creation of a server instance listening on port 8181. The code allows for the creation of server-side “rooms” hosting two client sockets at most. The first client that asks for the creation of a room is the channel initiator.

After channel creation, the server-side policy is the following:

1. The second client arriving is allowed to join the newly created channel.

2. All other clients are denied access to the room (and are consequently notified of such an event).

3. varstatic = require('node-static');

4.

5. var http = require('http');

6.

7. // Create a node-static server instance listening on port 8181

8. var file = new(static.Server)();

9.

10.// We use the http module’s createServer function and

11.// use our instance of node-static to serve the files

12.var app = http.createServer(function (req, res) {

13. file.serve(req, res);

14.}).listen(8181);

15.

16.// Use socket.io JavaScript library for real-time web applications

17.var io = require('socket.io').listen(app);

18.

19.// Let's start managing connections...

20.io.sockets.on('connection', function (socket){

21.

22. // Handle 'message' messages

23. socket.on('message', function (message) {

24. log('S --> Got message: ', message);

25.

26. socket.broadcast.to(message.channel).emit('message', \

27. message.message);

28. });

29.

30. // Handle 'create or join' messages

31. socket.on('create or join', function (channel) {

32. var numClients = io.sockets.clients(channel).length;

33. console.log('numclients = ' + numClients);

34.

35. // First client joining...

36. if (numClients == 0){

37. socket.join(channel);

38. socket.emit('created', channel);

39. // Second client joining...

40. } elseif (numClients == 1) {

41. // Inform initiator...

42. io.sockets.in(channel).emit('remotePeerJoining', channel);

43. // Let the new peer join channel

44. socket.join(channel);

45.

46. socket.broadcast.to(channel).emit('broadcast: joined', 'S --> \

47. broadcast(): client ' + socket.id + ' joined channel ' \

48. + channel);

49. } else { // max two clients

50. console.log("Channel full!");

51. socket.emit('full', channel);

52. }

53. });

54.

55. // Handle 'response' messages

56. socket.on('response', function (response) {

57. log('S --> Got response: ', response);

58.

59. // Just forward message to the other peer

60. socket.broadcast.to(response.channel).emit('response',

61. response.message);

62. });

63.

64. // Handle 'Bye' messages

65. socket.on('Bye', function(channel){

66. // Notify other peer

67. socket.broadcast.to(channel).emit('Bye');

68.

69. // Close socket from server's side

70. socket.disconnect();

71. });

72.

73. // Handle 'Ack' messages

74. socket.on('Ack', function () {

75. console.log('Got an Ack!');

76. // Close socket from server's side

77. socket.disconnect();

78. });

79.

80. // Utility function used for remote logging

81. function log(){

82. var array = [">>> "];

83. for (var i = 0; i < arguments.length; i++) {

84. array.push(arguments[i]);

85. }

86. socket.emit('log', array);

87. }

});

We’re now ready to get started with our signaling example walk-through.

Creating the Signaling Channel

We herein focus on the very first steps of the example call flow, as illustrated in Figure 4-2.

Let’s assume that a first client using the Chrome browser loads the HTML5 page of Example 4-1. The page first connects to the server and then prompts the user for the name of the channel (Figure 4-3):

...

// Connect to server

var socket = io.connect('http://localhost:8181');

// Ask channel name from user

channel = prompt("Enter signaling channel name:");

...

Moving the first steps: channel creation

Figure 4-2. The first steps: Channel creation

The example page loaded in Chrome (channel initiator)

Figure 4-3. The example page loaded in Chrome (channel initiator)

Once the user fills in the channel name field and hits the OK button, the JavaScript code in the page sends a create or join message to the server:

...

if (channel !== "") {

console.log('Trying to create or join channel: ', channel);

// Send 'create or join' to the server

socket.emit('create or join', channel);

}

...

Upon reception of the client’s request, the server performs the following actions:

1. Verifies that the mentioned channel is a brand new one (i.e., there are no clients in it)

2. Associates a server-side room with the channel

3. Allows the requesting client to join the channel

4. Sends back to the client a notification message called created

The following snippet shows this sequence of actions:

...

socket.on('create or join', function (channel) {

var numClients = io.sockets.clients(channel).length;

console.log('numclients = ' + numClients);

// First client joining...

if (numClients == 0){

socket.join(channel);

socket.emit('created', channel);

...

Figure 4-4 shows the server’s console right after the aforementioned actions have been performed.

When the initiating client receives the server’s answer, it simply logs the event both on the JavaScript console and inside the <div> element contained in the HTML5 page:

...

// Handle 'created' message

socket.on('created', function (channel){

console.log('channel ' + channel + ' has been created!');

console.log('This peer is the initiator...');

// Dynamically modify the HTML5 page

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) + ' --> Channel ' +

channel + ' has been created! </p>');

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> This peer is the initiator...</p>');

});

...

Signaling server managing inititator’s request

Figure 4-4. Signaling server managing initiator’s request

The situation described above is illustrated in Figure 4-5.

Initiator’s window after channel creation

Figure 4-5. Initiator’s window after channel creation

Joining the Signaling Channel

Let’s now move on to the second client, the channel joiner, focusing on the call flow section shown in Figure 4-6.

Joining an already existing channel

Figure 4-6. Joining an already existing channel

For the sake of completeness, we will this time use Firefox as the client browser, the look and feel of which, right after loading the application page, is illustrated in Figure 4-7.

As already described, the client first connects to the server and then sends it a create or join request. Since this time the requesting peer is not the initiator, the server’s behavior will be driven by the following code snippet:

...

} elseif (numClients == 1) {

// Inform initiator...

io.sockets.in(channel).emit('remotePeerJoining', channel);

// Let the new peer join channel

socket.join(channel);

socket.broadcast.to(channel).emit('broadcast: joined', 'S -->

broadcast(): client ' + socket.id + ' joined channel ' + channel);

...

Basically, the server will:

1. Notify the channel initiator of the arrival of a new join request.

2. Allow the new client to enter the already existing room.

3. Update (through a broadcast message) the channel initiator about the successful completion of the join operation, allowing it to prepare to start a new conversation.

The example page loaded in Firefox (channel ‘joiner')

Figure 4-7. The example page loaded in Firefox (channel joiner)

Such a sequence of actions is reported in Figure 4-8, which shows the server’s console at this stage of the call flow.

Signaling server managing joiner’s request

Figure 4-8. Signaling server managing joiner’s request

Figures 4-9 and 4-10 show, respectively, the joiner’s and initiator’s windows right after the former has successfully joined the signaling channel created by the latter. As the reader will recognize, this sequence of server-side actions is reported in red in the initiator’s HTML5 page in Figure 4-10, which now prompts the user for the very first message to be exchanged across the server-mediated communication path.

Joiner’s window after joining the channel

Figure 4-9. Joiner’s window after joining the channel

Starting a conversation after channel setup

Figure 4-10. Starting a conversation after channel setup

Starting a Server-Mediated Conversation

We have now arrived at the call flow stage reported in Figure 4-11, which basically captures the core of the application. In this phase, in fact, the initiator sends a first message to the joiner, who is first notified of this event and then prompted for the introduction of a proper answer.

Starting a conversation

Figure 4-11. Starting a conversation

As usual, the client retrieves the user’s input and emits a message towards the server in order for it to be properly dispatched. On the server’s side, the received message is simply broadcast[2] on the channel:

...

// Handle 'message' messages

socket.on('message', function (message) {

log('S --> Got message: ', message);

socket.broadcast.to(message.channel).emit('message', message.message);

});

...

The above described server’s behavior is illustrated in the console snapshot of Figure 4-12.

Signalling server acting as a relay node

Figure 4-12. Signaling server acting as a relay node

Figure 4-13 shows the remote peer (the joiner) that has just received the message relayed by the server.

Remote peer receiving relayed message from signaling server

Figure 4-13. Remote peer receiving relayed message from signaling server

As evidenced by the figure, the following actions are performed:

1. Logging the received message both on the JavaScript console and on the HTML5 page

2. Prompting the receiver for proper input

3. Sending the receiver’s answer back to the sender (across the signaling channel)

Such a sequence is driven by the following code snippet:

...

// Handle 'message' message

socket.on('message', function (message){

console.log('Got message from other peer: ' + message);

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Got message from other peer: </p>');

div.insertAdjacentHTML( 'beforeEnd', '<p style="color:blue">' +

message + '</p>');

// Send back response message:

// 1. Get response from user

var myResponse = prompt('Send response to other peer:', "");

// 2. Send it to remote peer (through server)

socket.emit('response', {

channel: channel,

message: myResponse});

});

...

As soon as the receiver hits the OK button on the prompt window in Figure 4-14, the response message is emitted towards the server, which forwards it to the remote party:

...

// Handle 'response' messages

socket.on('response', function (response) {

log('S --> Got response: ', response);

// Just forward message to the other peer

socket.broadcast.to(response.channel).emit('response',

response.message);

});

...

This behavior is once again illustrated by the server’s console snapshot in Figure 4-14.

Signaling server relaying remote peer’s response

Figure 4-14. Signaling server relaying remote peer’s response

Continuing to Chat Across the Channel

We are now in the steady-state portion of the application (Figure 4-15), where the two peers simply take turns in asking the server to relay messages towards the other party.

Signaling channel use in the steady-state

Figure 4-15. Signaling channel use in the steady state

Message exchanging is achieved on the client’s side through the following code:

...

// Handle 'response' message

socket.on('response', function (response){

console.log('Got response from other peer: ' + response);

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Got response from other peer: </p>');

div.insertAdjacentHTML( 'beforeEnd', '<p style="color:blue">' +

response + '</p>');

// Keep on chatting

var chatMessage = prompt('Keep on chatting. Write \

"Bye" to quit conversation', "");

...

...

// Keep on going: send response back to remote party (through server)

socket.emit('response', {

channel: channel,

message: chatMessage});

}

});

Basically, upon reception of a new message, each peer performs the usual logging operations and then prompts the user for new input. As long as the inserted text has a value other than Bye, it sends a new message to the remote party. Figure 4-16 shows the initiator’s window right before a new message is emitted across the channel.

Keeping on chatting (initiator’s side)

Figure 4-16. Continuing the chat (initiator’s side)

Figure 4-17 in turn shows the server’s console upon reception of such a message, which is, as usual, broadcast to the remote party.

Keeping on chatting (server’s side)

Figure 4-17. Continuing the chat (server’s side)

Finally, Figure 4-18 shows reception of the relayed message on the receiver’s side.

Keeping on chatting (joiner’s side)

Figure 4-18. Continuing the chat (joiner’s side)

Closing the Signaling Channel

We are now ready to analyze channel teardown, as described in the call flow snippet in Figure 4-19.

Closing the signaling channel

Figure 4-19. Closing the signaling channel

The teardown procedure is actually triggered by the insertion of a Bye message in one of the two browsers (see Figure 4-20).

What happens behind the scenes is the following:

...

// User wants to quit conversation: send 'Bye' to remote party

if(chatMessage == "Bye"){

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Sending "Bye" to server...</p>');

console.log('Sending "Bye" to server');

socket.emit('Bye', channel);

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Going to disconnect...</p>');

console.log('Going to disconnect...');

// Disconnect from server

socket.disconnect();

}

...

Going to close channel through ‘Bye’ message

Figure 4-20. Closing channel through a Bye message

As we can see in the code, the disconnecting client first sends a Bye message across the channel and immediately thereafter closes the web socket (Figure 4-21).

As soon as the server gets the Bye message, it first relays it to the remote party and then closes the communication channel towards the disconnecting client:

...

// Handle 'Bye' messages

socket.on('Bye', function(channel){

// Notify other peer

socket.broadcast.to(channel).emit('Bye');

// Close socket from server's side

socket.disconnect();

});

...

Initiator’s disconnection

Figure 4-21. Initiator’s disconnection

Let’s finally analyze the behavior of the peer receiving the Bye message from the remote party. The peer first logs information about the received message (both on the JavaScript console and inside the HTML5 page):

...

// Handle 'Bye' message

socket.on('Bye', function (){

console.log('Got "Bye" from other peer! Going to disconnect...');

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Got "Bye" from other peer!</p>');

...

Then, an Ack message is sent back to the server to confirm reception of the disconnection request:

...

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Sending "Ack" to server</p>');

// Send 'Ack' back to remote party (through server)

console.log('Sending "Ack" to server');

socket.emit('Ack');

// Disconnect from server

div.insertAdjacentHTML( 'beforeEnd', '<p>Time: ' +

(performance.now() / 1000).toFixed(3) +

' --> Going to disconnect...</p>');

console.log('Going to disconnect...');

socket.disconnect();

...

Finally, the receiving peer tears down its own connection to the server:

...

console.log('Going to disconnect...');

socket.disconnect();

});

The above sequence of actions can be easily identified in the snapshot in Figure 4-22.

Remote peer handling relayed disconnection message and disconnecting

Figure 4-22. Remote peer handling relayed disconnection message and disconnecting

The final actions are undertaken on the server’s side. Reception of the Ack message is logged on the console (see Figure 4-23) and the channel is eventually torn down:

// Handle 'Ack' messages

socket.on('Ack', function () {

console.log('Got an Ack!');

// Close socket from server's side

socket.disconnect();

});

Closing channel on the server’s side

Figure 4-23. Closing channel on the server’s side


[2] Note that broadcasting on a channel made of just two peers is equivalent to sending a notification to the peer who was not the sender of the message itself.