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

前端 未结 6 1989
余生分开走
余生分开走 2021-01-30 09:22

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.

O

相关标签:
6条回答
  • 2021-01-30 09:37

    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.

    var sse = new EventSource("...");
    sse.onerror = function() {
        sse.close();
    };
    
    0 讨论(0)
  • 2021-01-30 09:45

    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.

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

    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.

    0 讨论(0)
  • 2021-01-30 09:46

    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);
    
    0 讨论(0)
  • 2021-01-30 09:47

    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.

    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.

    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();
    
    0 讨论(0)
  • 2021-01-30 09:50

    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:

    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.

    0 讨论(0)
  • 2021-01-30 09:52

    I rewrote the solution of @Wade 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.

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

    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();
    
    0 讨论(0)
提交回复
热议问题