一文搞懂以太坊JSON-RPC:从基础原理到多链实战指南
2026/9/9 10:21:02 网站建设 项目流程

我最早接触以太坊开发时,最头疼的不是 Solidity 合约怎么写,而是跟节点打交道这件事。明明合约部署上去了,却不知道该怎么查数据、怎么发交易、怎么确认状态,翻文档翻得一头雾水。后来才发现,绝大多数跟链上交互的活,本质上都是在调一套叫 JSON-RPC 的接口。更关键的是,不仅以太坊主网用这套接口,几乎所有兼容 Ethereum 的链——BSC、Polygon、Arbitrum、Optimism,甚至各类 Layer 2 和测试网,都在沿用这套标准。理解了 JSON-RPC,就等于拿到了打开整个 EVM 生态大门的钥匙。

这篇文章我会从零开始拆解兼容 Ethereum 的 JSON-RPC 接口,讲清楚它是什么、核心方法怎么用、实战中怎么调试,以及我在多链开发中踩过的一些坑。不管你是刚入门的 Web3 开发者,还是准备做链上数据服务、钱包工具,这篇文章都能给你一份可以直接抄作业的参考。

1. JSON-RPC 是什么,为什么所有 EVM 链都在用它

1.1 先搞懂 RPC 的基本含义

RPC 的全称是 Remote Procedure Call,翻译过来就是“远程过程调用”。你在本地写代码时调用一个函数,那是本地调用;但如果你想让远端的程序帮你执行某个操作并返回结果,就需要通过网络发送请求,这就是远程调用。

JSON-RPC 是 RPC 的一种具体实现——用 JSON 格式来编码请求和响应。以太坊节点本身不提供像 MySQL 那样的 SQL 查询接口,也不提供像 RESTful API 那样的资源路径设计,它暴露的是一个 JSON-RPC 端点,通常是 HTTP 或 WebSocket 地址。你往这个端点发送一段 JSON 文本,节点解析后执行对应操作,再返回一段 JSON 文本。

这种设计听起来比 RESTful 简单粗暴,但却非常契合区块链节点的场景:节点需要支持的“操作”种类繁多且固定,用统一的 JSON 格式封装反而清晰直观。你可以直接拿 curl 命令去敲一个节点的 RPC 端点,立刻就能看到返回结果,调试体验很直接。

1.2 为什么 EVM 兼容链都继承这套接口

以太坊把 JSON-RPC 的接口规范定得很死,定义了从 eth_blockNumber 到 eth_call、eth_sendRawTransaction 这一整套标准方法。任何一条链,只要它想兼容 Ethereum 生态,就没办法避开这套接口。

原因也很实际。钱包(比如 MetaMask)需要连接节点才能读取用户余额、发送交易;区块浏览器需要从节点同步数据才能展示交易列表;DApp 前端需要调用合约方法才能实现业务逻辑。如果每条链都发明一套自己的接口,钱包和工具链就得为每条链单独适配,生态根本跑不起来。

所以你会发现,BSC、Polygon、Arbitrum、Optimism、Avalanche 这些链,它们的 RPC 端点请求格式几乎一模一样。你在以太坊主网上能用的 eth_ 开头方法,在这些链上基本都能直接用。唯一的区别是 chainId、区块时间和最终性规则这些链本身的特性参数不同。

1.3 JSON-RPC 请求和响应的基本结构

一个标准的 JSON-RPC 2.0 请求长这样:

{ "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] }
  • jsonrpc 固定为 "2.0",表示协议版本。
  • id 是你自己定义的请求标识,节点返回响应时会带上相同的 id,这样你就能把请求和响应对应起来。
  • method 是你要调用的方法名。
  • params 是该方法需要的参数数组或对象。

对应的响应分两种。成功的响应:

{ "jsonrpc": "2.0", "id": 1, "result": "0x10d4f" }

出错的响应:

{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32601, "message": "Method not found" } }

注意,result 和 error 不会同时出现。一旦返回了 error,说明这次调用整体失败了。

这里有一个新手容易忽略的点:以太坊节点返回的很多数值类型都是十六进制字符串,比如区块高度 "0x10d4f"。它不是十进制数字,也不是 JSON number,而是一个带 0x 前缀的十六进制字符串。很多人在第一次写代码时直接用 parseInt 去解析,结果得到 NaN,其实应该用 parseInt(result, 16) 或者交给对应的 SDK 去处理。

2. 核心接口方法拆解:从查询到交易全覆盖

2.1 链基础信息查询

先看几个最基础的,也是日常开发里用得最多的查询接口。

eth_blockNumber 用于获取当前最新区块高度。这个接口不需要任何参数,返回的是一个十六进制字符串。它是最基础的接口之一,因为很多业务逻辑都要以区块高度作为参考点,比如判断交易是否已经确认、计算某个时间点的链上状态等。

eth_chainId 用于获取当前链的 chainId。以太坊主网是 1,BSC 是 56,Polygon 是 137,Arbitrum One 是 42161。这个参数在签名交易时非常重要,因为要防止交易被重放到其他链上。

eth_gasPrice 返回当前网络的 gas 价格,单位是 wei。这个值会随着网络拥堵程度实时变化。发送交易前如果没有手动指定 gasPrice,钱包通常会调这个接口获取一个建议值。

eth_getBalance 用于查询地址余额。参数有两个:第一个是地址,第二个是区块高度,可以用 "latest"(最新已确认区块)、"pending"(包含待处理交易)、"earliest"(创世区块)或具体的十六进制区块号。这里我强烈建议刚开始用 latest,等理解透后再去折腾其他参数。

curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "eth_getBalance", "params": ["0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "latest"] }'

返回结果是一个十六进制字符串,单位是 wei。如果你不想被 wei 的零搞晕,可以在代码里用 ethers.js 的 formatEther 函数把 wei 转成 ETH。

2.2 交易相关的接口

eth_getTransactionByHash 是查询交易详情的核心接口,参数是交易哈希。返回结果包含这笔交易的 from、to、value、gas、gasPrice、nonce、input 数据等字段。如果交易不存在,返回 null。

eth_getTransactionReceipt 是查询交易回执的接口,参数也是交易哈希。回执里包含交易是否成功的状态(status,1 表示成功,0 表示失败)、实际消耗的 gas(gasUsed)、合约地址(如果是合约创建交易)、以及 logs 字段(事件日志)。

这两个接口配合使用可以判断一笔交易的最终状态。先通过 getTransactionByHash 确认交易已经被节点接收,再轮询 getTransactionReceipt 确认交易已经上链。如果 receipt 一直返回 null,说明交易还在 pending 状态;如果返回了 receipt 但 status 为 0,说明交易虽然上链了,但执行回滚了。

eth_sendRawTransaction 是发送交易的接口,但参数不是普通的对象,而是签名后的原始交易字节。为什么这么设计?因为这确保了只有持有私钥的人才能发起交易。节点只负责验证签名、检查 nonce 和余额,然后广播交易。

签名交易的过程通常是:构造一个交易对象(包含 from、to、value、gas、gasPrice、nonce、chainId、data),然后用私钥对整个交易做签名,得到一个十六进制字符串,再传给 eth_sendRawTransaction。在实际开发中,我会用 ethers.js 或 web3.js 来构造和签名交易,手写 RLP 编码实在太容易出错。

2.3 合约交互接口

eth_call 是在不消耗 gas 的情况下模拟执行一个合约调用。它不会改变链上状态,非常适合用来查询合约的只读方法。比如你想知道某个 ERC-20 代币的 totalSupply 是多少,就可以构造一个 eth_call 请求。

eth_call 的参数有两个:第一个是交易对象(包含 to、data 等字段),第二个是区块高度。交易对象的 data 字段是合约方法和参数的 ABI 编码结果,格式形如方法选择器(前4字节)+ 参数编码。

curl -X POST http://localhost:8545 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "eth_call", "params": [ { "to": "0x6b175474e89094c44da98b954eedeac495271d0f", "data": "0x18160ddd" }, "latest" ] }'

这里的 data "0x18160ddd" 就是 totalSupply() 的方法选择器。这个选择器是由函数签名 keccak256("totalSupply()") 的前4字节得到的。手动算选择器太麻烦,实际开发中用 ethers.js 的接口实例来生成 data 会方便得多。

eth_estimateGas 用于估算一笔交易需要消耗多少 gas。它和 eth_call 很相似,也是模拟执行,但返回的是 gas 消耗量。在发送交易前调用一下这个接口,可以有效避免因为 gas 设置过低导致交易失败。

还有一个接口 eth_getLogs 也很常用,用来按照过滤条件查询事件日志。参数是 fromBlock、toBlock、address、topics。比如你想查询某个地址在过去 1000 个区块内所有 Transfer 事件,就可以用这个接口。在实现链上数据监控、事件追踪业务时,这个接口是必用的。

2.4 其他实用接口

web3_clientVersion 返回节点的客户端版本信息,可以用来确认你连接的节点类型和版本。eth_blockNumber 我们已经说过了,eth_getBlockByNumber 可以查询区块的详细信息,包含区块内所有交易的哈希列表。eth_getCode 返回某个地址的合约字节码,可以用来判断该地址是否为合约地址(如果返回 "0x" 说明不是合约)。

还有 eth_subscribe 和 eth_unsubscribe,这两个是 WebSocket 接口,用于订阅新区块、新交易、事件日志等。如果你的应用需要实时监控链上数据,建议用 WebSocket 而不是轮询 HTTP,否则既浪费带宽又有延迟。

3. 实战:用 curl 和 ethers.js 跑通一次完整交互

3.1 准备一个可用的 RPC 端点

在动手之前,你需要一个可以访问的节点 RPC 端点。有三种常见方式:

  • 本地节点:比如用 Geth 或 Erigon 跑一个全节点,默认 RPC 地址是 http://localhost:8545。
  • 轻节点:比如用 Erigon 的 --state.cache 模式或者一些轻客户端方案。
  • 第三方节点服务商:Infura、Alchemy、QuickNode 等都提供免费层级的 RPC 端点。

我建议新手先注册一个第三方服务商账号拿免费端点,因为本地全节点同步数据要下载几十 GB 甚至上百 GB 的数据,时间成本太高。但如果你有隐私或安全方面的需求,本地节点会是更好的选择。

值得注意的是,第三方服务商的免费端点通常有速率限制。在开发和测试阶段够用,但如果你要做生产级应用,最好升级付费套餐,或者自己去跑节点。

3.2 用 curl 快速验证接口

拿到端点后,先用 curl 测试一下节点是否正常。这里我以公共测试网端点为例:

curl -X POST https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] }'

如果一切正常,你会看到类似这样的响应:

{ "jsonrpc": "2.0", "id": 1, "result": "0x10d4f" }

把 result 从十六进制转成十进制,就是当前区块高度。可以用 Python 一行搞定:print(int("0x10d4f", 16))。

3.3 用 ethers.js 封装常用操作

真正写业务代码时,直接用 curl 拼 JSON 太原始了。我一般用 ethers.js,因为它是目前 EVM 生态里最主流的 JavaScript SDK,接口封装得很完善。

安装 ethers.js:

npm install ethers

然后写一个简单的脚本,连接节点、查余额、查区块高度:

const { ethers } = require("ethers"); const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY"); async function main() { const blockNumber = await provider.getBlockNumber(); console.log("当前区块高度:", blockNumber); const balance = await provider.getBalance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e"); console.log("余额(ETH):", ethers.formatEther(balance)); const gasPrice = await provider.getFeeData(); console.log("当前 gasPrice:", gasPrice.gasPrice.toString()); } main();

ethers.js 底层就是在封装 JSON-RPC 调用。provider.getBlockNumber 对应的是 eth_blockNumber,provider.getBalance 对应的是 eth_getBalance。理解这层对应关系,能让你在排查问题时更快定位到具体是哪个 RPC 调用出了问题。

如果你要发送一笔交易,流程会稍微复杂一点:

const wallet = new ethers.Wallet("你的私钥", provider); const tx = await wallet.sendTransaction({ to: "0x接收方地址", value: ethers.parseEther("0.01"), }); console.log("交易哈希:", tx.hash); const receipt = await tx.wait(); console.log("交易状态:", receipt.status);

这里面 wallet.sendTransaction 内部做了几件事:调用 eth_estimateGas 估算 gas、调用 eth_gasPrice 获取建议 gas 价格、构造交易对象并用私钥签名、最后调用 eth_sendRawTransaction 把签名后的交易广播出去。tx.wait() 则会轮询 eth_getTransactionReceipt,直到交易上链。

3.4 完整调试一次合约调用

现在假设我要调用一个 ERC-20 代币的 balanceOf 方法,查询某个地址的持币数量。用 ethers.js 可以这样写:

const contract = new ethers.Contract( "0x6b175474e89094c44da98b954eedeac495271d0f", // DAI 合约地址 ["function balanceOf(address owner) view returns (uint256)"], provider ); const balance = await contract.balanceOf("0x742d35Cc6634C0532925a3b844Bc454e4438f44e"); console.log("DAI 余额:", ethers.formatEther(balance));

这个过程中 ethers.js 会构造一个 eth_call 请求,把函数名和参数编码进 data 字段,并发给节点。节点在 EVM 里执行 balanceOf,返回一个 32 字节的 uint256 值,ethers.js 再帮你解码成可读的数字。

如果你不用 ethers.js,也可以手动拼 eth_call 的 data:

const data = "0x70a08231" + "000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e";

这里 "0x70a08231" 是 balanceOf(address) 的方法选择器,后面跟的是地址参数,左补零到 64 个十六进制字符。手动拼接容易出错,地址大小写、补零数量不对都会导致调用失败。我在调试时更倾向于先用 ethers.js 自带的接口编码功能生成 data,再用 curl 去验证,这样两头都放心。

4. 多链兼容的差异与踩坑记录

4.1 不同链之间的接口差异点

理论上,所有 EVM 兼容链都支持 eth_ 系列方法,但在实际开发中,我遇到过不少细微的差异。

chainId 是最明显的一个。签名交易时必须使用目标链的 chainId,否则交易会被拒绝。以太坊主网是 1,Ropsten 是 3,BSC 是 56,Polygon 是 137,Arbitrum One 是 42161,Optimism 是 10。各家测试网的 chainId 也各不相同。

区块时间差异会影响到你的轮询逻辑。以太坊的区块时间大约 12 秒,BSC 大约 3 秒,Polygon 大约 2 秒。如果你在以太坊开发时习惯了等待 3 个确认,迁移到 BSC 后等 3 个确认就快得多,但最终性保证并不同。

还有一些链会有自己的额外接口,比如 BSC 有 eth_getProof 相关的扩展方法,Arbitrum 有一些 Layer 2 特有的方法。但如果你只是做基础的数据读取和交易发送,核心 eth_ 方法已经够用。

4.2 统一多链开发的常用策略

如果你要同时对接多条链,我建议把 RPC 端点配置和链参数抽离成统一配置。比如维护一个数组:

const CHAINS = [ { name: "ethereum", chainId: 1, rpcUrl: "https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY", explorerUrl: "https://etherscan.io", }, { name: "bsc", chainId: 56, rpcUrl: "https://bsc-dataseed.binance.org", explorerUrl: "https://bscscan.com", }, { name: "polygon", chainId: 137, rpcUrl: "https://polygon-rpc.com", explorerUrl: "https://polygonscan.com", }, ];

然后根据不同的 chainId 创建对应的 provider。这样管理起来很方便,也便于后续扩展新的链。

公共 RPC 端点的稳定性是个大坑。比如 Polygon 的公共端点有时候会拥堵或限流,BSC 的公共端点偶尔也会更新维护。生产环境务必准备多个备用 RPC,并做好失败时的自动切换。我在本地开发时习惯用环境变量管理端点地址,不会把端点硬编码到代码里。

4.3 常见错误码和排查思路

与 JSON-RPC 打交道时,你一定会遇到各种错误。我整理了一份高频错误速查表:

错误码错误信息含义排查方向
-32601Method not found方法不存在检查方法名拼写是否错误,链上是否支持该方法
-32602Invalid params参数无效检查参数个数、格式,特别留意十六进制和大小写
-32000Insufficient funds余额不足确认地址余额是否足够支付 gas 和转账金额
-32000Nonce too lownonce 过低检查本地 nonce 缓存,重新同步最新 nonce
-32000replacement transaction underpriced替换交易 gas 过低提高 gasPrice 重新发送,或等原交易超时
-32000execution reverted合约执行回滚用 eth_call 模拟执行,排查 contract 逻辑或参数

遇到错误时我建议的排查步骤是:先用 curl 直接调接口,确认是节点返回的问题还是 SDK 层的问题。然后在浏览器(比如 Etherscan)上查看相同的交易或地址,排除网络同步导致的延迟问题。最后再查看代码逻辑中参数是否传错。

4.4 一个亲身踩过的坑:nonce 管理不当

我在做批量转账工具时踩过一个 nonce 的坑。当时我同时发送多笔交易,用的是同一个钱包,每笔交易都调 eth_getTransactionCount 获取 nonce,结果因为并发请求,两笔交易拿到了相同的 nonce,导致其中一笔交易被节点拒绝。

后来我改为本地维护 nonce 计数器,每发送一笔交易就自增 1,并且在交易被打包确认后再和链上的实际 nonce 做一次校验,问题才解决。如果你要在代码里处理多笔连续交易,建议用 pending 状态下的 nonce 作为基准,而不是每次从链上查询。

这里我多说一句:以太坊的 nonce 是从 0 开始、按交易发送顺序递增的计数器。如果你有一笔交易卡在 pending 状态一直没上链,后面所有 nonce 更大的交易都会被阻塞。所以在批量发送场景下,要么确保每一笔都成功,要么用 replace / cancel 策略处理卡住的交易。

5. 常用工具链与生产环境经验

5.1 除了 curl 和 ethers.js,还有哪些调试利器

如果你还在手动拼 JSON 请求来调试节点接口,效率太低了。我常用几个工具来提升开发体验。

Postman 可以保存各种 RPC 请求模板并且支持环境变量。我一般会建一个 Environment 来管理不同链的 RPC 地址,这样切换主网、测试网非常方便。

Foundry 自带一个 cast 命令行工具,可以直接进行 RPC 调用。比如 cast call、cast send、cast balance,底层都是 JSON-RPC 的封装。这个工具在调试合约交互时特别顺手。

还有 geth 自带的 geth attach 命令,可以连接到一个正在运行的节点,并在 JavaScript 控制台里直接调用各种方法。这对于本地开发和排查节点同步问题很有帮助。

5.2 批量请求与性能优化技巧

如果在一个界面里需要同时显示用户的 ETH 余额、多种代币余额、最新区块高度,每个都单独发一个 RPC 请求会非常慢。JSON-RPC 支持批量请求——把多个请求对象放在一个数组里,一次 HTTP 请求发出去,节点按顺序处理并返回一个数组。

[ {"jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": []}, {"jsonrpc": "2.0", "id": 2, "method": "eth_getBalance", "params": ["0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "latest"]}, {"jsonrpc": "2.0", "id": 3, "method": "eth_gasPrice", "params": []} ]

这样一次请求就能拿到三个结果,大大减少了网络开销。在轮询区块头、批量查询余额等场景下,这个技巧非常实用。

另一个优化点是减少不必要的 eth_call。比如你要查询 100 个地址的 ERC-20 余额,逐个 eth_call 要发 100 次请求。更聪明的做法是合并到一次 multicall 合约调用里。Multicall3 合约在很多链上都有部署,可以一次性聚合多个调用。

5.3 日志和监控的最佳实践

生产环境的 JSON-RPC 调用一定要有日志和监控。至少需要记录:

  • 每次调用的方法名、耗时、是否成功。
  • 节点地址和当前请求的错误率。
  • 如果同时使用多个 RPC 提供商,做一个简单的健康检查,自动剔除不健康的端点。

我在项目中习惯用 Promise.race 实现 RPC 端点的超时控制,避免某个节点无响应时整个请求卡死。设置超时时间在 10 秒左右比较合理,太长影响体验,太短则容易误判。

对于 WebSocket 订阅场景,还要注意断线重连和重订阅的逻辑。WebSocket 连接会因为网络波动或服务端重启断开,如果应用没有处理重连逻辑,就会悄无声息地失去数据流。我的做法是维护一个心跳定时器,定期检查连接状态,断线后自动重连并重新订阅。

5.4 关于安全方面的提醒

调用 JSON-RPC 接口时,有几个安全点值得注意。第一,不要把私钥放在前端代码或客户端环境里。签名操作应该在后端或用户本地完成,私钥一旦泄露,资产就没了。第二,如果你在本地跑节点,确保 RPC 端点不要暴露在公网。Geth 默认只监听 127.0.0.1,不要轻易改成 0.0.0.0,否则任何人都可以调用你的节点接口发送交易。

第三方 RPC 提供商通常不建议直接在浏览器环境中暴露你的 API Key,特别是如果你的 Key 有配额限制,被他人盗用后会影响你的正常使用。更稳妥的做法是,把 RPC 请求通过你自己的后端服务转发,由后端统一管理 Key。

6. 最后的实践经验总结

如果你正在搭建一个需要与链上交互的系统,我的建议是先从最简单的 curl 开始验证节点连通性,再由浅入深地接触 eth_call、eth_sendRawTransaction 这些核心方法。每引入一个 SDK,都要想一想它底层调用了哪些 RPC 方法,这样出了问题你才能知道去哪里排查。

我一开始也犯过很多低级错误,比如把十六进制字符串直接当成十进制数字比较,或者忽略了 nonce 的自增规则。这些东西在写业务代码时看似不起眼,但一旦上了生产环境,每一个小错误都可能变成资金损失或服务事故。

就我个人经验来说,JSON-RPC 接口的知识不仅适用于以太坊主网,也几乎是所有 EVM 兼容链开发的基础功。只要理解了这个接口层,后续学习 Foundry、Hardhat、ethers.js、viem 之类工具都会顺畅很多。希望这篇分享能帮你少走点弯路,如果有什么问题,欢迎在评论区交流,我看到都会回复。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询