diff --git a/packages/node_modules/@node-red/nodes/core/network/22-websocket.html b/packages/node_modules/@node-red/nodes/core/network/22-websocket.html index 093256495..e3ee30eeb 100644 --- a/packages/node_modules/@node-red/nodes/core/network/22-websocket.html +++ b/packages/node_modules/@node-red/nodes/core/network/22-websocket.html @@ -176,7 +176,8 @@ defaults: { path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)}, tls: {type:"tls-config",required: false}, - wholemsg: {value:"false"} + wholemsg: {value:"false"}, + hb: {value: "", validate: RED.validators.number(/*blank allowed*/true) } }, inputs:0, outputs:0, @@ -188,11 +189,24 @@ $(".node-config-row-tls").toggle(/^wss:/i.test($(this).val())) }); $("#node-config-input-path").change(); + + var heartbeatActive = (this.hb && this.hb != "0"); + $("#node-config-input-hb-cb").prop("checked",heartbeatActive); + $("#node-config-input-hb-cb").on("change", function(evt) { + $("#node-config-input-hb-row").toggle(this.checked); + }) + $("#node-config-input-hb-cb").trigger("change"); + if (!heartbeatActive) { + $("#node-config-input-hb").val(""); + } }, oneditsave: function() { if (!/^wss:/i.test($("#node-config-input-path").val())) { $("#node-config-input-tls").val("_ADD_"); } + if (!$("#node-config-input-hb-cb").prop("checked")) { + $("#node-config-input-hb").val("0"); + } } }); @@ -259,6 +273,14 @@ +
+ + + + + + +

diff --git a/packages/node_modules/@node-red/nodes/core/network/22-websocket.js b/packages/node_modules/@node-red/nodes/core/network/22-websocket.js index e47439f5f..3373c6e72 100644 --- a/packages/node_modules/@node-red/nodes/core/network/22-websocket.js +++ b/packages/node_modules/@node-red/nodes/core/network/22-websocket.js @@ -55,6 +55,13 @@ module.exports = function(RED) { node.closing = false; node.tls = n.tls; + if (n.hb) { + var heartbeat = parseInt(n.hb); + if (heartbeat > 0) { + node.heartbeat = heartbeat * 1000; + } + } + function startconn() { // Connect to remote endpoint node.tout = null; var prox, noprox; @@ -93,9 +100,24 @@ module.exports = function(RED) { function handleConnection(/*socket*/socket) { var id = RED.util.generateId(); + socket.nrId = id; + socket.nrPendingHeartbeat = false; if (node.isServer) { node._clients[id] = socket; node.emit('opened',{count:Object.keys(node._clients).length,id:id}); + } else { + if (node.heartbeat) { + node.heartbeatInterval = setInterval(function() { + if (socket.nrPendingHeartbeat) { + // No pong received + socket.terminate(); + socket.nrErrorHandler(new Error("timeout")); + return; + } + socket.nrPendingHeartbeat = true; + socket.ping(); + },node.heartbeat); + } } socket.on('open',function() { if (!node.isServer) { @@ -103,6 +125,7 @@ module.exports = function(RED) { } }); socket.on('close',function() { + clearInterval(node.heartbeatInterval); if (node.isServer) { delete node._clients[id]; node.emit('closed',{count:Object.keys(node._clients).length,id:id}); @@ -117,13 +140,21 @@ module.exports = function(RED) { socket.on('message',function(data,flags) { node.handleEvent(id,socket,'message',data,flags); }); - socket.on('error', function(err) { + socket.nrErrorHandler = function(err) { + clearInterval(node.heartbeatInterval); node.emit('erro',{err:err,id:id}); if (!node.closing && !node.isServer) { clearTimeout(node.tout); node.tout = setTimeout(function() { startconn(); }, 3000); // try to reconnect every 3 secs... bit fast ? } - }); + } + socket.on('error',socket.nrErrorHandler); + socket.on('ping', function() { + socket.nrPendingHeartbeat = false; + }) + socket.on('pong', function() { + socket.nrPendingHeartbeat = false; + }) } if (node.isServer) { @@ -152,6 +183,19 @@ module.exports = function(RED) { node.server = new ws.Server(serverOptions); node.server.setMaxListeners(0); node.server.on('connection', handleConnection); + // Not adding server-initiated heartbeats yet + // node.heartbeatInterval = setInterval(function() { + // node.server.clients.forEach(function(ws) { + // if (ws.nrPendingHeartbeat) { + // // No pong received + // ws.terminate(); + // ws.nrErrorHandler(new Error("timeout")); + // return; + // } + // ws.nrPendingHeartbeat = true; + // ws.ping(); + // }); + // }) } else { node.closing = false; @@ -159,6 +203,9 @@ module.exports = function(RED) { } node.on("close", function() { + if (node.heartbeatInterval) { + clearInterval(node.heartbeatInterval); + } if (node.isServer) { delete listenerNodes[node.fullPath]; node.server.close(); @@ -168,8 +215,6 @@ module.exports = function(RED) { // RED.server.removeListener('upgrade', handleServerUpgrade); // serverUpgradeAdded = false; // } - - } else { node.closing = true; diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index f2ca607da..dd09e29f0 100755 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -512,6 +512,7 @@ "sendrec": "Send/Receive", "payload": "payload", "message": "entire message", + "sendheartbeat": "Send heartbeat", "tip": { "path1": "By default, payload will contain the data to be sent over, or received from a websocket. The listener can be configured to send or receive the entire message object as a JSON formatted string.", "path2": "This path will be relative to __path__.",