RPC在点我达网关的实践一

牧云云

RPC 是什么

  • RPC(Remote Procedure Call) 译为远端过程调用。即在一台机子上能调用到另外一台机子上的服务;
  • RPC 可以基于 HTTP 调用也可以基于 TCP 调用。基于 TCP 调用性能更佳,但是实现也更为复杂;
  • RPC 通常要实现两部分协议,一个是应用层协议(如 JSON),一个是用来传输数据的通讯层协议(如 Dubbo);

RPC 调用过程

  1. 调用方通过本地网关调用相应服务;
  2. 网关将服务名和参数封装为 RPC 对象传给客户端 RPC 框架(@dwd/noob-client);
  3. 客户端 RPC 框架(@dwd/noob-client)将数据转化成二进制形式,然后以 TCP 的形式传递给服务端 RPC 框架;
  4. 服务端 RPC 框架将二进制数据反序列化为 RPC 对象,并交由服务方处理,服务端处理后返回结果;
  5. 执行上述阶段的逆序操作;

关于 RPC 细节点,可以阅读 聊聊 Node.js RPC(一)— 协议

结合代码分析 Java 包的调用过程

初始化阶段 —— RPC 调用之前

在项目刚启动阶段,网关项目会通过 zk(路由管理系统) 进行路由寻址(寻找相应后端组的服务),后续网关项目就能和相应的后端组的服务直接通讯了。

在初始化阶段,网关项目会将项目信息、注册区域(异地多活)、所需服务等传递给 @dwd/noob-client,相关代码如下:

import { Client } from '@dwd/noob-client'

const client = new Client({  
  application: {                // 项目信息
    name: config.name,
  },
  registry: config.registry,    // 注册区域
  reference: config.references, // 所需服务
  routerServer: { address: routerAddress },
})

return client.init().then(() => {  
  for (const r of config.references) {
    loadService(r.id)
  }
})

根据 xml 文件生成相关的包文件

在项目伊始时,会通过后端给出的 xml 文件,根据脚本生成相应包的 ts 文件。比如以下代码为骑手服务组的一个包的 ts 文件。

import { Reference } from '@dwd/noob'  
import { provideService, javaType} from '../../util/dubbo'

@provideService('com.dianwoba.rider.elastic.provider.RiderElasticProvider')
export default class RiderElasticProvider implements com.dianwoba.rider.elastic.provider.RiderElasticProvider {

  constructor(private _ref: Reference<com.dianwoba.rider.elastic.provider.RiderElasticProvider> ) {}

  async pageSearch(@javaType({"name":"com.dianwoba.rider.elastic.domain.dto.param.RiderEsParamDTO","isPrimitive":false,"isArray":false,"isGeneric":false}) paramDTO: com.dianwoba.rider.elastic.domain.dto.param.RiderEsParamDTO) : Promise<com.dianwoba.dubbo.base.result.Pagination<com.dianwoba.rider.elastic.domain.dto.result.RiderEsDTO>> {
    return this._ref.invoke('pageSearch', Array.from(arguments))
  }

  ...
}

RiderElasticProvider 的实例化

网关的核心架构使用了 IoC 框架 inverify。在 IoC 架构下,实例化的过程在容器内进行。

在上述代码点开 provideService 方法,可以看到实例化的过程 new ServiceClass(reference),代码如下:

// dubbo.ts
import { Client } from '@dwd/noob-client'

export const provideService = <T>(interfaceName: string) => (ServiceClass: interfaces.Newable<T>) => {  
  const reference = Client.reference.get(interfaceName)                    // 获取远程引用资源
  const service = proxyService(new ServiceClass(reference), interfaceName) // 代理实例对象,下文解析
  moduleBind<T>(interfaceName).toConstantValue(service)                    // 依赖注入
}

reference 对象的 __proto__ 属性上有 invoke 方法(继承自 @dwd/noob)

proxyService 代理

proxyService 方法的作用给包类的每个方法做了一层代理,代理的具体作用是将传入参数和包名包装为 RPC 对象。

// dubbo.ts
function proxyService<T>(service:T, identifier: string) {  
  let handler = {
    apply: function(target: Function, thisArgument: any, argumentsList: any[]) {
      let funcName = target.name
      // 获取前面代码 @javaType({}) 中声明的对象
      let paramTypes: JavaType[] = Reflect.getMetadata(JAVATYPE_SYMBOL, service, funcName)
      // 工厂模式创建转化为 RPC 对象的方法
      let transform = converter.methodParameterTransformerFactory(...paramTypes)
      // 将传入参数和包名转化为 RPC 对象
      let args = transform(argumentsList)
      return target.apply(thisArgument, args)
    }
  }
  Object.getOwnPropertyNames(Object.getPrototypeOf(service)).filter(name => name !== 'constructor' && !name.startsWith('_')).forEach(methodName => {
    service[methodName] = new Proxy(service[methodName], handler) // 将原型链上 service[methodName] 赋值到 service[methodName] 上,并用 handler 进行代理
  })
  return service
}

番外笔记

ts 的语法

class Test {  
  constructor(private _ref) {}
}

ts 转换为 js 的形式如下

function Test(_ref) {  
  this._ref = _ref
}