一个事件源(SSE)是否应该无限尝试重新连接?

52

我正在开发一个利用服务器发送事件的项目,最近遇到了一个有趣的问题:在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();
7个回答

20

服务器端事件在所有浏览器中都有不同的工作方式,但它们在特定情况下都会关闭连接。例如,Chrome在服务器重新启动时关闭502错误的连接。因此,最好像其他人建议的那样使用保持连接或在每个错误上重新连接。保持连接仅在必须保持足够长以避免对服务器造成过大压力的指定时间间隔重新连接。在每个错误上重新连接具有可能的最低延迟。但是,只有采取最小化服务器负载的方法才能实现这一点。下面,我演示了一种以合理速率重新连接的方法。

此代码使用Debounce函数和重连间隔加倍。它可以很好地工作,在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 even here
    };
    evtSource.onopen = function(e) {
      // Reset reconnect frequency upon successful connection
      reconnectFrequencySeconds = 1;
    };
    evtSource.onerror = function(e) {
      evtSource.close();
      reconnectFunc();
    };
}
setupEventSource();

我本来想说你在Chrome和Safari中创建了多个EventSource连接,但是后来我再次检查了你的代码,发现你确保在创建新连接之前关闭当前连接。非常好! - Lucio Paiva
1
如果您关闭连接,那么下一次连接不会丢失last_event_id吗? - Dr Fred
@DrFred,也许最后一个事件ID丢失了。我没有尝试过。这段代码的整个目的是解决浏览器在连接丢失时无法重新连接的问题。最后一个事件ID应该有助于透明地重新发送消息,但整个机制不太好用,并且在每个浏览器中的工作方式都不同。因此,我不使用最后一个事件ID。它是不必要的。 - Mr Z

10

我注意到(至少在Chrome中),当您使用close()函数关闭SSE连接时,它不会尝试重新连接。

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

9

我改写了@Wade的解决方案,经过一些测试,我得出结论:功能保持不变,代码更简洁易读(我认为)。

有一件事我没有理解,为什么在每次尝试重新连接时timeout变量被设置回null后要清除Timeout。所以我完全省略了它。我也省略了检查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();

嗨,汤姆,你的代码与我使用的去抖函数不同。去抖函数是一个速率限制器,不仅仅是将超时时间加倍。你的代码会在每次错误时不断将超时时间加倍,这很快就会达到最大值64。在谷歌上搜索“去抖函数”。 - Mr Z
嗨,Wade,感谢您的回复。实际上,我编写的不是一个防抖函数,而是一种通过递增延迟来限制重新连接尝试的方法。我认为没有必要使用防抖函数,因为它是一个闭环,即没有外部函数重复调用“reconnectFunc”,它只在连接错误后被调用。是的,由于我的实现不符合防抖函数的定义,它会在每次连接丢失时增加频率/超时时间,但在成功重新连接后,它会被设置回1秒,正如我们所希望的那样。 - Tom Böttger

7

我和你一样地读了这个标准,但即使不是那样,也需要考虑浏览器的错误、网络错误、保持套接字打开但服务器挂掉等问题。因此,我通常在SSE提供的重新连接之上添加一个keep-alive。

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

var keepaliveSecs = 20;
var keepaliveTimer = null;

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

connect()的顶部调用gotActivity(),每当我收到消息时都这样做。(connect()基本上只是调用new EventSource()

在服务器端,可以在正常数据流之上每15秒钟输出一个时间戳(或其他内容),或者使用计时器,如果正常数据流静默了15秒钟,则输出一个时间戳(或其他内容)。

4

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

ReconnectingEventSource是最简单的解决方案,它是一个EventSource包装器。

可以与任何您选择的polyfill一起使用或不使用。


3

这里有一个人们可能会喜欢的变化

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

1

正如其他人已经提到的,不同的浏览器根据返回代码做出不同的反应。相反,我会直接关闭连接,然后检查服务器的健康状况以确保它再次运行。如果我们实际上不知道服务器/代理是否已经恢复,那么尝试重新打开流是徒劳的。

在Firefox和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,并在存储中跟踪状态,因此某些内容可能不会立即让人理解。

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