处理Websockets连接丢失问题

71

我最近搭建了一个本地WebSocket服务器,它正常运行,但是我遇到了一些麻烦,不知道该如何处理突然断开的连接,这种情况既不是客户端也不是服务器故意引起的,例如:服务器断电、以太网电缆拔出等等... 我需要让客户端在大约10秒内知道是否失去了连接。

客户端方面,连接很简单:

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!');
};

我可以手动触发连接断开,这个方法运行良好,

websocket_conn.close();

但是,如果我只是简单地从计算机后面拔掉以太网电缆,或禁用连接,onclose不会被调用。在另一篇帖子中,我读到当TCP检测到连接丢失时它最终会被调用,但这不是我需要的及时方式,因为我相信Firefox的默认值是10分钟,我也不想去改变数百台计算机的about:config值。我读到的唯一建议是使用“ping/pong”保持活动轮询样式方法,这似乎与WebSockets的概念相违背。

有没有更简单的方法来检测这种断开连接的行为?我读到的旧帖子是否仍然从技术角度保持最新状态,而最佳方法仍然是“ping/pong”样式?


3
如果你不想自己处理保持连接令牌、重新连接尝试等问题,那么你应该使用一个经过验证的库。http://socket.io/ 是一个不错的选择。 - Denys Séguret
6个回答

76

你需要添加ping pong方法

在服务器端创建一个代码,当接收到__ping__时,发送__pong__回来

以下是JavaScript代码:

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 --//
}

4
当结合此处的重连 WebSocket 实现 https://github.com/joewalnes/reconnecting-websocket 时,这对我非常有用,但有一个限制条件:我还必须将间隔计时器分配给一个变量,并在 ping 超时函数中清除它。 - Leo
10
如果你正在使用轮询,那么WebSocket有什么意义呢 :) (注:WebSocket是一种实时通信协议,而轮询则是定期向服务器发送请求以获取更新的数据。) - Steve Moretz
@stevemoretz 我其实正在问自己这个问题。 - manudicri
4
Websockets 似乎只是 TCP/IP 协议的一个非常薄的层,因此如果你需要在当前的 TCP/IP 实现之外实现一个附加功能,你需要自己添加它。这就是为什么回答者说你必须自己添加 ping/pong 的原因。然而,如果 Websockets 内置了这个功能,它将更易于配置且更通用,因为它似乎是一个常见的烦恼。 - LeanMan

12
这是我最终采用的解决方案,目前看起来工作得很好,它完全适用于我的项目设置,并且依赖于原始问题中未提及的一些条件,但如果有人碰巧做同样的事情,它可能会有用。
连接到WebSocket服务器发生在Firefox插件内部,默认情况下Firefox的TCP设置具有10分钟的超时时间。您可以通过about:config查询TCP来查看其他详细信息。
Firefox插件可以访问这些参数。
var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService);

您还可以通过指定分支和偏好以及新值来更改这些参数。

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

因此,任何安装了该插件的计算机都具有TCP连接的10秒超时时间。如果连接丢失,将触发onclose事件,显示警报并尝试重新建立连接。

websocket_conn.onclose = function (e) {
    document.getElementById('websocket_no_connection').style.display = 'block';
    setTimeout(my_extension.setup_websockets, 10000);
}; 

由于现在几乎没有更适合的答案,所以在这里最好的做法可能是接受你的建议。 - Denys Séguret

11

我采用了ping/pong的思路,并且它运行良好。下面是我的server.js文件中的实现:

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;
  });
});

这是我的index.html文件:

<!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>

如果您想亲自尝试,这里是 Cloud 9 沙盒的链接:https://ide.c9.io/design1online/web-sockets


2
我认为你缺少了一些变量和代码行。obj.action === 'join' 这句话从哪里来的?而且我认为你的变量缺少清除 setTimeout 的操作。 - Brad Vanderbush
obj.action === join 是由点击加入按钮触发的。setTimeout 在 keepAlive 中被引用。 - Jade
2
为什么要定义两次 wss.broadcast? - temirbek
那只是一个打字错误,已经修复了,谢谢。 - Jade

6

WebSocket协议定义了ping和pong的控制帧。因此,如果服务器发送一个ping,浏览器将回答一个pong,并且反过来也是如此。可能您使用的WebSocket服务器实现了它们,并且您可以定义一个超时,在此超时内浏览器必须响应或被视为死亡。这对于您在浏览器和服务器中的实现应该是透明的。

您可以使用它们来检测半开放连接:http://blog.stephencleary.com/2009/05/detection-of-half-open-dropped.html

还相关的是:WebSockets ping/pong, why not TCP keepalive?


1
答案不清楚。如何在JavaScript WebSocket中实现一个解决方案,当网络电缆断开时弹出警报? - Nathan B
Ping/pong控制帧在浏览器中尚未实现(至少在2023年),也无法手动发送,因此我们必须使用普通的WS消息来模拟它们。 - shelll

0

好的,我来晚了,但是希望我能在这里添加一些价值。我的TypeScript实现是在一个Angular应用程序中处理WebSocket连接丢失。这不使用PING PONG策略,以避免一直使服务器繁忙。只有在连接丢失后才开始尝试建立连接,并且每隔5秒尝试一次,直到成功连接为止。所以我们开始吧:

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);
   }
}

就是这样。如果您想在某个时候关闭websocket连接,请设置selfClosing = true。这将停止尝试重新连接。希望这有所帮助。我相信,同样的代码也可以在本地的JS中使用。


2
这对于重新连接非常有用,但我认为OP的问题在于它需要太长时间来检测到连接已断开。 - Andrew W. Phillips

0
你可以使用:
window.addEventListener("offline", closeHandler) // 在断开局域网连接后的1秒内对我有效
在代码中:
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("网络连接已断开。")}

回答需要支持信息 您的回答可以通过提供更多的支持信息来改进。请[编辑]以添加进一步的细节,例如引用或文档,以便他人可以确认您的回答是否正确。您可以在帮助中心找到关于如何撰写良好回答的更多信息。 - moken

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接