深入以太坊 JSON-RPC 源码,从请求到响应的完整链路剖析
2026.10.24 03:47:35 4 0
作为与以太坊节点交互的核心协议,JSON-RPC 承载了钱包查询余额、DApp 发送交易、区块浏览器同步数据等几乎所有外部通信,本文以 Go-Ethereum(geth)为例,深入其源码,拆解 JSON-RPC 从接收请求到返回结果的完整实现链路。
JSON-RPC 在以太坊中的定位
以太坊节点本质上是一个“状态机 + 服务器”,外部世界无法直接访问节点的内存状态,必须通过 JSON-RPC 接口,常见的调用形式如下:
{
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": ["0x407d73d8a49eeb85d32cf465507dd71d507100c1", "latest"],
"id": 1
}
geth 支持三种传输方式:IPC(本地进程间通信)、HTTP、WebSocket(支持订阅推送)。
源码目录结构
JSON-RPC 核心实现位于 internal/jsonrpc(旧版本为 rpc 包):
internal/jsonrpc/
├── server.go # Server 定义与启动
├── handler.go # 消息处理核心逻辑
├── client.go # 客户端实现
├── service.go # 服务注册与方法解析(反射)
├── types.go # BlockNumber 等参数类型
├── json.go # JSON 编解码器
├── websocket.go # WebSocket 支持
├── http.go # HTTP 传输层
└── ipc.go # IPC 传输层
而具体的 API 业务逻辑分布在其他包中:eth(交易、区块查询)、debug、txpool、net 等,通过服务注册机制挂载到 RPC 服务器上。
服务注册:方法是如何被暴露的
geth 巧妙地利用 Go 反射机制,将普通结构体方法自动导出为 RPC 方法,核心在 service.go 中:
// 方法必须满足导出规则:方法名首字母大写,前两个参数为 ctx 和 receive func (s *RPCService) suitableCallbacks(rcvr reflect.Value, name string) ...
以 eth_getBalance 为例,其底层实现在 eth/api.go:
func (s *BlockChainAPI) GetBalance(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Big, error) {
state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
return nil, err
}
return state.GetBalance(address).ToInt(), nil
}
注册时,GetBalance 会被自动转换为命名空间 eth 下的 getBalance 方法,ethapi 前缀与方法名拼接成完整的 RPC 方法名。
请求处理链路
一条请求的完整生命周期如下:
传输层接收:HTTP/WebSocket 连接建立后,数据交给 Codec 解析为统一的 jsonrpcMessage 结构。
消息分发:handler.go 中的 handleMsg 是关键入口:
func (h *handler) handleMsg(codec Codec) {
// 解析消息,判断是请求、通知还是批量调用
msgs, err := codec.readBatch()
...
switch {
case msg.isNotification(): // 无 id,无需响应
...
case msg.isCall(): // 标准 RPC 调用
go h.handleCall(ctx, msg, respond)
...
}
}
方法路由与调用:h.call 通过方法名找到对应的回调函数,利用预编译的参数解码器将 JSON 参数转换为 Go 类型(例如字符串 "latest" 转为 BlockNumber(-1),这是 types.go 中 UnmarshalJSON 完成的魔法),再反射调用真实方法。
结果编码返回:返回值经过 hex 编码规范处理(如大整数转 hexutil.Big),序列化后写回连接。
订阅机制的实现
eth_subscribe 是 WebSocket 场景的亮点,服务端方法接收一个特殊的 *rpc.Subscription 参数:
func (api *FilterAPI) NewHeads(ctx context.Context) (*rpc.Subscription, error) {
notifier, supported := rpc.NotifierFromContext(ctx)
if !supported {
return nil, rpc.ErrNotificationsUnsupported
}
subscription := notifier.CreateSubscription()
go func() {
for {
select {
case head := <-headersCh:
notifier.Notify(subscription.ID, head)
case <-subscription.Err():
return
}
}
}()
return subscription, nil
}
当连接断开时,subscription.Err() 通道关闭,goroutine 自动退出,避免资源泄漏——这是阅读源码时值得学习的并发处理模式。
值得借鉴的设计亮点
- 反射 + 代码生成缓存:方法解析在注册期完成一次,运行时直接查表调用,兼顾灵活与性能。
- Codec 抽象:编解码与传输解耦,HTTP、WebSocket、IPC 复用同一套处理逻辑。
- 类型自动转换:
BlockNumber、hexutil.Big等类型自带序列化逻辑,接口层无需手写解析代码。 - 上下文传递:
context.Context贯穿调用链,天然支持超时与取消。
读懂以太坊 JSON-RPC 源码,不仅是理解节点通信的基础,更是一次学习高质量 Go 工程实践的机会,建议读者从 handler.go 入手,打断点跟踪一次 eth_blockNumber 调用,亲身体会整条链路的运转,后续文章将继续剖析 ethclient 客户端实现与 Engine API 的共识通信细节。
关键词:以太坊、JSON-RPC、geth 源码、Go 语言、区块链开发
本文转载自互联网,如有侵权,联系删除
