RPC

RPC是指远程过程调用,允许在远程服务器上调用函数(过程),就像调用本地函数一样。与HTTP客户-服务器通信不同的是,分配不是通过HTTP方法和URL进行的,而是通过函数名称进行的。要发送的数据作为正常的函数参数被传递,服务器上的函数调用结果被发回给客户端。

RPC的优点是客户端-服务器的抽象更轻,因为没有使用头文件、URL、查询字符串或类似的东西。其缺点是,服务器上通过RPC的功能不能轻易被浏览器调用,往往需要一个特殊的客户端。

RPC的一个关键特征是,客户端和服务器之间的数据被自动序列化和反序列化。由于这个原因,类型安全的RPC客户端通常是可能的。因此,一些RPC框架强迫用户以特定的格式提供类型(参数类型和返回类型)。这可以是自定义DSL的形式,如gRPC(协议缓冲区)和GraphQL的代码生成器,也可以是JavaScript模式生成器的形式。数据的额外验证也可以由RPC框架提供,但不是所有的框架都支持。

在 Deepkit RPC 中,函数的类型是从 TypeScript 代码本身中提取的(见 Runtime Types),因此不需要使用代码生成器或手动定义它们。Deepkit支持参数和结果的自动序列化和反序列化。只要从Validation中定义了额外的约束,这些约束也会被自动验证。这使得通过RPC进行的通信极为类型安全和有效。Deepkit RPC中通过`rxjs`对流媒体的支持,也使这个RPC框架成为实时通信的合适工具。

为了说明RPC背后的概念,下面的代码。

//server.ts
class Controller {
    hello(title: string): string {
        return 'Hello ' + title
    }
}

像 "hello "这样的方法通常在服务器上的一个类中实现,然后可以从远程客户端调用。

//client.ts
const client = new RpcClient('localhost');
const controller = client.controller<Controller>();

const result = await controller.hello('World'); // => 'Hello World';

由于RPC从根本上是基于异步通信的,所以通信主要是通过HTTP,但也可以通过TCP或WebSockets发生。这意味着TypeScript本身的所有函数调用都被转换为一个`承诺'。有了相应的`await',可以异步接收结果。

Isomorphic TypeScript

只要一个项目在客户端(通常是前端)和服务器(后端)使用TypeScript,它就被称为Isomorphic TypeScript。那么,基于TypeScript的类型安全的RPC框架对这样的项目来说是特别有利的,因为类型可以在客户端和服务器之间共享。

为了利用这一点,在两边都使用的类型应该被换成一个单独的文件或包。在各自的页面上进行导入,然后再次合并它们。

//shared.ts
export class User {
    id: number;
    username: string;
}

interface UserControllerApi {
    getUser(id: number): Promise<User>;
}

//server.ts
import { User } from './shared';
class UserController implements UserControllerApi {
    async getUser(id: number): Promise<User> {
        return await datbase.query(User).filter({id}).findOne();
    }
}

//client.ts
import { UserControllerApi } from './shared';
const controller = client.controller<UserControllerApi>();
const user = await controller.getUser(2); // => User

接口`UserControllerApi`在这里充当了客户端和服务器之间的契约。服务器必须正确地实现这一点,客户端可以消费它。

向后兼容可以用与普通本地API相同的方式实现:要么将新的参数标记为可选,要么添加一个新的方法。

虽然也可以通过`import type { UserController } from './server.ts’直接导入`UserController',但这有其他缺点,比如不支持名义类型(这意味着类的实例不能用`instanceof’检查)。

Installation

由于Deepkit RPC是基于运行时类型的,因此必须正确安装`@deepkit/type`。参见运行时类型安装

如果这样做成功了,就可以安装`@deepkit/rpc`或Deepkit框架,该框架已经在引擎盖下使用该库。

npm install @deepkit/rpc

请注意,`@deepkit/rpc`中的控制器类是基于TypeScript装饰器的,这个功能必须通过`experimentalDecorators`相应启用。

如果服务器和客户端都有自己的`package.json`,则必须安装`@deepkit/rpc`包。

要通过TCP与服务器通信,必须在客户端和服务器上安装`@deepkit/rpc-tcp`包。

对于WebSocket通信,它还需要服务器上的包。而浏览器中的客户端则使用官方标准中的 "WebSocket"。

npm install @deepkit/rpc-tcp

一旦客户端要通过WebSocket在没有`WebSocket`的环境中使用(例如NodeJS),客户端就需要`ws`包。

npm install ws

使用

下面是一个基于WebSockets和`@deepkit/rpc`的低级API的全功能例子。一旦使用Deepkit框架,控制器就会通过应用模块提供,而不需要手动实例化RpcKernel。

文件:server.ts_

import { rpc, RpcKernel } from '@deepkit/rpc';
import { RpcWebSocketServer } from '@deepkit/rpc-tcp';

@rpc.controller('myController');
export class Controller {
    @rpc.action()
    hello(title: string): string {
        return 'Hello ' + title;
    }
}

const kernel = new RpcKernel();
kernel.registerController(Controller);
const server = new RpcWebSocketServer(kernel, 'localhost:8081');
server.start();

档。client.ts_

import { RpcWebSocketClient } from '@deepkit/rpc';
import type { Controller } from './server';

async function main() {
    const client = new RpcWebSocketClient('localhost:8081');
    const controller = client.controller<Controller>('myController');

    const result = await controller.hello('World');
    console.log('result', result);

    client.disconnect();
}

main().catch(console.error);

服务器控制器

远程过程调用中的 "过程 "也被称为行动。这样的动作被定义为一个类中的方法,并且用`@rpc.action`装饰器来标记。该类本身被`@rpc.controller`装饰器标记为控制器,并分配了一个唯一的名字。然后在客户中引用这个名字,以解决正确的控制器。任何数量的控制器都可以被定义和注册。

import { rpc } from '@deepkit/rpc';

@rpc.controller('myController');
class Controller {
    @rpc.action()
    hello(title: string): string {
        return 'Hello ' + title;
    }

    @rpc.action()
    test(): boolean {
        return true;
    }
}

只有那些同时被标记为"@rpc.action() "的方法才能被客户端处理。

类型必须是明确指定的,不能推断。这一点很重要,因为序列化器需要确切地知道类型是什么,以便将它们转换成二进制数据(BSON)或JSON。

客户端控制器

RPC中的正常流程是,客户可以在服务器上执行功能。然而,在Deepkit RPC中,服务器也有可能对客户端执行功能。为了允许这一点,客户也可以注册一个控制器。

TODO

依赖注入

控制器类由`@deepkit/injector`的依赖注入容器管理。如果使用Deepkit框架,这些控制器会自动访问提供控制器的模块的提供者。

在Deepkit框架中,控制器被实例化在依赖性注入作用域`rpc`中,因此所有的控制器都能自动访问这个作用域中的各种提供者。这些额外的提供者是`HttpRequest`(可选),RpcInjectorContextSessionStateRpcKernelConnection,和`ConnectionWriter`。

import { RpcKernel, rpc } from '@deepkit/rpc';
import { App } from '@deepkit/app';
import { Database, User } from './database';

@rpc.controller('my')
class Controller {
    constructor(private database: Database) {}

    @rpc.action()
    async getUser(id: number): Promise<User> {
        return await this.database.query(User).filter({id}).findOne();
    }
}

new App({
    providers: [{provide: Database, useValue: new Database}]
    controllers: [Controller],
}).run();

然而,只要手动实例化一个`RpcKernel',DI容器也可以被传递到那里。然后,RPC控制器通过这个DI容器被实例化。

import { RpcKernel, rpc } from '@deepkit/rpc';
import { InjectorContext } from '@deepkit/injector';
import { Database, User } from './database';

@rpc.controller('my')
class Controller {
    constructor(private database: Database) {}

    @rpc.action()
    async getUser(id: number): Promise<User> {
        return await this.database.query(User).filter({id}).findOne();
    }
}

const injector = InjectorContext.forProviders([
    Controller,
    {provide: Database, useValue: new Database},
]);
const kernel = new RpcKernel(injector);
kernel.registerController(Controller);

参见依赖注入以了解更多。

名义类型

当客户端收到来自函数调用的数据时,它先前在服务器上被序列化,然后在客户端被反序列化。如果现在在函数的返回类型中使用了类,它们会在客户端被重构,但会失去它们的名义身份和所有方法。为了抵制这种行为,类可以通过一个唯一的ID注册为名义类型。对于所有在RPC API中使用的类都应该这样做。

要注册一个类,必须使用装饰器`@entity.name('id')`。

import { entity } from '@deepkit/type';

@entity.name('user')
class User {
    id!: number;
    firstName!: string;
    lastName!: string;
    get fullName() {
        return this.firstName + ' ' + this.lastName;
    }
}

只要这个类现在被用作一个函数的结果,它的身份就被保留了。

const controller = client.controller<Controller>('controller');

const user = await controller.getUser(2);
user instanceof User; //true when @entity.name is used, and false if not

错误转发

RPC函数可以抛出错误。默认情况下,这些错误被转发到客户端并在那里再次抛出。如果使用了自定义错误类,其名义类型应被启用。参见RPC名义类型

@entity.name('@error:myError')
class MyError extends Error {}

//server
class Controller {
    @rpc.action()
    saveUser(user: User): void {
        throw new MyError('Can not save user');
    }
}

//client
//[MyError] makes sure the class MyError is known in runtime
const controller = client.controller<Controller>('controller', [MyError]);

try {
    await controller.getUser(2);
} catch (e) {
    if (e instanceof MyError) {
        //ops, could not save user
    } else {
        //all other errors
    }
}

安全

默认情况下,所有RPC函数都可以从任何客户端调用。点对点通信功能在默认情况下也被激活。为了能够准确地设置哪个客户被允许做什么,`RpcKernelSecurity’类可以被覆盖。

import { RpcKernelSecurity, Session, RpcControllerAccess } from '@deepkit/type';

//contains default implementations
class MyKernelSecurity extends RpcKernelSecurity {
    async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise<boolean> {
        return true;
    }

    async isAllowedToRegisterAsPeer(session: Session, peerId: string): Promise<boolean> {
        return true;
    }

    async isAllowedToSendToPeer(session: Session, peerId: string): Promise<boolean> {
        return true;
    }

    async authenticate(token: any): Promise<Session> {
        throw new Error('Authentication not implemented');
    }

    transformError(err: Error) {
        return err;
    }
}

要使用这个,要么把它的一个实例传给`RpcKernel`。

const kernel = new RpcKernel(undefined, new MyKernelSecurity);

或者在Deepkit Framework应用程序的情况下,`RpcKernelSecurity’类被覆盖了一个提供者。

import { App } from '@deepkit/type';
import { RpcKernelSecurity } from '@deepkit/rpc';
import { FrameworkModule } from '@deepkit/framework';

new App({
    controllers: [MyRpcController],
    providers: [
        {provide: RpcKernelSecurity, useClass: MyRpcKernelSecurity}
    ],
    imports: [new FrameworkModule]
}).run();

认证/会话

对象`session`默认是一个匿名会话,这意味着客户端没有认证自己。一旦它想进行认证,就会调用`authenticate`方法。 `authenticate`方法收到的令牌来自客户端,可以有任何值。

一旦客户端设置了一个令牌,只要调用第一个RPC函数或手动`client.connect()`,就会进行认证。

const client = new RpcWebSocketClient('localhost:8081');
client.token.set('123456789');

const controller = client.controller<Controller>('myController');

这里`RpcKernelSecurity.authenticate`接收到令牌`123456789`并可以相应地返回另一个会话。这个返回的会话将被传递给所有其他的方法,如`hasControllerAccess`。

import { Session, RpcKernelSecurity } from '@deepkit/rpc';

class UserSession extends Session {
}

class MyKernelSecurity extends RpcKernelSecurity {
    async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise<boolean> {
        if (controllerAccess.controllerClassType instanceof MySecureController) {
            //MySecureController requires UserSession
            return session instanceof UserSession;
        }
        return true;
    }

    async authenticate(token: any): Promise<Session> {
        if (token === '123456789') {
            return new UserSession('username', token);
        }
        throw new Error('Authentication failed');
    }
}

控制器访问

`hasControllerAccess`方法可用于确定客户端是否被允许执行特定的RPC功能。这个方法在每次RPC函数调用时都会执行。如果它返回 "false",则拒绝访问,并向客户抛出一个错误。

在`RpcControllerAccess`中,有几条关于RPC函数的有价值的信息。

interface RpcControllerAccess {
    controllerName: string;
    controllerClassType: ClassType;
    actionName: string;
    actionGroups: string[];
    actionData: { [name: string]: any };
}

组和其他数据可以通过装饰器`@rpc.action()`来改变。

class Controller {
    @rpc.action().group('secret').data('role', 'admin')
    saveUser(user: User): void {
    }
}


class MyKernelSecurity extends RpcKernelSecurity {
    async hasControllerAccess(session: Session, controllerAccess: RpcControllerAccess): Promise<boolean> {
        if (controllerAccess.actionGroups.includes('secret')) {
            //todo: check
            return false;
        }
        return true;
    }
}

变形误差

由于抛出的错误会自动转发到客户端,并附上所有信息,如错误信息和堆栈跟踪,这可能会不必要地公布敏感信息。要改变这一点,可以在`transformError`方法中修改抛出的错误。

class MyKernelSecurity extends RpcKernelSecurity {
    transformError(error: Error) {
        //wrap in new error
        return new Error('Something went wrong: ' + error.message);
    }
}

请注意,一旦错误被转换为一般的 "错误",完整的堆栈跟踪和错误的身份就会丢失。因此,在客户端的错误上不能使用`instanceof`检查。

如果在两个微服务之间使用Deepkit RPC,并且客户端和服务器因此处于开发者的完全控制之下,那么转变错误就很少有必要。另一方面,如果客户端在一个未知的人的浏览器中运行,那么你应该在`transformError’中非常小心地确定你想披露的信息。如果有疑问,每个错误都应该用一个通用的 "错误 "进行转换,以确保没有内部细节被泄露。在这一点上,记录错误将是一个好主意。

依赖注入

如果直接使用Deepkit RPC库,则要实例化`RpcKernelSecurity`类本身。如果这个类需要一个数据库或一个记录器,这必须被传入本身。

如果使用Deepkit框架,该类由依赖注入容器实例化,从而自动访问应用程序的所有其他提供者。

另见依赖性注入

流动的RxJS

TODO

传输协议

Deepkit RPC支持几种传输协议。WebSockets是兼容性最好的协议(因为浏览器支持它),同时也支持流媒体等所有功能。TCP通常更快,非常适合服务器(微服务)或非浏览器客户端之间的通信。

Deepkit的RPC HTTP协议是一个变种,在浏览器中特别容易调试,因为每个函数调用都是一个HTTP请求,但也有其局限性,如不支持RxJS流。

HTTP

TODO: 还没有实施。

WebSockets

@deepkit/rpc-tcp `RpcWebSocketServer`和浏览器WebSocket或Node `ws`包。

TCP

@deepkit/rpc-tcp RpcNetTcpServer`和`RpcNetTcpClientAdapter

Peer To Peer

TODO