How To keep and recover websocket connections
Posted: Thu Feb 19, 2015 9:59 am
Hello,
This is a long post, but hey, this is the Coding Corner!
I thought this story might be some useful for those deploying WebSocket connections in web pages. I do myself, having some cheap mobile tablets allowing me to control functions in EventGhost. WebSockets allows, as you know, to push information to the tablet, eliminating the page reload to get status changes.
There are certain things that I have discovered and actually spent a number of days to overcome. When you are establishing a WebSocket connection, you would most likely
- keep an eye on it so that it is ready and alive
- send keep alive messages from the client (I read somewhere that this is recommended)
- reconnect if for some reason it is closed
- monitor for incoming messages and, after a time out depending on how frequently you expect messages, close the WebSocket connection and try to open it again
After digging around on internet, I found the key. When a websocket connection is opened you can follow it's status using the readyState. These constants are used by the readyState attribute to describe the state of the WebSocket connection.
Opening the WebSocket connection first time is normally not causing any problem, readyState is 1 and you can both send and receive messages. But once it is established and gets interrupted, readyState changes to 3. In your code you have most likely implemented a timer that triggers a reconnect attempt. Here you have to be careful, especially if you are connecting over the network. Each attempt needs to time out before you try again and this is the major mistake I did in the beginning, I simply retried to quickly...it worked in the local machine but not over the network. What happened was the following:
1) EG Webserver was running
2) My tablets had connected and had established WebSocket connections, everything working fine
3) I close down EG
4) Tablets losing connections, starts retrying connections (every 2 seconds)
5) I start EG again
6) Now EG receives a large number of connection requests from each tablet (all depending on how long EG was stopped) since they where queued up and things are then messed up, sad for EG had to work hard
Something was needed here. The thing was, I wanted tablets to reconnect as fast as possible. Using the readyState I was able to handle the connections properly and then the above scenario worked ok without a lot of unnecessary attempts.
Just to give some examples of well working client test web pages, I provide them below. The first one is doing the job, it is connecting and recovering a WebSocket connection. In addition, I have added a ping function (keepAlive) that sends a message to the WebSocket server every 30 second. This version would normally be enough but I found another problem (when using Chrome) where the connection timed out but was kept open. I suspect this can be browser dependent (other people have reported similar problems with Chrome). Therefore I have added also a second example further down below.
First example:
In this second example, I have added some more advanced features using an external javascript, worker.js. This is monitoring for expected messages to arrive. If this doesn't happen within a defined time period, the script is forcing down the WebSocket connection. Then it tries to reconnect the normal way as in the first example. Also the second example is working very well during my tests. Only thing to think about is to adjust the time period for expected messages so that it fits your environmnet. In my case, i have 90 seconds, and you adjust it in the worker.js (I'll show below).
Second example:
This is the code for the worker.js script. What you eventually need to do is to modify the lines 20 and 41
The worker.js
Attached are the files, download and unzip them all in the webroot of EG's webserver and try out.
All my own web pages are now updated with the same logic and, cross your fingers, still working as expected. I hope the samples are helpful when you build or modify your own web pages! Best regards,
Walter
This is a long post, but hey, this is the Coding Corner!
I thought this story might be some useful for those deploying WebSocket connections in web pages. I do myself, having some cheap mobile tablets allowing me to control functions in EventGhost. WebSockets allows, as you know, to push information to the tablet, eliminating the page reload to get status changes.
There are certain things that I have discovered and actually spent a number of days to overcome. When you are establishing a WebSocket connection, you would most likely
- keep an eye on it so that it is ready and alive
- send keep alive messages from the client (I read somewhere that this is recommended)
- reconnect if for some reason it is closed
- monitor for incoming messages and, after a time out depending on how frequently you expect messages, close the WebSocket connection and try to open it again
After digging around on internet, I found the key. When a websocket connection is opened you can follow it's status using the readyState. These constants are used by the readyState attribute to describe the state of the WebSocket connection.
So with this in mind, I started to experiment a bit.Constant Value Description
CONNECTING 0 The connection is not yet open.
OPEN 1 The connection is open and ready to communicate.
CLOSING 2 The connection is in the process of closing.
CLOSED 3 The connection is closed or couldn't be opened.
Opening the WebSocket connection first time is normally not causing any problem, readyState is 1 and you can both send and receive messages. But once it is established and gets interrupted, readyState changes to 3. In your code you have most likely implemented a timer that triggers a reconnect attempt. Here you have to be careful, especially if you are connecting over the network. Each attempt needs to time out before you try again and this is the major mistake I did in the beginning, I simply retried to quickly...it worked in the local machine but not over the network. What happened was the following:
1) EG Webserver was running
2) My tablets had connected and had established WebSocket connections, everything working fine
3) I close down EG
4) Tablets losing connections, starts retrying connections (every 2 seconds)
5) I start EG again
6) Now EG receives a large number of connection requests from each tablet (all depending on how long EG was stopped) since they where queued up and things are then messed up, sad for EG had to work hard
Something was needed here. The thing was, I wanted tablets to reconnect as fast as possible. Using the readyState I was able to handle the connections properly and then the above scenario worked ok without a lot of unnecessary attempts.
Just to give some examples of well working client test web pages, I provide them below. The first one is doing the job, it is connecting and recovering a WebSocket connection. In addition, I have added a ping function (keepAlive) that sends a message to the WebSocket server every 30 second. This version would normally be enough but I found another problem (when using Chrome) where the connection timed out but was kept open. I suspect this can be browser dependent (other people have reported similar problems with Chrome). Therefore I have added also a second example further down below.
First example:
Code: Select all
<html lang="sv">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
<META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
<title>EventGhost connection and recovery test</title>
<script language="javascript">
initial = window.setInterval(refresh, 2000);
keepAlive = window.setInterval(kAlive, 30000);
wSocket = null;
inProgress = false;
var prot = "";
var ip = location.host;
if (location.protocol == "https:") {
prot = "wss://";
}
else {
prot = "ws://";
}
//var url = prot + ip + "/ws"; //mandatory for Tornado
var url = prot + ip; //enough for Webserver
function init() {
// event handlers for WebSocket:
inProgress = true;
wSocket = new WebSocket(url);
writeToScreen('init.begin:readyState :'+wSocket.readyState);
writeToScreen('init.begin:inProgress :'+inProgress);
wSocket.onopen = function() {
document.getElementById("output").style.backgroundColor ="white"; //White
writeToScreen('onOpen:readyState :'+wSocket.readyState);
doSend('Ping: '+keepAlive);
}
wSocket.onmessage = wsMessage;
wSocket.onclose = function() {
document.getElementById("output").style.backgroundColor ="#C0C0C0"; //Grey
writeToScreen('onClose:readyState :'+wSocket.readyState);
}
inProgress = false;
writeToScreen('init.finished:inProgress :'+inProgress);
}
function kAlive() {
doSend('Ping: '+keepAlive);
}
function refresh() {
if (wSocket.readyState == 3){
if (inProgress == false) {
writeToScreen('refresh:inProgress :'+inProgress);
init();
}
}
}
function writeToScreen(message) {
var pre = document.createElement("p");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
output.appendChild(pre);
}
function doSend(strng) {
writeToScreen('doSend:readyState :'+wSocket.readyState);
if (wSocket.readyState == 1) {
wSocket.send(JSON.stringify(strng));
}
}
function wsMessage(event) {
writeToScreen(event.data);
}
window.addEventListener("load", init, false);
</script>
<div id="output"></div>
In this second example, I have added some more advanced features using an external javascript, worker.js. This is monitoring for expected messages to arrive. If this doesn't happen within a defined time period, the script is forcing down the WebSocket connection. Then it tries to reconnect the normal way as in the first example. Also the second example is working very well during my tests. Only thing to think about is to adjust the time period for expected messages so that it fits your environmnet. In my case, i have 90 seconds, and you adjust it in the worker.js (I'll show below).
Second example:
Code: Select all
<html lang="sv">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
<META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
<title>EventGhost connection and improved recovery test</title>
<script language="javascript">
var myVar = 0;
var worker = new Worker('worker.js');
initial = window.setInterval(refresh, 2000);
keepAlive = window.setInterval(kAlive, 30000);
wSocket = null;
inProgress = false;
var prot = "";
var ip = location.host;
if (location.protocol == "https:") {
prot = "wss://";
}
else {
prot = "ws://";
}
//var url = prot + ip + "/ws"; //mandatory for Tornado
var url = prot + ip; //enough for Webserver
function doWork(prm) {
worker.postMessage(prm);
}
function checkEven(val) {
return (val%2 == 0);
}
worker.onmessage = function (evt) {
myVar = evt.data[1];
writeToScreen('counter :'+evt.data[0]);
if (evt.data[0] == 999) {
document.getElementById("output").style.color ="red";
wSocket.close();
}
else {
if (checkEven(evt.data[0])) {
document.getElementById("output").style.color ="black";
}
else {
document.getElementById("output").style.color ="blue";
}
}
}
function init() {
// event handlers for WebSocket:
inProgress = true;
wSocket = new WebSocket(url);
writeToScreen('init.begin:readyState :'+wSocket.readyState);
writeToScreen('init.begin:inProgress :'+inProgress);
wSocket.onopen = function() {
document.getElementById("output").style.backgroundColor ="white"; //White
writeToScreen('onOpen:readyState :'+wSocket.readyState);
doSend('Ping: '+keepAlive);
}
wSocket.onmessage = wsMessage;
wSocket.onclose = function() {
document.getElementById("output").style.backgroundColor ="#C0C0C0"; //Grey
writeToScreen('onClose:readyState :'+wSocket.readyState);
}
inProgress = false;
writeToScreen('init.finished:inProgress :'+inProgress);
}
function kAlive() {
doSend('Ping: '+keepAlive);
}
function refresh() {
if (wSocket.readyState == 3){
if (inProgress == false) {
writeToScreen('refresh:inProgress :'+inProgress);
init();
}
}
}
function writeToScreen(message) {
var pre = document.createElement("p");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
output.appendChild(pre);
}
function doSend(strng) {
writeToScreen('doSend:readyState :'+wSocket.readyState);
if (wSocket.readyState == 1) {
wSocket.send(JSON.stringify(strng));
}
}
function wsMessage(event) {
writeToScreen(event.data);
doWork(myVar);
}
window.addEventListener("load", init, false);
</script>
<div id="output"></div>
if (i > 30) {
The timer with interval (3 seconds) is incrementing i every 3 seconds means that after 90 seconds, we will get a re-connection attempt. You may experiment with what is best for you.myVar=setInterval(function(){myTimer()},3000);
The worker.js
Code: Select all
var myVar = 0;
var i = 1;
var j = 0;
var myArray = new Array();
var myTimers = new Array();
function checkEven(val)
{
return (val%2 == 0);
}
function myTimer() {
myArray[0] = i;
myArray[1] = myVar;
myArray[2] = myTimers;
postMessage(myArray);
i++;
if (i > 30) {
myArray[0] = 999;
myArray[1] = myVar;
myArray[2] = myTimers;
postMessage(myArray);
i = 1;
}
}
onmessage = function (event) {
for (j=0; j<myTimers.length; j++) {
clearInterval(myTimers[j]);
}
if (checkEven(i)) {
i = 2;
}
else {
i = 1;
}
myVar = null;
myVar=setInterval(function(){myTimer()},3000);
myTimers.splice(0, myTimers.length, myVar);
}
All my own web pages are now updated with the same logic and, cross your fingers, still working as expected. I hope the samples are helpful when you build or modify your own web pages! Best regards,
Walter