Должен ли EventSource (SSE) бесконечно пытаться повторно подключаться?

Я работаю над проектом, использующим 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 от пользователей, в основном из-за того, что вы не можете иметь несколько вкладок с потоком событий с одного и того же URL-адреса, открытого в Chrome, но это новое открытие, вероятно, будет более серьезной проблемой. Хотя, если 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();

person rhyek    schedule 03.07.2014    source источник


Ответы (7)


События на стороне сервера работают по-разному во всех браузерах, но все они закрывают соединение при определенных обстоятельствах. Chrome, например, закрывает соединение при ошибке 502 при перезапуске сервера. Таким образом, лучше всего использовать поддержку активности, как предлагают другие, или переподключаться при каждой ошибке. 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();
person Wade    schedule 27.01.2019
comment
Я собирался сказать, что вы создаете несколько соединений EventSource в Chrome и Safari, но затем я снова проверил ваш код, и вы обязательно закрываете текущее соединение, прежде чем создавать новое. Хорошо! - person Lucio Paiva; 01.08.2019

Я читаю стандарт так же, как и вы, но, даже если нет, нужно учитывать ошибки браузера, сетевые ошибки, серверы, которые умирают, но держат сокет открытым и т. д. Поэтому я обычно добавляю поддержку активности поверх re -connect, который предоставляет 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 секунд.

person Darren Cook    schedule 07.07.2014

Что я заметил (по крайней мере, в Chrome), так это то, что когда вы закрываете соединение SSE с помощью функции close(), оно не будет пытаться снова подключиться.

var sse = new EventSource("...");
sse.onerror = function() {
    sse.close();
};
person Martin    schedule 21.06.2016

Я переписал решение @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();
person Tom Böttger    schedule 10.04.2020

вот еще вариант, кому может понравиться

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);
person lazieburd    schedule 15.04.2020

Как уже упоминалось, разные браузеры делают разные вещи в зависимости от кода возврата. Вместо этого я просто закрываю соединение, а затем проверяю работоспособность сервера, чтобы убедиться, что он снова работает. Я думаю, что глупо пытаться повторно открыть поток, если мы на самом деле не знаем, вернулся ли сервер/прокси.

Протестировано в 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 и отслеживаю состояние в магазине, поэтому некоторые вещи могут не иметь непосредственного смысла.

person StartupGuy    schedule 25.04.2020

В моем текущем разработчике приложений Node.js я заметил, что Chrome автоматически переподключается при перезапуске моего приложения, а Firefox — нет.

ReconnectingEventSource, оболочка EventSource, является самым простым решением, которое я нашел.

Работает с полифиллом или без него по вашему выбору.

person Walter Monroe    schedule 08.07.2021