进入本文博客正题之前,或者对EventSource完全还没了解之前,可以简单阅读一下下面这篇博客https://blog.csdn.net/qq_44327851/article/details/135157086

        通过对EventSource的简单学习之后,我们很容易就能发现EventSource其中一个特性就是——自动重连,其中它还提供了一个retry字段来设置重连的时间间隔。那具体该如何使用呢?又该如何实现来实现重连呢?

首先需要纠正大家一个误区!!!:

        EventSource(SSE)本身并没有提供自动重连的机制,它所谓的自动重连特性是指浏览器自动处理与服务器的连接断开并尝试重新连接的过程。

EventSource retry 重连字段的介绍:

        当使用 EventSource 建立连接后,如果连接中断,浏览器会自动尝试重新连接服务器。这意味着浏览器会在连接中断后自动发起新的连接请求,以尝试重新建立与服务器的连接,以确保数据的实时传输。其中EventSource 对象提供了 retry 字段用于指定在连接中断后重新连接的时间间隔(以毫秒为单位)。当连接中断后,浏览器会根据 retry 字段的值来确定重新连接的时间间隔。

retry 字段的使用方法如下:

        下面的示例中,我们将 eventSource 对象的 retry 字段设置为 3000,表示在连接中断后,浏览器会每隔 5 秒尝试重新连接服务器一次。

let eventSource = new EventSource('your_event_source_url');
eventSource.retry = 5000; // 设置重新连接的时间间隔为3秒(3000毫秒)

        需要注意的是,retry 字段是可选的,如果不设置 retry 字段,浏览器会使用默认的重新连接时间间隔。另外,一些服务器端也可能会忽略 retry 字段,因此在实际使用中需要根据具体情况进行调整。总之,通过设置 retry 字段,开发者可以控制浏览器在连接中断后重新连接的时间间隔,以满足实际应用的需求。

        但是浏览器在连接中断后会自动尝试重新连接,这种重连机制并不是完全可靠的,有时候可能会出现连接失败的情况。这个时候我们需要手动干预重连。

浏览器自动重连注意事项:

  1. EventSource 对象的默认重新连接时间间隔是 3 秒。这意味着如果未显式设置 retry 字段,浏览器会在连接中断后每隔 3 秒尝试重新连接服务器一次。
  2.  在浏览器的开发者工具中的 Network 面板通常不会显示 EventSource 的重新连接过程,因为 EventSource 是基于长轮询机制的,浏览器会在后台自动处理重新连接过程,而不会在 Network 面板中显示每次重新连接的请求。
  3. 在浏览器中,EventSource 对象会自动处理网络连接的断开和重连,以确保与服务器的连接保持活动状态。当连接断开时,浏览器会自动尝试重新连接,这个过程是由浏览器内部处理的,不会触发 EventSource 对象的 onerror 或 onclose 事件。
  4. 如果想要观察 EventSource 的连接情况,可以在控制台中输出相关信息或者利用事件监听来捕获连接状态的变化。通过监听 EventSource 对象的事件(如 onopen、onerror 等),可以观察连接的建立和断开情况,以及根据需要执行相应的重连逻辑

手动重连实现:

       这里就是来专门解释注意事项的第三点(捕捉EventSource的相关信息从而进行重连),也就是我们编写相关代码去参与干预重连机制。在这里强调一点:进行手动重连之前,请务必保证之前的SSE连接已经断开!!!

实现方法:

  1. 使用错误处理机制:在 eventSource 连接中断后,可以通过监听 error 事件来捕获连接错误,并在错误处理函数中尝试重新连接。例如,可以在 error 事件处理函数中调用 eventSource 的 close 方法关闭连接,然后再调用 eventSource 的 open 方法重新建立连接。
    let eventSource = new EventSource('your_event_source_url');
    
    eventSource.addEventListener('error', function() {
      eventSource.close();
      eventSource = new EventSource('your_event_source_url');
    });
  2. 使用手动重连按钮:在 eventSource 连接中断后,可以显示一个按钮供用户手动触发重新连接操作。例如,可以在页面上添加一个按钮,并在按钮点击事件中调用 eventSource 的 close 方法关闭连接,然后再调用 eventSource 的 open 方法重新建立连接。这种方法可以让用户自主决定何时重新连接,增加了灵活性。
    let eventSource = new EventSource('your_event_source_url');
    let reconnectButton = document.getElementById('reconnectButton');
    
    reconnectButton.addEventListener('click', function() {
      eventSource.close();
      eventSource = new EventSource('your_event_source_url');
    });

示例: 

        EventSource 中断后进行重连,但是重连次数不超过 3 次,并且每次重连间隔为 6 秒。

let eventSource;
let reconnectCount = 0;
const maxReconnectAttempts = 3;
const reconnectInterval = 6000; // 6 秒

function connectEventSource() {
  eventSource = new EventSource('your_event_source_url');

  eventSource.onopen = function(event) {
    console.log('Connection opened');
    reconnectCount = 0; // 重置重连次数
  };

  eventSource.onerror = function(event) {
    console.error('Connection error:', event);
    if (reconnectCount < maxReconnectAttempts) {
      reconnectCount++;
      console.log(`Reconnecting attempt ${reconnectCount} in 6 seconds...`);
      setTimeout(() => {
        connectEventSource();
      }, reconnectInterval);
    } else {
      console.log('Exceeded maximum reconnection attempts.');
      eventSource.close(); // 关闭 EventSource 连接
    }
  };
}

connectEventSource();

但是在示例中其实有一个特别重要的点,我们是没有考虑到的:

        如果在代码中实现了自定义的重连逻辑(如上面的示例代码),浏览器的自动重连机制和自定义重连逻辑可能会冲突,相互干扰,导致重连次数超过预期。因此当在 onerror 事件处理程序中编写重连逻辑时,可能会导致浏览器和服务器之间的 EventSource 连接频繁断开和重连,从而在网络面板中出现大量的 SSE 连接。

解决办法:

  1. 使用 eventSource.readyState 属性来检查连接状态,避免重复重连:
    let eventSource = new EventSource('your_endpoint');
    
    eventSource.onopen = () => {
      console.log('Connection established');
    };
    
    eventSource.onerror = () => {
      if (eventSource.readyState === EventSource.CLOSED) {
        console.log('Connection closed, will not attempt to reconnect');
      }
    };
  2. 结合自己项目所使用的UI框架的生命周期来实现,这里将展示Angular框架的实现:
     reConnectHttp = false;
     preReConnectHttp:boolean;
     reConnectHttpCount = 1;
     httpUrl:string;// your sse url
     source:any;
    
     ngOnInit() {
        this.connectEventSource(this.httpUrl);
     }
      
     ngDoCheck() {
        if(this.reConnectHttp != this.preReConnectHttp) {
          this.preReConnectHttp = this.reConnectHttp;
        }else {
          return;
        }
        if(this.reConnectHttp && this.source readyState === EventSource.CLOSED) {) {
          if(this.reConnectHttpCount > 1 && this.reConnectHttpCount <=3) {
            this.reConnectHttpCount++;
            setTimeout(() => {
              this.connectEventSource(this.httpUrl);
            },3000);
          }else if(this.reConnectHttpCount === 1){
            this.reConnectHttpCount++;
            this.connectEventSource(this.httpUrl);
          }
        }
      }
    
      /**
       * connect event source.
       */
      connectEventSource(url:string) {
        this.messages = [];
        this.messagesDetails = _.cloneDeep([]);
        this.reConnectHttp = false;
        this.preReConnectHttp = false;
        let that = this;
        this.ctrl = new AbortController();
        let t1 = new Date().getTime();
        this.source = fetchEventSource(url, {
          method: 'GET',
          headers: {
           'Content-Type': 'application/json',
           'Accept':'text/event-stream'
          },
          // body: JSON.stringify({}),//{} body params
          signal: this.ctrl.signal,
          openWhenHidden: true, // This is important to avoid the connection being closed when the tab is not active
          async onopen(response) {
            let t2 = new Date().getTime();
            if (response.ok && response.headers.get('content-type') === 'text/event-stream') {
              console.log('eventSource Start:', t2, ', diff:', t2 - t1);
              that.reConnectHttpCount = 1;
            } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
              console.log('eventSource Request error!', t2, ', diff:', t2 - t1);
              that.reConnectHttp = true;
            } else {
              console.log('eventSource Other error!', t2, ', diff:', t2 - t1);
              that.reConnectHttp = true;
            }
          },
          async onmessage(event) {
            let t3 = new Date().getTime();
            console.log('--eventSource data--', event, t3, ', diff:', t3 - t1);
            event.data && that.messageRecieved(event.data);
          },
          onerror(error) {
            let t4 = new Date().getTime();
            console.error('eventSource Error:', error, t4, ', diff:', t4 - t1);
            that.ctrl.abort();
            that.reConnectHttp = true;
            throw error;
          },
          async onclose() {
            let t5 = new Date().getTime();
            console.log('eventSource Close connection', t5, ', diff:', t5 - t1);
            that.ctrl.abort();
            that.reConnectHttp = true;
            // if the server closes the connection unexpectedly, retry:
            return;
          }
        }).then((response) => {
            console.log('--eventSource response--', response);
        }).then((data) => console.log('--eventSource then data--', data)).catch((error) => console.error('eventSource Error:', error));
      }
  3. 在 onerror 事件处理程序中添加延迟重连,以避免过于频繁地尝试重连:
    let eventSource = new EventSource('your_endpoint');
    let reconnectTimeout = 5000; // 5 seconds
    
    eventSource.onerror = () => {
      setTimeout(() => {
        eventSource.close();
        eventSource = new EventSource('your_endpoint');
      }, reconnectTimeout);
    };
  4. 使用 eventSource.onclose 事件处理程序来处理连接关闭的情况,而不是仅依赖于 onerror
    let eventSource = new EventSource('your_endpoint');
    
    eventSource.onopen = () => {
      console.log('Connection established');
    };
    
    eventSource.onerror = () => {
      console.log('Connection error');
    };
    
    eventSource.onclose = () => {
      console.log('Connection closed');
      // Optionally, you can attempt to reconnect here
    };

By the Way: 

        readyState属性:检查 EventSource 对象的连接状态,只读属性,可以返回当前连接的状态,具体取值如下:

  • EventSource.CONNECTING (0): 表示连接正在建立中。
  • EventSource.OPEN(1): 表示连接已经建立并处于打开状态。
  • EventSource.CLOSED(2): 表示连接已经关闭。

        我们可以根据 readyState的值来执行相应的逻辑,以确保连接的稳定性和正确性。以下是一个示例代码:

function fetchEventSource(url) {
  let eventSource = new EventSource(url);

  // Check the readyState of the EventSource object
  if (eventSource.readyState === EventSource.CONNECTING) {
    console.log('Connection is in the process of being established');
  } else if (eventSource.readyState === EventSource.OPEN) {
    console.log('Connection is open');
  } else if (eventSource.readyState === EventSource.CLOSED) {
    console.log('Connection is closed');
  }

  // Handle other EventSource events as needed
  eventSource.onopen = () => {
    console.log('Connection established');
  };

  eventSource.onerror = () => {
    console.log('Connection error');
  };

  eventSource.onclose = () => {
    console.log('Connection closed');
  };

  return eventSource;
}

// Example usage of fetchEventSource method
let url = 'your_endpoint';
let eventSource = fetchEventSource(url);
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐