In this tutorial we are going to create a basic signalling server using javascript and nodejs. It will be able to connect two users together.
Assume we have 2 users UserA and UserB the scheme of our server will be: 1. Each user will start by registering themselves on the server(simple string login). 2. Now UserA can call UserB. UserA makes an offer with the user identifier(UserB) he wishes to call. 3. The other user(User B) should answer. 4. In the end ice candidates are sent between users so they can make a connection. Remember that there are no rules implementing a signalling server.
First of all you should install node.js on your computer. To test it create an index.js file and put the following code:
console.log("node testing");
Then run it using node index.js. You might see the output phrase in the console.
We are going to use WebSockets - a bidirectional socket connection between a web browser and a web server. Run npm install ws to install WebSockets library for node.js.
Let’s start using it. Insert the following code in an index.js file:
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
wss.on('connection', function(connection){
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message){
console.log("Got message:", message);
});
connection.send("Hello world");
});
To test our server install wscat. Run npm install -g wscat. This will install wscat globally so you will be able to access it through a command line.
Run our server in one console window. Then open a new console window and run wscat -c ws://localhost:9090. Then try to send a message to the server.
Let’s add a simple string based auth. Modify index.js:
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection){
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message){
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Error parsing JSON");
data = {};
}
//switching type of the user message
switch (data.type){
//when a user tries to login
case "login":
console.log("User logged as", data.name);
//if anyone has already logged with this username refuse
if(users[data.name]){
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Unrecognized command: " + data.type
});
break;
}
});
//when user exits
connection.on("close", function(){
if(connection.name){
delete users[connection.name];
}
});
connection.send("Hello world");
});
function sendTo(conn, message){
conn.send(JSON.stringify(message));
}
Now connect to our server and try to login:
run wscat -c ws://localhost:9090
run {"type":"login","name":"UserA"} when connected
Let’s implement the offer handler, which is called when one user want to call another:
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null){
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
The answer handler is called when user answers to someone’s offer:
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null){
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
The final part is handling ICE candidates between users. Remember that candidate messages might happen multiple times between users. Add candidate handler:
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null){
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
A good feature is implementing the leave handler which will allow our users to disconnect from another user and notify our server to disconnect any user references. Add the leave handler:
case "leave":
console.log("Disconnecting user from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null){
sendTo(conn, {
type: "leave"
});
}
break;
To handle a case when a user drops a connection we need to modify the close handler:
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "canidate" state
connection.on("close", function(){
if(connection.name){
delete users[connection.name];
if(connection.otherName){
console.log("Disconnecting user from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null){
sendTo(conn, {
type: "leave"
});
}
}
}
});
The entire code of our signalling server:
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection){
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message){
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Error parsing JSON");
data = {};
}
//switching type of the user message
switch (data.type){
//when a user tries to login
case "login":
console.log("User logged as", data.name);
//if anyone has already logged with this username refuse
if(users[data.name]){
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null){
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null){
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null){
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting user from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null){
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Unrecognized command: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "canidate" state
connection.on("close", function(){
if(connection.name){
delete users[connection.name];
if(connection.otherName){
console.log("Disconnecting user from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null){
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
//when server is ready to accept WebSocket connections
wss.on('listening', function(){
console.log("Server started...");
});
function sendTo(conn, message){
conn.send(JSON.stringify(message));
}
In the real world, signalling is not defined by the WebRTC specification. So there are a few glitches like complex firewall systems or VPN(Virtual Private Network) when you fall back on other technologies such as HTTP instead of WebSockets. I also advise you to check XMPP and SIP protocols which give a lot of power to any typical WebRTC app and SIP-based phone devices integration.
That’s all for today :)