【问题标题】:Is an EventSource (SSE) supposed to try to reconnect indefinitely?EventSource (SSE) 是否应该无限期地尝试重新连接?
【发布时间】:2014-07-03 22:11:19
【问题描述】:

我正在开发一个使用 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


    【解决方案1】:

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

    此代码使用去抖动功能以及重新连接间隔加倍。它运行良好,以 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 连接,但后来我再次检查了您的代码,并且您确保在创建新连接之前关闭当前连接。好!
    • 如果关闭连接,下次连接不会丢失last_event_id吗?
    • @DrFred,可能最后一个事件 ID 丢失了。我还没有尝试过。这段代码的重点是解决连接丢失时浏览器无法重新连接的问题。最后一个事件 ID 应该有助于透明地重新发送消息,但整个机制并不能很好地工作,并且在每个浏览器中的工作方式都不同。所以,我不使用最后一个事件 ID。这是不必要的。
    【解决方案2】:

    我重写了@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();
    

    【讨论】:

    • 嗨,Tom,您的代码与我使用的 debounce 函数不同。 debounce 函数是一个速率限制器,不只是将超时时间加倍。您的代码将不断将每个错误的超时时间加倍,这将很快达到最大值 64。在 Google 上查找“去抖动功能”。
    • 您好韦德,感谢您的回复。事实上,我写的不是去抖动功能,它只是一种限制重新连接尝试的方法(通过增量延迟)。我相信不需要去抖函数,因为它是一个闭环,即没有外部函数重复调用reconnectFunc,它仅在连接错误后才被调用。是的,由于我的实现不是每个定义的去抖函数,它会在每次连接丢失时增加频率/超时,但它会在成功重新连接后设置回 1 秒,就像我们想要的那样。
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

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

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

      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 秒。

      【讨论】:

        【解决方案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);
        

        【讨论】:

          【解决方案6】:

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

          ReconnectingEventSourceEventSource 包装器,是我找到的最简单的解决方案。

          使用或不使用您选择的 polyfill。

          【讨论】:

            【解决方案7】:

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

            在 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 并在商店中跟踪状态,所以有些事情可能不会立即生效。

            【讨论】:

              猜你喜欢
              • 2017-03-12
              • 1970-01-01
              • 1970-01-01
              • 2013-05-27
              • 1970-01-01
              • 2014-03-16
              • 1970-01-01
              • 1970-01-01
              • 2020-12-29
              相关资源
              最近更新 更多