相关文章推荐

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',可以异步接收结果。

只要一个项目在客户端(通常是前端)和服务器(后端)使用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’检查)。

下面是一个基于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);
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
//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
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;

Um diese zu nutzen wird entweder dem RpcKernel der Provider übergeben:

const kernel = new RpcKernel([{provide: RpcKernelSecurity, useClass: MyKernelSecurity, scope: 'rpc'}]);

Oder im Falle einer Deepkit Framework Anwendung die Klasse RpcKernelSecurity mit einem Provider in der App überschrieben.

import { App } from '@deepkit/type';
import { RpcKernelSecurity } from '@deepkit/rpc';
import { FrameworkModule } from '@deepkit/framework';
new App({
    controllers: [MyRpcController],
    providers: [
        {provide: RpcKernelSecurity, useClass: MyRpcKernelSecurity, scope: 'rpc'}
    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;
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框架,该类由依赖注入容器实例化,从而自动访问应用程序的所有其他提供者。

另见依赖性注入

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

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

TODO: 还没有实施。

WebSockets

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

@deepkit/rpc-tcp RpcNetTcpServer`和`RpcNetTcpClientAdapter

 
推荐文章