Client RPC

概要

从一个客户端程序来访问节点有多种方式,但是如果你的客户端一个用一个 JVM 兼容语言所编写的话,那么最方便的方式就是使用客户端类库来跟节点互动。这个类库会通过使用一个消息队列协议来跟运行着的节点进行连接,然后会提供一个简单的 RPC 接口来跟节点互动。你可以像通常那样去调用一个 Java 对象,然后来回的消息交互它会帮你控制。

这个客户端类库的起点是 CordaRPCClient 类。它提供了 一个 start 方法,返回了一个 CordaRPCConnection,包含了一个 CordaRPCops 的实现,可以通过 Kotlin 中的 proxy 和 Java 中的 getProxy() 来进行访问。 RPCs 返回的 observables 可以被订阅来接收节点的任何更新的 ongoing stream。

注意:这个返回的 CordaRPCConnection 即使创建和消费很小一部分服务器端资源都是非常昂贵的(消耗大)。当你使用完了它之后,调用它的 close 方法。或者你也可以在 CordaRPCClient 上使用 use 方法,这个会在传入的 lambda 结束之后自动清理。不要为你的每次调用创建一个新的 proxy - 重用一个已经存在的。

RPC 权限

如果一个节点的 owner 想跟它的节点通过 RPC 来互动的话(比如读取节点 storage 中的内容),他必须要定义一个或多个 RPC 用户。每个用户会通过一个用户名和密码来进行验证,还会被赋予一系列的RPC 能够使用的权限。

RPC 用户信息会被添加到节点的 node.conf 文件中的 rpcUsers 列表中:

rpcUsers=[
    {
        username=exampleUser
        password=examplePass
        permissions=[]
    }
    ...
]

默认的,RPC 用户不允许执行任何的 RPC 操作。

赋予 flow 权限

使用 StartFlow.<fully qualified flow name> 来给一个 RPC 用户提供开始某个指定的 flow 的权限:

rpcUsers=[
    {
        username=exampleUser
        password=examplePass
        permissions=[
            "StartFlow.net.corda.flows.ExampleFlow1",
            "StartFlow.net.corda.flows.ExampleFlow2"
        ]
    }
    ...
]

你也可以使用 InvokeRpc.startFlow 来给 RPC 用户提供启动任何 flow 的权限:

rpcUsers=[
    {
        username=exampleUser
        password=examplePass
        permissions=[
            "InvokeRpc.startFlow"
        ]
    }
    ...
]

赋予其他的 RPC 权限

可以使用 InvokeRpc.<rpc method name> 来给 RPC 用户分配执行一个指定的 RPC 操作的权限:

rpcUsers=[
    {
        username=exampleUser
        password=examplePass
        permissions=[
            "InvokeRpc.nodeInfo",
            "InvokeRpc.networkMapSnapshot"
        ]
    }
    ...
]

赋予所有权限

使用 ALL 将允许 RPC 用户进行所有 RPC 操作(包括启动任何的 flow):

rpcUsers=[
    {
        username=exampleUser
        password=examplePass
        permissions=[
            "ALL"
        ]
    }
    ...
]

RPC 安全管理

设置 rpcUsers 提供了一个简单的方式来为一个固定的一些用户赋予 RPC 权限,但是有一些很明显的不足。为了支持更高安全和灵活性,Corda 提供了额外的安全功能,比如:

这些功能是由 node.confsecurity 字段里的一系列选项来控制的。下边的例子演示了如何配置可从一个远程的数据库取回的用户验证信息和权限信息,密码是以哈希加密过的格式并且开启了用户数据在内存中 caching:

security = {
    authService = {
        dataSource = {
            type = "DB",
            passwordEncryption = "SHIRO_1_CRYPT",
            connection = {
               jdbcUrl = "<jdbc connection string>"
               username = "<db username>"
               password = "<db user password>"
               driverClassName = "<JDBC driver>"
            }
        }
        options = {
             cache = {
                expireAfterSecs = 120
                maxEntries = 10000
             }
        }
    }
}

也可以通过指定一个 INMEMORY 类型的 dataSource 来在 security 结构中指定一个用户的静态列表:

security = {
    authService = {
        dataSource = {
            type = "INMEMORY",
            users = [
                {
                    username = "<username>",
                    password = "<password>",
                    permissions = ["<permission 1>", "<permission 2>", ...]
                },
                ...
            ]
        }
    }
}

注意:一个有效的配置不能够同时指定 rpcUserssecurity 字段。这么做会在启动节点的时候造成异常

dataSource 结构定义了数据提供者来提供用户的验证信息和权限信息。这里有两种支持的数据源类型,通过 dataSource.type 字段来定义: INMEMORY:通过 users 字段指定的用户验证信息和权限信息的静态列表 DB:可以通过 connection 描述的 JDBC 连接的一个外部的 RDBMS。注意:不像 INMEMORY case,在一个用户的数据库中,权限是被分配给角色的而不是个人。当前的实现期望数据库根据下边的 schema 来存储数据:

注意:这里并没有强制每个列的 SQL 类型(尽管我们的测试对于 usernamerole_name 使用的是 VARCHAR SQL 类型,passwordTEXT 类型)。在每个表中也可以按照需要增加额外的列。

当安全性是很重要的时候,将密码以明文的形式存储是不被鼓励的。密码默认是明文的格式,除非对 passwordEncryption 字段指定了不同的格式,比如:

passwordEncryption = SHIRO_1_CRYPT

SHIRO_1_CRYPT 表示 Apache Shiro fully reversible Modular Crypt 格式,这是当前唯一支持的非明文密码的哈希加密格式。可以使用 Apache Shiro Hasher 命令行工具 来生成哈希加密密码。

在用户验证信息和权限信息的外部数据源之上的一个 cache 层在很多情况下会很大程度的改善效率,但是会带来一个可控的对底层的数据的抓取延迟。Caching 默认是被 disabled,可以通过定义 security.authService 中的 options.cache 字段来开启,比如:

options = {
     cache = {
        expireAfterSecs = 120
        maxEntries = 10000
     }
}

这个会开启一个包含在节点的内存中的非持久化的 cache,最大的输入数量设置为 maxEntries,这个 entries 会在 expireAfterSecs 秒钟后过期。

Observables

RPC 系统使用一种特殊的方式来处理 observables。当一个方法返回 observable 的时候,或者直接返回,或者作为一个 response object graph 的子对象返回,客户端会创建一个跟服务器端匹配的 observable。服务器端 observable 发出的对象会被放进一个队列中,这个队列会被客户端来消费。返回的 observable 设置可能会发出含有更多的 observables 的 object graphs,它会像你期望的那样来工作的。

这个特性是要有一定的消耗的:server 必须要不断地接收由服务器端发出的对象直到你把他们下载下来。注意,服务器端的 observation buffer 是固定的,一旦被用光,客户端会变得很慢。你应该 subscribe 所有返回的 observables,否则的话随着 observations 的进入,客户端的内存就会不断的被使用。如果你不想要一个 observable,那么你先 subscribe 然后立即 unsubscribe,这样会清理掉客户端的 buffer,也会让 server 停止 streaming。如果你的 app 退出了的话,那么服务器端的资源会被自动释放。

注意:如果你在客户端泄露了一个 observable,然后它得到了搜集到的垃圾,你会在 log 中看到一个警示提示,并且 observable 会被自动的 unsubscribe。但是不要完全依赖于这个,因为垃圾回收是无法预测( non-deterministic)的。

未来

一个方法也能够在它的 object graph 中返回一个 ListenableFuture 并且会像对待 observables 的方式来对待它。在未来调用 cancel 方法会解除对任何未来的值的订阅并释放所有的资源。

版本(Versioning)

客户端 RPC 协议是使用节点的 Platform Version 来定义版本的。当一个代理(proxy)被创建后,server 会查询它的版本,你可以指定你的最小版本要求。在后期的版本中被添加的方法都会带有 @RPCSinceVersion 的注解。如果你使用了一个 server 不支持的方法,一个 UnsupportedOperationException 的异常会被抛出。如果你想知道 server 的版本,只需要使用 protocolVersion 属性(比如 Java 中的 getProtocolVersion)。

线程安全(Thread safety)

一个代理(proxy)是线程安全的、阻塞的(blocking)并且允许多个 RPCs 同时存在。任何返回的和你订阅的 observables 将会有对象会在后台运行的线程池中被有序地 emitted。每个 observable stream 会被绑定到一个单独的线程,但是要注意到的是,两个独立的 observables 可能在不同的线程上调用他们对应的 callbacks。

异常处理

如果 RPC 基础架构本身出了问题,一个 RPCException 会被抛出。如果你调用了一个要求比当前 server 支持的版本更高的一个方法, UnsupportedOperationException 异常会被抛出。否则的话,如果 server 的实现抛出了一个异常,这个异常会被序列化(serialised)并且被重新抛到客户端,因为它实际是从被调用的 RPC 方法中被抛出来的。

Wire 协议

客户端 RPC wire 协议是在 net/corda/client/rpc/RPCApi.kt 中被定义和文档化的。

Wire 安全

CordaRPCClient 一个类型为 SSLConfiguration 的可选的构造函数参数,默认为 null,这个允许跟节点使用 SSL 进行沟通。默认的 null 值表示在 RPC 的上下文中是没有使用 SSL 的。

Corda 节点的白名单类

CorDapps 对于通过 RPC 所使用的任何类,都要使用 Corda 的序列化(serialization) framework 将他们添加至白名单,除非这些类已经在 DefaultWhitelist 中默认地被添加到白名单中了。添加白名单的操作既可以通过 plugin architecture 或者使用 @CordaSerializable 注解来实现。