我正在开发一个利用 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();
服务器端事件在所有浏览器中的工作方式都不同,但它们都会在某些情况下关闭连接。例如,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();
我注意到(至少在 Chrome 中)是,当您使用
close()
函数关闭 SSE 连接时,它不会尝试再次重新连接。
var sse = new EventSource("...");
sse.onerror = function() {
sse.close();
};
我重写了@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();
我以与您相同的方式阅读标准,但即使没有,也需要考虑浏览器错误、网络错误、服务器死机但保持套接字打开等。因此,我通常在重新连接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 秒。
这是人们可能喜欢的另一种变体
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);
在我当前的 Node.js 应用程序开发中,我注意到 Chrome 会在我的应用程序重新启动时自动重新连接,但 Firefox 不会。
EventSource
包装器,是我发现的最简单的解决方案。
可以使用或不使用您选择的 Polyfill。
正如有人已经提到的,不同的浏览器根据返回代码执行不同的操作。我所做的只是关闭连接,然后检查服务器运行状况以确保其再次启动。我认为如果我们实际上不知道服务器/代理是否已经回来,尝试重新打开流是愚蠢的。
在 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 结合使用并跟踪商店中的状态,因此有些事情可能不会立即有意义。