EventSource (SSE) 是否应该无限期地尝试重新连接?

问题描述 投票:0回答:7

我正在开发一个利用 Server-Sent-Events 的项目,并且刚刚遇到了一些有趣的事情:Chrome 和 Firefox 之间的连接丢失处理方式不同。

在 Chrome 35 或 Opera 22 上,如果您失去与服务器的连接,它将每隔几秒无限期地尝试重新连接,直到成功。另一方面,在 Firefox 30 上,它只会尝试一次,然后您必须刷新页面或处理引发的错误事件并手动重新连接。

我更喜欢 Chrome 或 Opera 的方式,但是阅读 http://www.w3.org/TR/2012/WD-eventsource-20120426/#processing-model,似乎一旦 EventSource 尝试重新连接并由于网络错误或其他原因失败,因此不应重试连接。但不确定我是否正确理解了规范。

我决定要求用户使用 Firefox,主要是因为您不能在 Chrome 上打开来自同一 URL 的多个选项卡和事件流,但这一新发现可能会是一个更大的问题。不过,如果 Firefox 的行为符合规范,那么我不妨以某种方式解决它。

编辑:

我现在将继续瞄准 Firefox。这就是我处理重新连接的方式:

var es = null;
function initES() {
    if (es == null || es.readyState == 2) { // this is probably not necessary.
        es = new EventSource('/push');
        es.onerror = function(e) {
            if (es.readyState == 2) {
                setTimeout(initES, 5000);
            }
        };
        //all event listeners should go here.
    }
}
initES();
javascript google-chrome firefox server-sent-events
7个回答
23
投票

服务器端事件在所有浏览器中的工作方式都不同,但它们都会在某些情况下关闭连接。例如,Chrome 会在服务器重新启动时因 502 错误而关闭连接。因此,最好像其他人建议的那样使用保持活动状态,或者在每次出现错误时重新连接。保持活动状态仅以指定的时间间隔重新连接,该时间间隔必须保持足够长的时间,以避免服务器不堪重负。每次发生错误时重新连接的延迟尽可能最低。但是,只有采取将服务器负载保持在最低限度的方法,才有可能实现这一点。下面,我演示了一种以合理速率重新连接的方法。

此代码使用去抖功能以及重新连接间隔加倍。它运行良好,连接时间为 1 秒、4 秒、8 秒、16 秒……最多 64 秒,并以相同的速率不断重试。

function isFunction(functionToCheck) {
  return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}

function debounce(func, wait) {
    var timeout;
    var waitFunc;
    
    return function() {
        if (isFunction(wait)) {
            waitFunc = wait;
        }
        else {
            waitFunc = function() { return wait };
        }
        
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            func.apply(context, args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, waitFunc());
    };
}

// reconnectFrequencySeconds doubles every retry
var reconnectFrequencySeconds = 1;
var evtSource;

var reconnectFunc = debounce(function() {
    setupEventSource();
    // Double every attempt to avoid overwhelming server
    reconnectFrequencySeconds *= 2;
    // Max out at ~1 minute as a compromise between user experience and server load
    if (reconnectFrequencySeconds >= 64) {
        reconnectFrequencySeconds = 64;
    }
}, function() { return reconnectFrequencySeconds * 1000 });

function setupEventSource() {
    evtSource = new EventSource(/* URL here */); 
    evtSource.onmessage = function(e) {
      // Handle event here
    };
    evtSource.onopen = function(e) {
      // Reset reconnect frequency upon successful connection
      reconnectFrequencySeconds = 1;
    };
    evtSource.onerror = function(e) {
      evtSource.close();
      reconnectFunc();
    };
}
setupEventSource();

11
投票

我注意到(至少在 Chrome 中)是,当您使用

close()
函数关闭 SSE 连接时,它不会尝试再次重新连接。

var sse = new EventSource("...");
sse.onerror = function() {
    sse.close();
};

11
投票

我重写了@Wade的解决方案,经过一些测试,我得出的结论是,功能保持不变,代码更少,可读性更好(imo)。

我不明白的一件事是,如果每次尝试重新连接时

timeout
变量都设置回
null
,为什么要清除超时。所以我就完全省略了它。我还省略了对
wait
参数是否为函数的检查。我只是假设是这样,所以它使代码更清晰。

var reconnectFrequencySeconds = 1;
var evtSource;

// Putting these functions in extra variables is just for the sake of readability
var waitFunc = function() { return reconnectFrequencySeconds * 1000 };
var tryToSetupFunc = function() {
    setupEventSource();
    reconnectFrequencySeconds *= 2;
    if (reconnectFrequencySeconds >= 64) {
        reconnectFrequencySeconds = 64;
    }
};

var reconnectFunc = function() { setTimeout(tryToSetupFunc, waitFunc()) };

function setupEventSource() {
    evtSource = new EventSource("url"); 
    evtSource.onmessage = function(e) {
      console.log(e);
    };
    evtSource.onopen = function(e) {
      reconnectFrequencySeconds = 1;
    };
    evtSource.onerror = function(e) {
      evtSource.close();
      reconnectFunc();
    };
}

setupEventSource();

7
投票

我以与您相同的方式阅读标准,但即使没有,也需要考虑浏览器错误、网络错误、服务器死机但保持套接字打开等。因此,我通常在重新连接SSE提供的。

在客户端,我使用几个全局变量和一个辅助函数来实现:

var keepaliveSecs = 20;
var keepaliveTimer = null;

function gotActivity() {
  if (keepaliveTimer != null) {
    clearTimeout(keepaliveTimer);
  }
  keepaliveTimer = setTimeout(connect,keepaliveSecs * 1000);
}

然后我就在

gotActivity()
上方拨打
connect()
,然后每次收到消息。 (
connect()
基本上只是打电话给
new EventSource()

在服务器端,它可以在正常数据流之上每 15 秒吐出一个时间戳(或其他内容),或者使用计时器本身并在正常数据流安静时吐出时间戳(或其他内容) 15 秒。


5
投票

这是人们可能喜欢的另一种变体

let events = null;

function connect() {
    events = new EventSource("/some/url");
    events.onerror = function() {
        events.close();
    }
}
connect();

let reconnecting = false;
setInterval(() => {
    if (events.readyState == EventSource.CLOSED) {
        reconnecting = true;
        console.log("reconnecting...");
        connect();
    } else if (reconnecting) {
        reconnecting = false
        console.log("reconnected!");
    }
}, 3000);

4
投票

在我当前的 Node.js 应用程序开发中,我注意到 Chrome 会在我的应用程序重新启动时自动重新连接,但 Firefox 不会。

ReconnectingEventSource,一个

EventSource
包装器,是我发现的最简单的解决方案。

可以使用或不使用您选择的 Polyfill。


1
投票

正如有人已经提到的,不同的浏览器根据返回代码执行不同的操作。我所做的只是关闭连接,然后检查服务器运行状况以确保其再次启动。我认为如果我们实际上不知道服务器/代理是否已经回来,尝试重新打开流是愚蠢的。

在 FF 和 Chrome 中测试:

let sseClient

function sseInit() {
  console.log('SSE init')
  sseClient = new EventSource('/server/events')
  sseClient.onopen = function () { console.log('SSE open ') }
  sseClient.onmessage = onMessageHandler
  sseClient.onerror = function(event) {
    if (event.target.readyState === EventSource.CLOSED) {
      console.log('SSE closed ' + '(' + event.target.readyState + ')')
    } else if (event.target.readyState === EventSource.CONNECTING) {
      console.log('SSE reconnecting ' + '(' + event.target.readyState + ')')
      sseClient.close()
    }
  }
}

sseInit()

setInterval(function() {
  let sseOK
  if (sseClient === null) {
    sseOK = false
  } else {
    sseOK = (sseClient.readyState === EventSource.OPEN)
  }
  if (!sseOK) {
    // only try reconnect if server health is OK
    axios.get('/server/health')
      .then(r => {
        sseInit()
        store.commit('setServerOK_true')
      })
      .catch(e => {
        store.commit('setServerOK_false')
        sseClient = null
      })
  }
}, 5000)

注意,我将 Vue 与 ECMAScript 结合使用并跟踪商店中的状态,因此有些事情可能不会立即有意义。

© www.soinside.com 2019 - 2024. All rights reserved.