Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
RPC 故障排查12 分钟阅读

Ethereum WebSocket RPC 断开:原因及使用 ethers.js 和 web3.js 重连

了解为什么 Ethereum WebSocket RPC 连接会断开,以及如何使用 ethers.js 和 web3.js 实现健壮的重连逻辑,包含可运行的代码示例。

TL;DR

Ethereum WebSocket RPC 连接会因空闲超时、连接数限制和网络问题而断开。本文解释了底层机制,并提供了 ethers.js 和 web3.js 的重连模式,包括可运行示例和诊断步骤。

直接回答:为什么你的 Ethereum WebSocket RPC 总是断开

如果你正在使用 Ethereum WebSocket RPC 端点并频繁遇到断开问题,根本原因通常是以下三种之一:提供商或负载均衡器强制执行的空闲超时、连接数限制或网络不稳定。解决方案是实现一个健壮的重连策略,自动重新订阅你感兴趣的事件。本文解释了通过 WSS 使用 eth_subscribe 的机制、连接断开的原因,以及如何使用 ethers.js 和 web3.js 构建弹性客户端。

简而言之,WebSocket 连接不是永久的;它们受制于超时和资源限制。通过理解这些约束并编写重连代码,你可以维护可靠的区块链数据流。

Ethereum WebSocket 订阅的工作原理

Ethereum 节点暴露了一个支持 eth_subscribe 方法的 WebSocket JSON-RPC API。该方法允许客户端订阅实时事件,如新区块头、待处理交易和日志。节点会发送一个订阅 ID,然后在事件发生时推送通知。

WebSocket 连接是一个持久的 TCP 连接,带有 WebSocket 升级。客户端和服务器都可以关闭它。服务器(节点或其前面的代理)可能因不活动、资源限制或维护而关闭连接。客户端也可能因网络变化或应用程序逻辑而关闭它。

例如,geth 的 WebSocket 服务器有 --ws 启用它,--ws.addr 绑定地址,--ws.port--ws.origins 限制允许的来源。默认的 --ws.originslocalhost,如果你的客户端来源不被允许,可能会导致断开。此外,geth 对 WebSocket 连接有一个空闲超时,不能通过 CLI 配置,但在某些版本中设置为 60 秒。这意味着如果 60 秒内没有发送消息,服务器可能会关闭连接。这是不频繁的订阅(如安静网络上的待处理交易)断开的常见原因。

更多细节,请参阅 geth JSON-RPC 文档

WebSocket 断开的常见原因

有几个因素可能导致你的 Ethereum WebSocket RPC 连接断开:

  1. 空闲超时:许多提供商和负载均衡器会在一段时间(例如 60 秒)后关闭空闲连接。如果你的订阅没有频繁收到事件,连接可能会被关闭。

  1. 最大连接数:节点和提供商限制每个 IP 或每个客户端的并发 WebSocket 连接数。超过此限制可能导致新连接被拒绝或现有连接被丢弃。

  1. 提供商维护:基础设施提供商可能重启节点或执行维护,导致所有连接断开。

  1. 网络问题:不稳定的互联网连接、防火墙或 NAT 超时也可能终止 WebSocket 连接。

  1. 客户端问题:代码中的错误,例如未处理 ping/pong 帧,可能导致连接被视为死亡。

  1. geth WebSocket 设置:如前所述,--ws.origins 和空闲超时会影响连接稳定性。

ethers.js 中的重连模式

ethers.js 提供了一个 WebSocketProvider 类,它包装了 WebSocket 连接。但是,它不会自动重连。你需要自己实现重连逻辑。推荐的模式是监听 close 事件,然后尝试使用指数退避重连。

这是一个可运行的示例,演示了一个简单的重连循环:

const { ethers } = require('ethers');

const WS_URL = 'wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'; // 替换为你的端点

let provider;
let shouldReconnect = true;
let retryCount = 0;
const maxRetries = 10;

function createProvider() {
  provider = new ethers.WebSocketProvider(WS_URL);

  provider._websocket.on('close', (code, reason) => {
    console.log(`WebSocket closed: code=${code}, reason=${reason}`);
    if (shouldReconnect) {
      const delay = Math.min(1000 * 2 ** retryCount, 30000); // 指数退避
      retryCount++;
      console.log(`Reconnecting in ${delay}ms...`);
      setTimeout(createProvider, delay);
    }
  });

  provider._websocket.on('error', (error) => {
    console.error('WebSocket error:', error);
  });

  // 重连后重新订阅事件
  provider.on('block', (blockNumber) => {
    console.log('New block:', blockNumber);
  });
}

createProvider();

// 优雅关闭
process.on('SIGINT', () => {
  shouldReconnect = false;
  provider.destroy();
  process.exit();
});

在这个示例中,我们在关闭时创建一个新的 provider,并重新订阅 'block' 事件。注意,我们使用 provider._websocket 来访问底层的 WebSocket 对象,这在 ethers.js v5 中未正式记录但有效。在 ethers.js v6 中,API 可能不同;请查阅文档。

另一种方法是使用 WebSocketProvideron('error') 事件来检测连接问题并触发重连。但是,close 事件在检测断开方面更可靠。

web3.js 中的重连模式

web3.js 也提供了一个 WebSocket 提供者,但它有内置的重连选项。创建 Web3 实例时,你可以传递一个 WebsocketProvider,其 clientConfig 包含 reconnectdelay 选项。

示例:

const Web3 = require('web3');

const WS_URL = 'wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'; // 替换

const provider = new Web3.providers.WebsocketProvider(WS_URL, {
  clientConfig: {
    // 启用自动重连
    reconnect: {
      auto: true,
      delay: 5000, // 毫秒
      maxAttempts: 10,
      onTimeout: false
    }
  }
});

const web3 = new Web3(provider);

// 订阅新区块头
const subscription = web3.eth.subscribe('newBlockHeaders', (error, result) => {
  if (error) console.error(error);
  console.log('New block header:', result);
});

// 处理提供者错误
provider.on('error', (error) => {
  console.error('Provider error:', error);
});

provider.on('connect', () => {
  console.log('Connected');
});

provider.on('end', () => {
  console.log('Connection ended');
});

web3.js 中的 reconnect 选项处理自动重连,但你可能仍然需要在重连后重新订阅事件。connect 事件可用于重新建立订阅。

请注意,web3.js 的自动重连可能不会自动重新订阅;你需要在 connect 事件中处理。

事件驱动的重新订阅模式

一个健壮的模式是将连接逻辑与订阅逻辑分离。在每次(重新)连接时,你应该重新订阅所有期望的事件。这确保了重连后不会丢失任何数据。

这是一个概念模式:

let subscriptions = [];

function setupSubscriptions(provider) {
  // 清除现有订阅
  subscriptions.forEach(sub => sub.unsubscribe());
  subscriptions = [];

  // 订阅新区块
  const sub = provider.on('block', (blockNumber) => {
    console.log('New block:', blockNumber);
  });
  subscriptions.push(sub);

  // 订阅日志(示例)
  const filter = { address: '0x...' };
  const logSub = provider.on(filter, (log) => {
    console.log('Log:', log);
  });
  subscriptions.push(logSub);
}

// 每次连接后调用 setupSubscriptions

这种模式确保重连后所有订阅都重新建立。它还允许你集中管理订阅。

诊断:获取最近区块并记录关闭代码

要诊断 WebSocket 连接断开的原因,你可以编写一个脚本,获取最近的区块并记录关闭代码和原因。这有助于识别断开是由于空闲超时、服务器关闭还是其他原因。

这是一个使用 ethers.js 的可运行诊断脚本:

const { ethers } = require('ethers');

const WS_URL = 'wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'; // 替换

const provider = new ethers.WebSocketProvider(WS_URL);

provider._websocket.on('close', (code, reason) => {
  console.log(`Close code: ${code}`);
  console.log(`Close reason: ${reason}`);
  // 常见代码:1000(正常),1006(异常),1011(服务器错误)
});

provider._websocket.on('error', (error) => {
  console.error('Error:', error);
});

// 获取最近的区块
async function fetchRecentBlocks() {
  const latest = await provider.getBlockNumber();
  console.log('Latest block:', latest);
  for (let i = latest; i > latest - 5; i--) {
    const block = await provider.getBlock(i);
    console.log(`Block ${i}: timestamp=${block.timestamp}`);
  }
}

fetchRecentBlocks().catch(console.error);

// 保持进程存活
setInterval(() => {}, 1000);

运行此脚本并观察关闭代码。如果你看到代码 1006,表示连接异常关闭,可能是由于网络问题或服务器超时。如果你看到 1000,则是正常关闭,可能是由于空闲超时。

你还可以监控消息之间的时间,看看连接是否在一段时间不活动后断开。

常见失败和修复

以下是常见问题及其修复:

  1. 空闲超时:定期发送 ping 帧或使用生成频繁事件的订阅。一些提供商允许你设置自定义的 keep-alive 间隔。

  1. 最大连接数:确保你没有从同一 IP 打开多个连接。使用单个连接并多路复用订阅。

  1. 提供商维护:使用指数退避和抖动实现重连,以避免压倒服务器。

  1. geth origins:如果你运行自己的 geth 节点,设置 --ws.origins* 或你的特定来源,以避免连接被拒绝。

  1. 客户端错误:确保正确处理 pingpong 帧。大多数 WebSocket 库会自动处理,但如果你使用原始 WebSocket,则需要实现它。

  1. 网络问题:使用可靠的互联网连接,并考虑使用处理重连的 WebSocket 代理。

权衡和限制

重连逻辑增加了应用程序的复杂性。你需要处理重新订阅、避免重复事件和管理状态。此外,自动重连可能不适合所有用例,例如当你需要按顺序处理事件且没有间隙时。

另一个限制是 WebSocket 连接在请求-响应模式上不如 HTTP 可靠。如果你只需要偶尔的数据,考虑使用 HTTP JSON-RPC。

另外,请注意,一些提供商在 WebSocket 超时方面可能有不同的行为。始终查看提供商的文档以了解特定设置。

后续步骤和进一步阅读

既然你了解了 Ethereum WebSocket 断开的原因和解决方案,你可以在应用程序中实现健壮的重连策略。对于更高级的模式,考虑使用像 reconnecting-websocket 或带有内置重连的 ws 这样的库。

如果你正在寻找可靠的 Ethereum RPC 提供商,请查看 OnFinality 的 Ethereum 网络页面 和我们的 RPC 助手 以找到最适合你需求的端点。我们的 API 服务 提供强大的 WebSocket 支持,并自动处理重连。

更多故障排查技巧,请参阅我们的 通用 WebSocket RPC 断开修复 指南。别忘了查看 geth JSON-RPC 文档 了解服务器端设置。

探索 OnFinality Learn 上的更多文章,加深你对区块链基础设施的理解。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始