Go语言版以太坊节点(Geth)RPC机制分析与实践

古翠码翁 2022-09-24 11:52:03

声明:本文系作者原创,同文也发表在作者微信公众号,转载请注明出处。

一、前言

不久前主导了设计开发一个数据平台项目,涉及多个数据库系统,且数据之间关系复杂,对上层应用开发者来说,直接操作数据难度颇高,难优化;对于架构层面,这么做既不安全也不高效,会导致程序逻辑复杂、耦合度高、维护难度大。为此在数据架构设计时,特别增加了一层支持业务的查询API,以RPC(远程过程调用)方式提供服务,支持上层业务。

因时间紧迫,来不及从头开始学习各种RPC开发框架,技术实践上就决定直接拿以太坊节点的RPC机制改造达到目的。

采用这种方式原因大致有三:

  1. 以太坊节点作为区中心化的网络计算节点,很多服务都是通过RPC方式对外开放,本身有一套完善的RPC实现机制;

  1. 业务涉及到处理区块链数据,直接使用以太坊框架很多现成的开发库可以复用,对业务支持顺畅;

  2. 之前有阅读一些以太坊的代码,加上用Go实现轻车熟路,上手方便,大概2天阅读代码,2天实践就差不多完成了基础架构。

    本文就对以太坊的RPC工作机制做一个梳理分析,也是前期工作学习成果的一个总结。为突出实践性,在代码分析后,我还将其中必要的实现代码抽离出来,构成一个轻量RPC框架TinyGethRPC,在简单业务场景下,用户直接添加自己的业务逻辑就可以使用了。此外,个人感觉阅读Geth的代码也是学习Go语言的一个比较不错的途径。

    这里附上项目链接https://github.com/swankong/TinyGethRPC ,需要指出这里代码绝大部分直接摘取自Geth开源代码,遵循Geth的版权协议。

    二、GethRPC机制分析

    2.1 一个简单例子

    开始分析之前,先给一个简单直观的例子。

    假设用户本地部署了一个Geth以太坊节点,并开启了RPC功能。以太坊节点的RPC服务支持Http,WebSocket,IPC等多种协议。这里仅以http协议为例,服务开放端口为8545(默认)。尝试http请求方式调用RPC服务,获取当前最新区块号,一般如下操作:

    curl -XPOST http://localhost:8545 -H 'Content-Type: application/json' -d '{"method": "eth_blockNumber", "params": [], "id":1}'
    返回内容:
    {"jsonrpc":"2.0","id":1,"result":"0xecfc2a"}

    上面是一个标准的以太坊RPC请求和返回内容,具体对应到代码中的数据结构我们后面会分析。这里通过主机地址和端口号,发送了一个json串,指明调用服务名字和参数(空),主机接受服务请求后,返回结果。如果本地没有部署以太坊节点,可以尝试使用公开的以太坊节点infura(infura.io)或者alchemy(alchemy.com)按照主页上提示的说明尝试。

    2.2 以太坊RPC服务启动流程

    以太坊节点是如何实现RPC服务的呢,我们先从节点启动流程分析一下。这里我们用框图来说明,不做具体代码分析,框图中标明了程序所在的文件和执行的具体函数,有兴趣可以进一步阅读,这部分内容总体来说结构清晰,逻辑简单,很容易理解。

    另外,后面的代码分析部分,为了尽量避免在多层的函数嵌套中迷失方向,我会标明每个代码块所在的文件和调用栈,以尽可能做到逻辑清晰。

    图1所示流程是以太坊节点从命令行执行启动后,一路初始化过程,先启动Node,在Node下面有一系列服务,RPC是其中之一,http协议的RPC又是几种RPC服务之一。

     图1 . Geth节点RPC服务启动流程

     图2 是Geth的代码目录结构,如无特别说明,本文中代码路径都是相对根目录GO-ETHEREUM-MASTER的下层目录。红框标记部分是本分代码分析会涉及的模块。参考代码版本是geth-linux-amd64-1.10.17-25c9b49f.tar.gz

     图2. Geth的代码目录结构

    2.2 设置Http协议的RPC服务和注册API

    本节介绍设置和启动http RPC服务具体步骤,先给出流程框图,了解一下大致过程。

     

    图3. httpRPC服务初始化、API注册流程

    type httpServer struct {
        log      log.Logger  // 打印log
        timeouts rpc.HTTPTimeouts  // 超时设置
        mux      http.ServeMux // registered handlers go here http多路选择器
        mu       sync.Mutex  // 协程之间同步互斥量
        server   *http.Server // net/http 的数据结构http.Server指针
        listener net.Listener // non-nil when server is running
        // HTTP RPC handler things.
        httpConfig  httpConfig  // 配置信息
        httpHandler atomic.Value // *rpcHandler 封装成原子类型,存放指针 硬件支持 比mutex效率高
        // WebSocket handler things.  websocket相关,略去
        wsConfig  wsConfig
        wsHandler atomic.Value // *rpcHandler
        // These are set by setListenAddr.
        endpoint string  // 服务根目录
        host     string  // 主机地址
        port     int  // 端口 
        handlerNames map[string]string  // 方法名映射
    }

    代码片段1. httpServer 数据结构 

    文件:node/rpcstack.go

    代码片段1 中所示是重要的数据结构,定义了一个http服务器结构,具体意义见中文注释。其中比较重要的数据成员包括:

    1. mux -- golang自带的一个http路由库,实现请求的url路径与相应的处理函数的映射关系。

    2. server -- golang自带的http.Sever指针,http.Server实现了一个http协议的服务器。

    3. httpHandler -- 注意一下数据类型,是atomic.Value,这是go语言的原子数据类型。这个数据类型在底层通过硬件实现原子操作,即对Value读写时候,不会被其他线程打断。atomic类型因为实在硬件级别实现了原子操作,要比互斥量mutex(在操作系统级别实现)的效率高很多。

    
    initHttp := func(server *httpServer, apis []rpc.API, port int) error {
            if err := server.setListenAddr(n.config.HTTPHost, port); err != nil {
                return err
            }
            if err := server.enableRPC(apis, httpConfig{
                CorsAllowedOrigins: n.config.HTTPCors,
                Vhosts:             n.config.HTTPVirtualHosts,
                Modules:            n.config.HTTPModules,
                prefix:             n.config.HTTPPathPrefix,
            }); err != nil {
                return err
            }
            servers = append(servers, server)
            return nil
        }

    代码片段2. Http协议的RPC服务器初始化

     

    文件:node/node.go

    调用栈: initHttp()

    initHttp完成Http RPC服务器的初始化工作。

    第2~4行,设置侦听地址和端口,默认端口8545.

    第5~12行,设置服务器配置信息,包括对外开放的api,http跨域访问,虚拟主机等。设置完成后,将server结构放入到server列表,后续会一一调用start()启动服务。

    其中第5行传入的参数apis就是对外开放服务的api。通过node.apis()方法获得。来看一下apis()的定义。

    
    // apis returns the collection of built-in RPC APIs.
    func (n *Node) apis() []rpc.API {
        return []rpc.API{
            {
                Namespace: "admin",
                Version:   "1.0",
                Service:   &privateAdminAPI{n},
            }, {
                Namespace: "admin",
                Version:   "1.0",
                Service:   &publicAdminAPI{n},
                Public:    true,
            }, {
                Namespace: "debug",
                Version:   "1.0",
                Service:   debug.Handler,
            }, {
                Namespace: "web3",
                Version:   "1.0",
                Service:   &publicWeb3API{n},
                Public:    true,
            },
        }
    }
    
    // publicAdminAPI is the collection of administrative API methods exposed over
    // both secure and unsecure RPC channels.
    type publicAdminAPI struct {
        node *Node // Node interfaced by this API
    }
    
    // Peers retrieves all the information we know about each individual peer at the
    // protocol granularity.
    func (api *publicAdminAPI) Peers() ([]*p2p.PeerInfo, error) {
    }
    
    // NodeInfo retrieves all the information we know about the host node at the
    // protocol granularity.
    func (api *publicAdminAPI) NodeInfo() (*p2p.NodeInfo, error) {
    }
    
    // Datadir retrieves the current data directory the node is using.
    func (api *publicAdminAPI) Datadir() string {
    }
    
    // API 结构的定义在rpc/types.go中,这里一并列出,可以看到各个成员的类型
    // 其中Service是一个接口类型,等同于C语言中的 void *
    // API describes the set of methods offered over the RPC interface
    type API struct {
        Namespace     string      // namespace under which the rpc methods of Service are exposed
        Version       string      // api version for DApp's
        Service       interface{} // receiver instance which holds the methods
        Public        bool        // indication if the methods must be considered safe for public use
        Authenticated bool        // whether the api should only be available behind authentication.
    }

    代码片段3. Node模块开放的对外服务API

    文件:node/api.go

    调用栈: main()->geth()->makeFullNode()->makeConfigNode()->node.New()->node.apis()

    代码片段3 是RPC开放服务的API定义部分。

    3~23行,可以看到,定义了4组API,其中Namespace可认为是API的前缀。回顾2.1节给出的例子,我们传递的方法名是"method": "eth_blockNumber",这里面, eth对应Namespace,blockNumber是方法名,中间下划线"_"是Geth自动添加的,后文的参数解析我们会接触到。实际上RPC服务的解析过程就是拿到eth_blockNumber这个字符出串,然后根据"_"分成两部分,然后从函数映射表中找到eth路径下的blockNumber方法地址,执行过程调用返回结果。

    Service是接口类型,以第11行为例,凡是publicAdminAPI类型下的方法,都可以通过第11行的Service入口调用,即运行时通过第11行的入口,可以找到并执行34~43行的方法。

    比如执行Peers()方法,可以执行

    curl -XPOST http://localhost:8545 -H 'Content-Type: application/json' -d '{"method": "admin_peers", "params": [], "id":1}'
    type rpcHandler struct {
        http.Handler
        server *rpc.Server
    }
    // enableRPC turns on JSON-RPC over HTTP on the server.
    func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error {
        h.mu.Lock()
        defer h.mu.Unlock()
    
        if h.rpcAllowed() {
            return fmt.Errorf("JSON-RPC over HTTP is already enabled")
        }
    
        // Create RPC server and handler.
        srv := rpc.NewServer()
        if err := RegisterApis(apis, config.Modules, srv, false); err != nil {
            return err
        } 
        h.httpConfig = config
        h.httpHandler.Store(&rpcHandler{
            Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret),
            server:  srv,
        })
        return nil
    }
     //处理不同的 http/header信息,串起来,形成stack

    代码片段4. enableRPC()

    文件:node/rpcstack.go

    调用栈:initHttp()->enableRPC()

    弄清apis的来源后,再来看enableRPC()里面执行了什么。

    第16行,将对外服务的api函数名称、地址写进srv内部的映射表(后文具体分析)

    第20行,httpHandler.Store是一个原子写入操作,中间不可打断。这一部将rpcHanlder地址写入到httpHandler中。其中http.Handler是一个接口,定义了一个方法ServeHTTP()。因为golang支持鸭子类型(即对行为建模),任何实现了这个方法的类型都属于这个接口。只要NewHTTPHandlerStack返回的类型实现了ServeHTTP() 方法,就属于这个接口,感兴趣可以具体查阅node/rpcstack.go中的代码实现, New HTTPHandlerStack()中创建的几个handler对象都实现了ServeHTTP()方法。

    第21行,这实际是创建了一个链表,针对不同http请求的Header信息,用不同的handler处理。

    func (r *serviceRegistry) registerName(name string, rcvr interface{}) error {
        rcvrVal := reflect.ValueOf(rcvr)
        if name == "" {
            return fmt.Errorf("no service name for type %s", rcvrVal.Type().String())
        }
        callbacks := suitableCallbacks(rcvrVal)
        if len(callbacks) == 0 {
            return fmt.Errorf("service %T doesn't have any suitable methods/subscriptions to expose", rcvr)
        }
    
        r.mu.Lock()
        defer r.mu.Unlock()
        if r.services == nil {
            r.services = make(map[string]service)
        }
        svc, ok := r.services[name]
        if !ok {
            svc = service{
                name:          name,
                callbacks:     make(map[string]*callback),
                subscriptions: make(map[string]*callback),
            }
            r.services[name] = svc
        }
        for name, cb := range callbacks {
            if cb.isSubscribe {
                svc.subscriptions[name] = cb
            } else {
                svc.callbacks[name] = cb
            }
        }
        return nil
    }
    
    // suitableCallbacks iterates over the methods of the given type. It determines if a method
    // satisfies the criteria for a RPC callback or a subscription callback and adds it to the
    // collection of callbacks. See server documentation for a summary of these criteria.
    func suitableCallbacks(receiver reflect.Value) map[string]*callback {
        typ := receiver.Type() // type: *RPCService  1 Method: Modules()
        callbacks := make(map[string]*callback)
        for m := 0; m < typ.NumMethod(); m++ {
            method := typ.Method(m)
            if method.PkgPath != "" {
                continue // method not exported
            }
            cb := newCallback(receiver, method.Func) // 可以自行阅读创建newCallBack方法 
            if cb == nil {
                continue // function invalid
            }
            name := formatName(method.Name)
            callbacks[name] = cb
        }
        return callbacks
    }

    代码片段6. registerName()

    文件:rpc/service.go

    调用栈:initHttp()->enableRPC()->rpc.NewSever()->Server.RegisterName()->serviceRegistry.registerName()

    第2行,这里用了golang的反射,reflect.ValueOf()是运行时动态获取接口rcvr具体值。

    第3行,进行合法性检查,检查rcvrVal的值是不是符合callback函数的要求。

    第11~32行,构建映射表,因为golang的map不是协程安全的,因此操作前要加锁。然后创建方法映射表。这里name="rpc",函数callbacks从suitableCallbacks()中解析而出。

    再来看suitableCallbacks()方法:

    第39行,获取receiver的类型,应该是*RPCService类型(RPCService的指针)。

    第41~52行,遍历*RPCService的类型定义的方法,根据源代码实现,这个类型只有一个方法即Modules(),定义在rpc/server.go中。对*RPCService每个方法,做合法性检查,然后将method对应的方法转换成要给callback对象赋值给cb。formatName将首字母变成小写,然后加入到callback映射表。

    // Modules returns the list of RPC services with their version number
    func (s *RPCService) Modules() map[string]string {
        s.server.services.mu.Lock()
        defer s.server.services.mu.Unlock()
    
        modules := make(map[string]string)
        for name := range s.server.services.services {
            modules[name] = "1.0"
        }
        return modules
    }

    代码片段7. RPCService.Modules()的定义

    为了验证上文的分析,可以执行下面命令,并查看返回结果:

    curl -XPOST http://localhost:8545 -H 'Content-Type: application/json' -d '{"method": "rpc_modules", "params": [], "id":1}'
    返回结果:
    "jsonrpc":"2.0","id":1,"result":{"admin":"1.0","debug":"1.0","eth":"1.0","net":"1.0","personal":"1.0","rpc":"1.0","web3":"1.0"}}
    根据返回结果可见,本地节点开放了"admin", "debug", "eth", "net", "personal", "rpc", "web3"等几个模块的RPC服务。
    // RegisterApis checks the given modules' availability, generates an allowlist based on the allowed modules,
    // and then registers all of the APIs exposed by the services.
    func RegisterApis(apis []rpc.API, modules []string, srv *rpc.Server, exposeAll bool) error {
        if bad, available := checkModuleAvailability(modules, apis); len(bad) > 0 {
            log.Error("Unavailable modules in HTTP API list", "unavailable", bad, "available", available)
        }
        // Generate the allow list based on the allowed modules
    allowList := make(map[string]bool)
        for _, module := range modules {
            allowList[module] = true
        }
        // Register all the APIs exposed by the services
        for _, api := range apis {
            if exposeAll || allowList[api.Namespace] || (len(allowList) == 0 && api.Public) {
                if err := srv.RegisterName(api.Namespace, api.Service); err != nil {
                    return err
                }
            }
        }
        return nil
    }

    代码片段8. RegisterApis()

    文件:rpc/rpcstack.go

    调用栈:initHttp()->enableRPC()->RegisterApis()

    回到enableRPC(),完成一个RPCServer结构创建后,会执行RegisterApis,将对外开放的API方法(代码片段3)写入到映射表中,在服务运行时,根据请求参数查表执行对应的服务API。转到代码片段8,来看一下RegisterApis()的实现。

    第4~6行,做检查,不再赘述。

    第8~11行,根据开放的模块设置开放列表,这个参数可以通过命令行传递,启动时指定Geth节点开放哪些模块的RPC服务。

    第第14~19行,对于每个rpc.API结构体,满足开放条件的话,就写入到映射列表。其中名字是Namesapce定义的名字,api.Service是前文提到的接口。RegietgerName里面会利用反射机制,遍历每个api.Service结构拥有的方法,将方法名和入口地址写入到映射表(方法名第一个字母小写)。

    再回顾代码片段3中给出的例子。这里就理解了为什么"admin" Namespace空间下,类型publicAdminAPI拥有的方法func (api *publicAdminAPI) Peers() ([]*p2p.PeerInfo, error) ,在RPC调用下,传入的参数是"admin_peers"了。

    2.3 完成设置后,启动HTTP 服务

    先给出设置完服务RPC服务后,启动服务的流程框图。

     图4. Http服务启动流程

    // start starts the HTTP server if it is enabled and not already running.
    func (h *httpServer) start() error {
        h.mu.Lock()
        defer h.mu.Unlock()
    
        if h.endpoint == "" || h.listener != nil {
            return nil // already running or not configured
        }
    
        // Initialize the server.
        h.server = &http.Server{Handler: h} // 重要
        if h.timeouts != (rpc.HTTPTimeouts{}) {
            CheckTimeouts(&h.timeouts)
            h.server.ReadTimeout = h.timeouts.ReadTimeout
            h.server.WriteTimeout = h.timeouts.WriteTimeout
            h.server.IdleTimeout = h.timeouts.IdleTimeout
        }
    
        // Start the server.
        listener, err := net.Listen("tcp", h.endpoint) // 侦听
        if err != nil {
            // If the server fails to start, we need to clear out the RPC and WS
            // configuration so they can be configured another time.
            h.disableRPC()
            h.disableWS()
            return err
        }
        h.listener = listener
        go h.server.Serve(listener) // Serve accepts incoming HTTP connections on the listener l, creating a new service goroutine for each. The service goroutines read requests and then call handler to reply to them.
    
        if h.wsAllowed() { // ws, ignore 跳过
            url := fmt.Sprintf("ws://%v", listener.Addr())
            if h.wsConfig.prefix != "" {
                url += h.wsConfig.prefix
            }
            h.log.Info("WebSocket enabled", "url", url)
        }
        // if server is websocket only, return after logging
        if !h.rpcAllowed() {
            return nil
        }
        // Log http endpoint. 输出启动信息
        h.log.Info("HTTP server started",
            "endpoint", listener.Addr(), "auth", (h.httpConfig.jwtSecret != nil),
            "prefix", h.httpConfig.prefix,
            "cors", strings.Join(h.httpConfig.CorsAllowedOrigins, ","),
            "vhosts", strings.Join(h.httpConfig.Vhosts, ","),
        )
    
        // Log all handlers mounted on server.
        var paths []string
        for path := range h.handlerNames {
            paths = append(paths, path)
        }
        sort.Strings(paths)
        logged := make(map[string]bool, len(paths))
        for _, path := range paths {
            name := h.handlerNames[path]
            if !logged[name] {
                log.Info(name+" enabled", "url", "http://"+listener.Addr().String()+path)
                logged[name] = true
            }
        }
        return nil
    }

    代码片段9. 启动Http服务

    文件:node/rpcstack.go

    调用栈:initHttp()->startRPC()->httpServer.start()

    2.2节介绍了服务的初始化设置工作。完成设置后,开始启动服务。

    代码片段9是服务启动的实现,这段代码只需要重点关注2行内容。

    第11行,这里比较重要,用go自带的http包,声明了http.Server,并将handler设置为h。这个h是代码片段1中定义的结构体。根据go语言对行为建模的特性,http.Handler是一个接口,定义了ServeHTTP()方法(详见代码片段10),只要h指向的结构体实现了这个方法,它就属于这个接口。h指向httpServer,httpServer是现的ServeHTTP()方法就在node/rpcstack.go中定义。2.4节会进一步分析。

    第29行,启动一个go协程,调用go语言自带的http.Serve()方法。这个方法侦听listener中设置的端口,对于每一个新进入的请求,都会创建一个新的go协程处理。创建的处理协程会调用在第11行注册的ServeHTTP()方法,用户的自定义处理逻辑直接是现在ServeHTTP()。

    到了这里,整个http的RPC服务运行框架就基本明了了。后面深入到ServeHTTP()看一下具体的RPC运行机制:如何解析请求的参数,找到请求的方法,然后执行相应的服务返回结果。

    另外,我曾考虑过,如果是高并发场景下,按照29行处理的方式可能会出现问题,瞬间系统创建大量协程可能导致资源耗尽而崩溃,后续迭代设计中可能需要进一步优化。

    
    type Handler interface {
            ServeHTTP(ResponseWriter, *Request)
    }
    type Server struct {
            // Addr optionally specifies the TCP address for the server to listen on,        // in the form "host:port". If empty, ":http" (port 80) is used.// The service names are defined in RFC 6335 and assigned by IANA.// See net.Dial for details of the address format.
            Addr string        Handler Handler // handler to invoke, http.DefaultServeMux if nil
        ... ...
    }

    代码片段 10. go自带http包中关于Handler的接口定义

    2.4 RPC运行的机制

    本节分析一下RPC处理http请求的具体工作机制。

    func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        // check if ws request and serve if ws enabled 跳过
        ws := h.wsHandler.Load().(*rpcHandler)
        if ws != nil && isWebsocket(r) {
            if checkPath(r, h.wsConfig.prefix) {
                ws.ServeHTTP(w, r)
            }
            return
        }
        // if http-rpc is enabled, try to serve request
        rpc := h.httpHandler.Load().(*rpcHandler)  // 类型断言
        if rpc != nil {
            // First try to route in the mux.
            // Requests to a path below root are handled by the mux,
            // which has all the handlers registered via Node.RegisterHandler.
            // These are made available when RPC is enabled.
            muxHandler, pattern := h.mux.Handler(r)
            if pattern != "" {
                muxHandler.ServeHTTP(w, r)
                return
            }
    
            if checkPath(r, h.httpConfig.prefix) {
                rpc.ServeHTTP(w, r)
                return
            }
        }
        w.WriteHeader(http.StatusNotFound)
    }
    

    代码片段11. ServeHTTP()

    文件:node/rpcstack.go

    调用栈:http.Serve()->ServeHTTP()

    http服务启动后,对于每一个新进入的请求,程序会创建新的go协程,并执行ServeHTTP()。按照代码片段11,我们看看了ServeHTTP()具体做了什么。

    第3~9行,websocket协议相关处理,不详述。

    第11行,回忆前文所述,h.httpHandler是一个atomic.Value类型,通过Load()执行原子读操作,拿到了一个指针,然后做一个类型断言,获得一个指向rpcHandler的指针。

    第17~21行,根据根路径的前缀寻找对应的方法,多数情况下这部分会跳过到23行。

    第23~25行,实际上就是执行24行的rpc.ServHTTP(),传入的参数r是http请求http.Request。

    
    const (
        maxRequestContentLength = 1024 * 1024 * 5
        contentType             = "application/json"
    )
    
    // https://www.jsonrpc.org/historical/json-rpc-over-http.html#id13
    var acceptedContentTypes = []string{contentType, "application/json-rpc", "application/jsonrequest"}
    
    // ServeHTTP serves JSON-RPC requests over HTTP.
    func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        // Permit dumb empty requests for remote health-checks (AWS)
        if r.Method == http.MethodGet && r.ContentLength == 0 && r.URL.RawQuery == "" {
            w.WriteHeader(http.StatusOK)
            return
        }
        if code, err := validateRequest(r); err != nil {
            http.Error(w, err.Error(), code)
            return
        }
    
        // Create request-scoped context.
        connInfo := PeerInfo{Transport: "http", RemoteAddr: r.RemoteAddr}
        connInfo.HTTP.Version = r.Proto
        connInfo.HTTP.Host = r.Host
        connInfo.HTTP.Origin = r.Header.Get("Origin")
        connInfo.HTTP.UserAgent = r.Header.Get("User-Agent")
        ctx := r.Context()
        ctx = context.WithValue(ctx, peerInfoContextKey{}, connInfo)
    
        // All checks passed, create a codec that reads directly from the request body
        // until EOF, writes the response to w, and orders the server to process a
        // single request.
        w.Header().Set("content-type", contentType)
        codec := newHTTPServerConn(r, w)
        defer codec.close()
        s.serveSingleRequest(ctx, codec)
    }

    代码片段 12. Server.ServeHTTP

    文件:rpc/http.go

    调用栈:http.Serve()->ServeHTTP()->Sever.ServeHTTP()

    接上文,rpc.ServeHTTP()实际上是调用了rpc/http.go中的Serve.ServeHTTP(),具体见代码段12,以下逐步分析:

    第12~15行,做检查,对无内容的GET请求返回OK状态(status 200)。

    第16~28行,请求检查和属性设置,设定go协程上下文,用于go协程的数据传递。

    第33行,注意传入content-type被设定为applcation/json (第3行的常量定义)。

    第36行,处理请求,包括解析json格式的请求数据,并做执行具体的处理。

    // serveSingleRequest reads and processes a single RPC request from the given codec. This
    // is used to serve HTTP connections. Subscriptions and reverse calls are not allowed in
    // this mode.
    func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
        // Don't serve if server is stopped.
        if atomic.LoadInt32(&s.run) == 0 {
            return
        }
    
        h := newHandler(ctx, codec, s.idgen, &s.services)
        h.allowSubscribe = false
        defer h.close(io.EOF, nil)
    
        reqs, batch, err := codec.readBatch()
        if err != nil {
            if err != io.EOF {
                codec.writeJSON(ctx, errorMessage(&invalidMessageError{"parse error"}))
            }
            return
        }
        if batch {
            h.handleBatch(reqs)
        } else {
            h.handleMsg(reqs[0])
        }
    }

    代码片段13. Server.serveSingleRequest()

    文件:rpc/server.go

    调用栈:http.Serve()->ServeHTTP()->Sever.ServeHTTP()->Server.serveSingleRequest()

    接着从代码片段13我们来看serveSingleRequest()。

    第10行,这里重要的结构是s.services,这个数据结构包含了前面代码片段6 registerName()中构建的函数中注册表。

    第14 行,json 编码/解码器 读取 http request 请求信息,然后做解码,再传递给parseMessage

    第24 行,处理请求,假设当前是非批量处理情景下。

    // A value of this type can a JSON-RPC request, notification, successful response or
    // error response. Which one it is depends on the fields.
    type jsonrpcMessage struct {
        Version string          `json:"jsonrpc,omitempty"`
        ID      json.RawMessage `json:"id,omitempty"`
        Method  string          `json:"method,omitempty"`
        Params  json.RawMessage `json:"params,omitempty"`
        Error   *jsonError      `json:"error,omitempty"`
        Result  json.RawMessage `json:"result,omitempty"`
    }
    // parseMessage parses raw bytes as a (batch of) JSON-RPC message(s). There are no error
    // checks in this function because the raw message has already been syntax-checked when it
    // is called. Any non-JSON-RPC messages in the input return the zero value of
    // jsonrpcMessage.
    func parseMessage(raw json.RawMessage) ([]*jsonrpcMessage, bool) {
        if !isBatch(raw) {
            msgs := []*jsonrpcMessage{{}}
            json.Unmarshal(raw, &msgs[0])
            return msgs, false
        }
        dec := json.NewDecoder(bytes.NewReader(raw))
        dec.Token() // skip '['
        var msgs []*jsonrpcMessage
        for dec.More() {
            msgs = append(msgs, new(jsonrpcMessage))
            dec.Decode(&msgs[len(msgs)-1])
        }
        return msgs, true
    }

    代码片段14. parseMessage() 解析json消息体

    文件:rpc/json.go

    调用栈:http.Serve()->ServeHTTP()->Sever.ServeHTTP()->Server.serveSingleRequest()->codec.ReadBatch()->parseMessage()

    进入到代码片段14,来看看消息解析部分。

    如果是非批量模式下,直接对json字节串做Unmarshal,将结果映射到jsonrpcMessage结构体中。

    由类型jsonrpcMessage类型定义可见,这些字段跟我们之前rpc例子中传递的内容都能一一对应。

    实际上,RPC请求和应答返回的数据结构都是这个jsonrpcMessage结构。

    这里各个字段分别介绍一下:

    1. Version -- 版本号字段,请求时可以忽略,返回时会有,一般会是 "jsonrpc": "2.0"

    2. ID -- 请求的ID,请求和应答的ID要一一对应。请求时这个字段一般不会忽略,根据需要设置,可以递增形式。

    3. Method -- 请求方法名,格式是 模块名_方法名,其中方法名首字母小写(但代码实现中方法名首字母通常大写,因为go语言首字母小写的变量和方法对模块外是不开放的),模块名和方法名中间用下划线“_"连接,比如"eth_blockNumber"方法。

    4. Params -- 参数列表,请求方法的参数,通常是字典列表形式。

    5. Error -- 错误信息,返回信息中才有的字段。

    6. Result -- 结果信息,返回信息中才有的字段。

    
    // handleMsg handles a single message.
    func (h *handler) handleMsg(msg *jsonrpcMessage) {
        if ok := h.handleImmediate(msg); ok {
            return
        } // 处理订阅相关的,method call都是false
        h.startCallProc(func(cp *callProc) {
            answer := h.handleCallMsg(cp, msg)
            h.addSubscriptions(cp.notifiers) // 订阅
            if answer != nil {
                h.conn.writeJSON(cp.ctx, answer)
            }
            for _, n := range cp.notifiers {
                n.activate()  // 订阅
            }
        })
    }
    // startCallProc runs fn in a new goroutine and starts tracking it in the h.calls wait group.
    func (h *handler) startCallProc(fn func(*callProc)) {
        h.callWG.Add(1)
        go func() {
            ctx, cancel := context.WithCancel(h.rootCtx)
            defer h.callWG.Done()
            defer cancel()
            fn(&callProc{ctx: ctx})
        }()
    }
    type callProc struct {
        ctx       context.Context
        notifiers []*Notifier
    }
    // handleCallMsg executes a call message and returns the answer.
    func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
        start := time.Now()
        switch {
        case msg.isNotification(): // ID为空, Method为空
            h.handleCall(ctx, msg)
            h.log.Debug("Served "+msg.Method, "duration", time.Since(start))
            return nil
        case msg.isCall(): // ID不为空,Method 不为空
            resp := h.handleCall(ctx, msg)
            var ctx []interface{}
            ctx = append(ctx, "reqid", idForLog{msg.ID}, "duration", time.Since(start))
            if resp.Error != nil {
                ctx = append(ctx, "err", resp.Error.Message)
                if resp.Error.Data != nil {
                    ctx = append(ctx, "errdata", resp.Error.Data)
                }
                h.log.Warn("Served "+msg.Method, ctx...)
            } else {
                h.log.Debug("Served "+msg.Method, ctx...)
            }
            return resp
        case msg.hasValidID(): // ID长度大于0,且不以{或者[开头
            return msg.errorResponse(&invalidRequestError{"invalid request"})
        default:
            return errorMessage(&invalidRequestError{"invalid request"})
        }
    }
    
    // handleCall processes method calls.
    func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
        if msg.isSubscribe() {
            return h.handleSubscribe(cp, msg)
        }
        var callb *callback
        if msg.isUnsubscribe() {
            callb = h.unsubscribeCb
        } else {
            callb = h.reg.callback(msg.Method)
        }
        if callb == nil {
            return msg.errorResponse(&methodNotFoundError{method: msg.Method})
        }
        args, err := parsePositionalArguments(msg.Params, callb.argTypes)
        if err != nil {
            return msg.errorResponse(&invalidParamsError{err.Error()})
        }
        start := time.Now()
        answer := h.runMethod(cp.ctx, msg, callb, args)
    
        // Collect the statistics for RPC calls if metrics is enabled.
        // We only care about pure rpc call. Filter out subscription.
        if callb != h.unsubscribeCb {
            rpcRequestGauge.Inc(1)
            if answer.Error != nil {
                failedRequestGauge.Inc(1)
            } else {
                successfulRequestGauge.Inc(1)
            }
            rpcServingTimer.UpdateSince(start)
            newRPCServingTimer(msg.Method, answer.Error == nil).UpdateSince(start)
        }
        return answer
    }

    代码片段15. 执行消息处理

    文件:rpc/handler.go

    调用栈:serveSingleRequest()->h.handleMsg()

    接收请求,解析消息,获得请求的方法名和参数后,开始具体执行请求的方法。代码片段15就是这个过程,终于到了RPC执行服务的部分了。

    解析好的json消息结构体传递给handleMsg()做具体执行。

    第3行,这部分跳过,一般在订阅模式下需要执行一些快速响应操作。

    第6~15行,h.startCallProc() 接受一个函数作为参数。这个函数定义直接写在了参数列表中。

    先看第18~26行,首先协程同步的WaitGroup增加1(第19行),这个变量用于协程计数,完成后调用WaitGroup.Done()减1,主线程如果处于等待,需要等WaitGroup所有写成结束,计数归零后才继续运行。第24行,调用传入的函数fn,在第6~15行中定义。参数是callProc类型。看一下定义,实际上就是传入了上下文ctx。

    执行第24行时候,我们就跟随程序跳到第6~15行,这部分最关键是第7行,其余部分都是执行后的结果处理,包括订阅模式下结果分发等。

    第7行转入执行handleCallMsg()方法(第32行),在这里,大部分情况下的方法调用是isCall()类型,那么就执行handleCall() 方法(第40行接第61行)。

    这时,查找具体方法入口,见第69行。这个查找过程在下面代码片段16中单独给出。可以看到,将jsonrpcMessage的method字段做分割处理,这个分隔符serviceMethodSeprator 的值是下划线"_"。假设传入method字段是"admin_peers",经过代码片段16的第3行处理,就会变成:elem[0] = "admin", elem[1] = "peers"。然后第9行是个查表操作,注意对map结构协程安全的操作方法是加锁。查找映射表找到了对应方法的入口地址,这个地址在代码片段6中的初始化时已经设置好。

    
    // callback returns the callback corresponding to the given RPC method name.
    func (r *serviceRegistry) callback(method string) *callback {
        elem := strings.SplitN(method, serviceMethodSeparator, 2)
        if len(elem) != 2 {
            return nil
        }
        r.mu.Lock()
        defer r.mu.Unlock()
        return r.services[elem[0]].callbacks[elem[1]]
    }

    代码片段16. RPC服务查表寻找方法过程

    文件:rpc/service.go

    调用栈:serveSingleRequest()->handler.handleMsg()->handler.startCallProc()->handler.handleCall()->serviceRegistry.callback()

    找到方法后,再去找参数代码片段15的第17行。

    方法入口和参数都准备好后,直接调用即可完成一次RPC服务。见代码片段15第79行。

    至此,RPC服务的执行过程我们也从头到尾过了一遍。最后再分析一下方法调用中的参数解析,这里用到了一些golang的反射机制。见下文代码片段17.

    rpc/service.go
    // callback is a method callback which was registered in the server
    type callback struct {
        fn          reflect.Value  // the function
        rcvr        reflect.Value  // receiver object of method, set if fn is method
        argTypes    []reflect.Type // input argument types
        hasCtx      bool           // method's first argument is a context (not included in argTypes)
        errPos      int            // err return idx, of -1 when method cannot return error
        isSubscribe bool           // true if this is a subscription callback
    }
    
    // parsePositionalArguments tries to parse the given args to an array of values with the
    // given types. It returns the parsed values or an error when the args could not be
    // parsed. Missing optional arguments are returned as reflect.Zero values.
    func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]reflect.Value, error) {
        dec := json.NewDecoder(bytes.NewReader(rawArgs))
        var args []reflect.Value
        tok, err := dec.Token()
        switch {
        case err == io.EOF || tok == nil && err == nil:
            // "params" is optional and may be empty. Also allow "params":null even though it's
            // not in the spec because our own client used to send it.
        case err != nil:
            return nil, err
        case tok == json.Delim('['):
            // Read argument array.
            if args, err = parseArgumentArray(dec, types); err != nil {
                return nil, err
            }
        default:
            return nil, errors.New("non-array args")
        } // 以上错误处理
        // Set any missing args to nil.
        for i := len(args); i < len(types); i++ {
            if types[i].Kind() != reflect.Ptr {
                return nil, fmt.Errorf("missing value for required argument %d", i)
            }
            args = append(args, reflect.Zero(types[i]))
        }
        return args, nil
    }
    
    func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Value, error) {
        args := make([]reflect.Value, 0, len(types))
        for i := 0; dec.More(); i++ {
            if i >= len(types) {
                return args, fmt.Errorf("too many arguments, want at most %d", len(types))
            }
            argval := reflect.New(types[i]) // 初始化一个type类型的空值
            if err := dec.Decode(argval.Interface()); err != nil { // 解析参数填入
                return args, fmt.Errorf("invalid argument %d: %v", i, err)
            }
            if argval.IsNil() && types[i].Kind() != reflect.Ptr { // 空值报错
                return args, fmt.Errorf("missing value for required argument %d", i)
            }
            args = append(args, argval.Elem()) // 填入Value
        }
        // Read end of args array.
        _, err := dec.Token()
        return args, err
    }

    代码片段17. 参数的解析过程

    文件:rpc/json.go

    调用栈:serveSingleRequest()->handler.handleMsg()->handler.startCallProc()->handler.handleCall()->parsePositionalArguments()

    代码片段17是参数解析的过程:

    第16行,解码json串,参数列表是以'['开头,因此如果正常参数列表,就会在第27行进入parseArgumentArray()

    第44行,进入参数列表解析函数。types目标方法的参数类型列表,具体在rpc/sevice.go中定义,见第6行。这里创建一个参数队列,类型是是反射Value值,是一个动态值,实际上是一个空接口。

    第45~57行,对解析的参数列表,逐个遍历。第49行先创建一个空值,类型是types[i]中定义的类型,然后将json值解码到argval类型的接口中,如果能成功动态转换赋值就解析出一个参数,否则出错,解析成功后,将参数值填入到args切片中。这种参数结构一个缺点是调用传参顺序必须与方法声明的参数顺序一致,如果想用任意顺序和字典形式传参,需要将函数接收的参数声明称结构struct形式,用json编码机制传传递,具体实现可以网上搜索例子。

    整个解析的过程都是通过反射这一动态类型解析机制实现的,特点是灵活,所有类型都是运行时才确定,缺点当然是性能较差,反射机制性能肯定比静态数据解析差很多,但也是为了灵活性牺牲了一定性能。

    三、TinyGethRPC:一个简化的Go语言版RPC框架

    经过前文的分析,对Geth的RPC机制有了一个全面的了解。现在进入实践环节——将http RPC部分抽取出来,形成一个轻量级的RPC框架。

    先来梳理一下相关模块:

    首先,Geth顶层是结构是Node,一个Node下运行若干服务,包括p2p网络、eth数据库、http、websocket等服务,我们只需要一个http服务,因此只需要Node结构中相应的httpSever分支即可,我们不妨把底层结构就定义为server。

    其次,RPC服务框架大概涉及到node、rpc两个模块,大概10余个文件。

    再者,有关websocket、ipc以及订阅等相关处理内容的逻辑都可以清除。

    经过精简后,构建的TinyGethRPC目录结构如下:

    ├── go.mod
    ├── go.sum
    ├── main.go
    ├── rpc
    │   ├── errors.go
    │   ├── handler.go
    │   ├── http.go
    │   ├── json.go
    │   ├── service.go
    │   ├── types.go
    │   └── utils.go
    └── server
        ├── api.go
        ├── config.go
        └── httpserver.go

    TinyGethRPC目录结构

    main.go是程序主入口,服务端口直接硬编码在程序中,可以根据需要修改。

    server/api.go文件比较重要,用户可以根据自己需求添加程序逻辑。api.go文件内容如下所示

    
    package server
    
    import (
        "fmt"
        "TinyGethRPC/rpc"
    )
    
    type PublicServerAPI struct{
    
    }
    
    // apis returns the collection of built-in RPC APIs.
    func Apis() []rpc.API {
        return []rpc.API{
                {
                    Namespace: "tiny",
                    Version:   "1.0",
                    Service:   &PublicServerAPI{},
                    Public:    true,
                },
        }
    }
    
    // Test RPC function
    func (s *PublicServerAPI) HelloWorld(name string) string {
        res := fmt.Sprintf("%s, hello world!\n", name)
        return  res
    }
    

    sever/api.go中只定义了一个方法HelloWorld(),返回一个字符串。用户可以在其中加入自己的逻辑实现更复杂的RPC应用。

    运行go build编译文件后,运行TinyGethRPC启动服务,默认服务端口9988。我们发出一个http请求,执行一下HelloWorld方法。注意请求时候,方法名是tiny_helloWorld,传入一个参数"zhangsan",可以看到下面的返回结果与程序定义的一致。

    在下文例子中,按照列表传入参数必须与函数声明一一对应,可以通过结构体和json编码方式,支持更复杂参数类型,感兴趣可以自行搜索相关材料。

    请求命令行:
    curl -XPOST http://localhost:9988 -H 'content-type: application/json' -d'{"method": "tiny_helloWorld", "params":["Zhangsan"], "id":1}'
    返回结果:
    {"jsonrpc":"2.0","id":1,"result":"Zhangsan, hello world!\n"}

    四、结语

    本文从Geth的源代码实现入手,分析了Geth节点的RPC实现机制。进一步地,通过对源代码裁剪,构建了一个简单的基于http协议的RPC服务框架,利用这个框架可以实现一些简单的RPC服务业务。

    文中介绍内容是我日常工作中的一些经验总结,错误疏漏之处在所难免,包括代码实例中的内容可能还存在很多有待优化地方,欢迎批评指正。随着未来工作深入,可能会对以太坊实现的机制进行更深一步挖掘,包括不限于哈希算法、节点数据库设计、网络通信协议以及以太坊虚拟机实现机制等。期待这些经验总结能够对读者实践有所帮助。

    ...全文
    2847 回复 打赏 收藏 转发到动态 举报
    写回复
    用AI写文章
    回复
    切换为时间正序
    请发表友善的回复…
    发表回复

    45

    社区成员

    发帖
    与我相关
    我的任务
    社区描述
    这里是CSDN讨论Web产品技术和产业发展的大本营, 欢迎所有Web3业内和关心Web3的朋友们.
    社区管理员
    • Web3天空之城
    加入社区
    • 近7日
    • 近30日
    • 至今
    社区公告
    暂无公告

    试试用AI创作助手写篇文章吧