71

I've recently set-up a local WebSocket server which works fine, however I'm having a few troubles understanding how I should handle a sudden loss of connection which neither the client or server intentionally initiated, i.e: Server loses power, ethernet cables pulled out etc... I need the client's to know whether connection has been lost within ~10seconds.

Client side, connection is simply:

var websocket_conn = new WebSocket('ws://192.168.0.5:3000');

websocket_conn.onopen = function(e) {
    console.log('Connected!');
};

websocket_conn.onclose = function(e) {
    console.log('Disconnected!');
};

I can manually trigger the connection disconnect which works fine,

websocket_conn.close();

But if I simply pulled the ethernet cable out the back of the computer, or disabled the connection, onclose doesn't get called. I've read in another post that it would eventually get called when TCP detects loss of connectivity, but it's not in the timely manner that I need as the default for Firefox I believe is 10 minutes, and I don't really want to go around hundreds of computers about:config changing this value. The only other suggestion I've read is to use a 'ping/pong' keep-alive polling style method which seems counterintuitive to the idea of websockets.

Is there an easier way to detect this kind of disconnect behaviour? Are the old posts i'm reading still up to date from a technical point, and the best method is still 'ping/pong' style?

Community
  • 1
  • 1
SubjectCurio
  • 4,702
  • 3
  • 32
  • 46
  • 3
    If you don't want to deal yourself with keep alive tokens, planning reconnection tries, and so on, then you should use a proven library. http://socket.io/ comes to mind. – Denys Séguret Nov 17 '14 at 10:55

6 Answers6

74

You have to add ping pong method

Create a code in server when receive __ping__ send __pong__ back

JavaScript code is give below

function ping() {
        ws.send('__ping__');
        tm = setTimeout(function () {

           /// ---connection closed ///


    }, 5000);
}

function pong() {
    clearTimeout(tm);
}
websocket_conn.onopen = function () {
    setInterval(ping, 30000);
}
websocket_conn.onmessage = function (evt) {
    var msg = evt.data;
    if (msg == '__pong__') {
        pong();
        return;
    }
    //////-- other operation --//
}
Sarath Ak
  • 7,903
  • 2
  • 47
  • 48
  • 4
    This was really useful to me when combined with this reconnecting websocket implementation here: https://github.com/joewalnes/reconnecting-websocket , with one caveat: I also had to assign the interval timer to a variable and clear it within the ping timeout function. – Leo Jul 31 '19 at 12:40
  • 9
    What's the point of websocket if you are doing polling with it :) – Steve Moretz Oct 18 '21 at 07:51
  • @stevemoretz I'm actually asking myself that – manudicri Jan 20 '22 at 21:58
  • 4
    Websockets seem like a really thin layer to TCP/IP so if you need to implement an additional feature beyond the current implementation of TCP/IP you would have to add it yourself. Hence why the answerer stated you have to add ping/ping yourself. It would be nice however, if websockets had this built in so it could be configurable and more generic as it seems its a common annoyance. – LeanMan Jan 27 '22 at 09:15
12

This was the solution I ended up doing which seems to work fine for the time being, it's entirely specific to my project's setup & relies on criteria being in place that wasn't originally mentioned in my question, but it might be useful for someone else if they happen to be doing the same thing.

The connection to the websocket server occurs within a Firefox addon, and by default Firefox's TCP setup has a 10 minute timeout. You can see additional details with about:config and searching for TCP.

Firefox addons can access these parameters

var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService);

and also change these parameters by specifying the branch & preference along with the new value

prefs.getBranch("network.http.tcp_keepalive.").setIntPref('long_lived_idle_time', 10);

So now, any computer with the addon installed have a 10 second timeout for TCP connections. If the connection is lost, the onclose event is triggered which displays an alert and also attempts to re-establish connection

websocket_conn.onclose = function (e) {
    document.getElementById('websocket_no_connection').style.display = 'block';
    setTimeout(my_extension.setup_websockets, 10000);
}; 
SubjectCurio
  • 4,702
  • 3
  • 32
  • 46
11

I used the ping/pong idea and it works nicely. Here's my implementation in my server.js file:

var SOCKET_CONNECTING = 0;
var SOCKET_OPEN = 1;
var SOCKET_CLOSING = 2;
var SOCKET_CLOSED = 3;

var WebSocketServer = require('ws').Server
wss = new WebSocketServer({ port: 8081 });

//Send message to all the users
wss.broadcast = function broadcast(data,sentBy)
{
  for (var i in this.clients)
  {
    this.clients[i].send(data);
  }
};

var userList = [];
var keepAlive = null;
var keepAliveInterval = 5000; //5 seconds

//JSON string parser
function isJson(str)
{
 try {
    JSON.parse(str);
  }
  catch (e) {
    return false;
  }
  return true;
}

//WebSocket connection open handler
wss.on('connection', function connection(ws) {
  
  function ping(client) {
    if (ws.readyState === SOCKET_OPEN) {
      ws.send('__ping__');
    } else {
      console.log('Server - connection has been closed for client ' + client);
      removeUser(client);
    }
  }
  
  function removeUser(client) {
    
    console.log('Server - removing user: ' + client)
    
    var found = false;
    for (var i = 0; i < userList.length; i++) {
      if (userList[i].name === client) {
        userList.splice(i, 1);
        found = true;
      }
    }
    
    //send out the updated users list
    if (found) {
      wss.broadcast(JSON.stringify({userList: userList}));
    };
    
    return found;
  }
  
  function pong(client) {
    console.log('Server - ' + client + ' is still active');
    clearTimeout(keepAlive);
    setTimeout(function () {
      ping(client);
    }, keepAliveInterval);
  }

  //WebSocket message receive handler
  ws.on('message', function incoming(message) {
    if (isJson(message)) {
      var obj = JSON.parse(message);
      
      //client is responding to keepAlive
      if (obj.keepAlive !== undefined) {
        pong(obj.keepAlive.toLowerCase());
      }
      
      if (obj.action === 'join') {
        console.log('Server - joining', obj);
        
        //start pinging to keep alive
        ping(obj.name.toLocaleLowerCase());
        
        if (userList.filter(function(e) { return e.name == obj.name.toLowerCase(); }).length <= 0) {
          userList.push({name: obj.name.toLowerCase()});
        }
        
        wss.broadcast(JSON.stringify({userList: userList}));
        console.log('Server - broadcasting user list', userList);
      }
    }
    
    console.log('Server - received: %s', message.toString());
    return false;
  });
});

Here's my index.html file:

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <title>Socket Test</title>
    </head>
    <body>
        <div id="loading" style="display: none">
            <p align="center">
                LOADING...
            </p>
        </div>
        <div id="login">
            <p align="center">
                <label for="name">Enter Your Name:</label>
                <input type="text" id="name" />
                <select id="role">
                    <option value="0">Attendee</option>
                    <option value="1">Presenter</option>
                </select>
                <button type="submit" onClick="login(document.getElementById('name').value, document.getElementById('role').value)">
                    Join
                </button>
            </p>
        </div>
        <div id="presentation" style="display: none">
            <div class="slides">
                <section>Slide 1</section>
                <section>Slide 2</section>
            </div>
            <div id="online" style="font-size: 12px; width: 200px">
                <strong>Users Online</strong>
                <div id="userList">
                </div>
            </div>
        </div>
        <script>
            function isJson(str) {
                try {
                   JSON.parse(str);
                }
                catch (e) {
                   return false;
                }
                return true;
            }

            var ws;
            var isChangedByMe = true;
            var name = document.getElementById('name').value;
            var role = document.getElementById('role').value;

            function init()
            {
                loading = true;
                ws = new WebSocket('wss://web-sockets-design1online.c9users.io:8081');

                //Connection open event handler
                ws.onopen = function(evt)
                {
                    ws.send(JSON.stringify({action: 'connect', name: name, role: role}));
                }

                ws.onerror = function (msg) {
                    alert('socket error:' + msg.toString());
                }

                //if their socket closes unexpectedly, re-establish the connection
                ws.onclose = function() {
                    init();
                }
                
                //Event Handler to receive messages from server
                ws.onmessage = function(message)
                {
                    console.log('Client - received socket message: '+ message.data.toString());
                    document.getElementById('loading').style.display = 'none';

                    if (message.data) {

                        obj = message.data;
                    
                        if (obj.userList) {
                        
                            //remove the current users in the list
                            userListElement = document.getElementById('userList');
                            
                            while (userListElement.hasChildNodes()) {
                                userListElement.removeChild(userListElement.lastChild);
                            }

                            //add on the new users to the list
                            for (var i = 0; i < obj.userList.length; i++) {
                            
                                var span = document.createElement('span');
                                span.className = 'user';
                                span.style.display = 'block';
                                span.innerHTML = obj.userList[i].name;
                                userListElement.appendChild(span);
                            }
                        }
                    }

                    if (message.data === '__ping__') {
                        ws.send(JSON.stringify({keepAlive: name}));
                    }

                    return false;
                }
            }

            function login(userName, userRole) {

                if (!userName) {
                    alert('You must enter a name.');
                    return false;
                } 

                //set the global variables
                name = userName;
                role = userRole;

                document.getElementById('loading').style.display = 'block';
                document.getElementById('presentation').style.display = 'none';
                document.getElementById('login').style.display = 'none';
                init();
            }
        </script>
    </body>
</html>

Here's a link to the cloud 9 sandbox if you want to try it out yourself: https://ide.c9.io/design1online/web-sockets

Jade
  • 619
  • 5
  • 13
6

The websocket protocol defines control frames for ping and pong. So basically, if the server sends a ping, the browser will answer with a pong, and it should work also the other way around. Probably the WebSocket server you use implements them, and you can define a timeout in which the browser must responds or be considered dead. This should be transparent for your implementation in both browser and server.

You can use them to detect half open connections: http://blog.stephencleary.com/2009/05/detection-of-half-open-dropped.html

Also relevant: WebSockets ping/pong, why not TCP keepalive?

Community
  • 1
  • 1
vtortola
  • 34,709
  • 29
  • 161
  • 263
  • 1
    Answer is not clear. How to implement in javascript websocket a solution that pops an alert when internet cable has been disconnected? – Nathan B Dec 14 '20 at 12:59
  • Ping/pong control frames are not implemented in browsers (at least in 2023), and it is also not possible to send them manually and we must use normal WS messages to emulate them. – shelll Jan 30 '23 at 11:03
0

Okay I'm late to the party but hope I should be able to add some value here. My TypeScript implementation in an Angular app to handle WebSocket Connection lost. This does not use PING PONG strategy in order to avoid keeping server busy all the time. This starts attempting to establish connection only after a connection is lost and keep trying after every 5 seconds till its connected successfully. So here we go:

export class WebSocketClientComponent implements OnInit {

   webSocket?: WebSocket;
   selfClosing = false;
   reconnectTimeout: any;

   ngOnInit(): void {
      this.establishWebSocketConnection();
   }

   establishWebSocketConnection() {

      this.webSocket = new WebSocket('YourServerURlHere');
 
      this.webSocket.onopen = (ev: any) => {
  
         if (this.reconnectTimeout) {
            clearTimeout(this.reconnectTimeout);
         }
      }

      this.webSocket.onclose = (ev: CloseEvent) => {
  
         if (this.selfClosing === false) {
            this.startAttemptingToEstablishConnection();
         }
      }
   }

   private startAttemptingToEstablishConnection() {
      this.reconnectTimeout = setTimeout(() => this.establishWebSocketConnection(), 5000);
   }
}

And thats it. If you want to close websocket connection at some point set selfClosing = true. This would stop attempting to reconnect again. Hope this helps. I'm sure, same code can be used in native JS as well.

Muhammad Qasim
  • 1,622
  • 14
  • 26
0

You can use:
window.addEventListener("offline", closeHandler) // works for me in 1 seconds after break LAN connection

in code:
const createChannel = () => {
ws && closeChannelCommon()
ws = new WebSocket( "wss://social-network.samuraijs.com/handlers/ChatHandler.ashx" )
ws?.addEventListener( 'open', openHandler )
ws?.addEventListener( 'message', messageHandler )
ws?.addEventListener( 'close', closeHandler )
window.addEventListener( 'offline', closeHandler )
}

const closeHandler = () => {console.log("The network connection has been lost.");}

  • Answer needs supporting information Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](https://stackoverflow.com/help/how-to-answer). – moken Jul 08 '23 at 06:18