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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/24564030/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-28 03:03:03  来源:igfitidea点击:

Is an EventSource (SSE) supposed to try to reconnect indefinitely?

javascriptgoogle-chromefirefoxserver-sent-events

提问by rhyek

I'm working on a project utilizing Server-Sent-Events and have just run into something interesting: connection loss is handled differently between Chrome and Firefox.

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

On Chrome 35 or Opera 22, if you lose your connection to the server, it will try to reconnect indefinitely every few seconds until it succeeds. On Firefox 30, on the other hand, it will only try once and then you have to either refresh the page or handle the error event raised and manually reconnect.

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

I much prefer the way Chrome or Opera does it, but reading http://www.w3.org/TR/2012/WD-eventsource-20120426/#processing-model, it seems as though once the EventSource tries to reconnect and fails due to a network error or other, it shouldn't retry the connection. Not sure if I'm understanding the spec correctly, though.

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

I was set on requiring Firefox to users, mostly based on the fact that you can't have multiple tabs with an event stream from the same URL open on Chrome, but this new finding would probably be more of an issue. Although, if Firefox behaves according to spec then I might as well work around it somehow.

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

Edit:

编辑:

I'm going to keep targeting Firefox for now. This is how I'm handling reconnections:

我现在将继续以 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();

回答by Darren Cook

I read the standard the same way as you but, even if not, there are browser bugs to consider, network errors, servers that die but keep the socket open, etc. Therefore, I usually add a keep-alive on top of the re-connect that SSE provides.

我以与您相同的方式阅读标准,但即使不是,也有需要考虑的浏览器错误、网络错误、服务器死机但保持套接字打开等。因此,我通常在 re 之上添加一个 keep-alive -SSE 提供的连接。

On the client-side I do it with a couple of globals and a helper function:

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

var keepaliveSecs = 20;
var keepaliveTimer = null;

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

Then I call gotActivity()at the top of connect(), and then every time I get a message. (connect()basically just does the call to new EventSource())

然后我gotActivity()在顶部调用connect(),然后每次收到消息。(connect()基本上只是调用new EventSource()

On the server-side, it can either spit out a timestamp (or something) every 15 seconds, on top of normal data flow, or use a timer itself and spit out a timestamp (or something) if the normal data flow goes quiet for 15 seconds.

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

回答by Wade

Server Side Events work differently in all of the browsers, but they all close the connection during certain circumstances. Chrome, for example, closes the connection on 502 errors while a server is restarted. So, it is best to use a keep-alive as others suggest or reconnect on every error. Keep-alive only reconnect at a specified interval that must be kept long enough to avoid overwhelming the server. Reconnecting on every error has the lowest possible delay. However, it is only possible if you take an approach that keeps server load to a minimum. Below, I demonstrate an approach that reconnects at a reasonable rate.

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

This code uses a debounce function along with reconnect interval doubling. It works well, connecting at 1 second, 4, 8, 16...up to a maximum of 64 seconds at which it keeps retrying at the same rate. I hope this helps some people.

此代码使用去抖动功能以及重新连接间隔加倍。它运行良好,以 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();

回答by Martin

What I've noticed (in Chrome at least) is that when you close your SSE connection using close()function, it won't try to reconnect again.

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

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

回答by TomCB

Unfortunately my reputation points are still too low to make a comment under the postof @Wade. I am asking myself why his/your answer is so complicated.

不幸的是,我的声誉点仍然太低,无法在@Wade的帖子下发表评论。我在问自己为什么他/你的答案如此复杂。

I rewrote the solution and after a little bit of testing I came to the conclusion that the functionality stayed the same with less code and better readability (imo). However I am still a beginner and I would love to get feedback from you. I am not sure if I am missing something crucial.

我重写了解决方案,经过一些测试后,我得出的结论是,功能保持不变,代码更少,可读性更好(imo)。然而,我仍然是一个初学者,我很想得到你的反馈。我不确定我是否遗漏了一些关键的东西。

One thing I did not understand was, why you clear the Timeout if the timeoutvariable gets set back to nullevery time you try to reconnect. So I just omitted it completely. And I also omitted the check if the waitargument is a function. I just assume it is, so it makes the code cleaner.

我不明白的一件事是,如果每次尝试重新连接时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();

回答by lazieburd

here's another variation folks might like

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

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

回答by MikeM

As someone already mentioned different browsers do different things depending on the return code. What I do instead is just close the connection regardless then check server health to make sure its up again. I think its silly trying to re-open a stream if we don't actually know if the server/proxy is back yet.

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

Tested in FF and Chrome:

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

Note, I am using Vue with ECMAScript and tracking state in a store so some things might not make immediate sense.

请注意,我使用 Vue 和 ECMAScript 并跟踪商店中的状态,因此有些事情可能没有立即意义。