apache / dubbo-js

The Typescript implementation of Apache Dubbo. An RPC and microservice framework for Node.js and Web development.
https://dubbo.apache.org/
Apache License 2.0
765 stars 160 forks source link

Mvp 版本讨论 #322

Open creasy2010 opened 1 year ago

creasy2010 commented 1 year ago

以此为Mvp目标,大家看看是否有要补充的。


# filename: helloworld.proto

syntax = "proto3";
package grpc.health.v1;

option go_package = "dubbojs/mvp";

message HealthCheckRequest {
  string service = 1;
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
    SERVICE_UNKNOWN = 3;  // Used only by the Watch method.
  }
  ServingStatus status = 1;
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

service Mvp {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
fengwei5280 commented 1 year ago

辛苦东哥,protobuf这边没有问题; invoker这边比较关心最终生成代码的调用方式,期待 @creasy2010 更新

hufeng commented 1 year ago

ok,可以以这个例子作为开始

hufeng commented 1 year ago

@creasy2010 目前生成的代码是什么样子

hufeng commented 1 year ago

因为protocolbuff 强依赖proto或者proto生成的valid type文件定义,所以IDL生成的代码需要和序列化模块耦合起来 接下来讨论IDL和serialization的协作机制以及代码生成部分。

序列化接口设计

interface Serialization {
  // load 所有的proto文件,获取request类型
  loadProto(protoPath: string): void;

 // encode 请求数据,后置type参数且是可选,为了将来切换其他协议如不需要type类型,可以不传,我们接口可以不改 
  encode<T>(data: T, type?: string): Buffer;

// decode 请求数据,后置type参数且是可选,为了将来切换其他协议如不需要type类型,可以不传,我们接口可以不改 
  decode<T>(data: Buffer, type?: string): T;
}

序列化模块的设计

IDL 代码生成

针对Mvp的proto定义,对client stub service代码样例:

// define service interface
export interface Mvp {
  SayHello(req: HelloRequest): Promise<HelloReply>;
  Check(req: HealthCheckRequest): Promise<HealthCheckResponse>;
}

// define enum
enum ServingStatus {
  UNKNOWN = 0,
  SERVING = 1,
  NOT_SERVING = 2,
  SERVICE_UNKNOWN = 3, // Used only by the Watch method.
}

// define request && response, 实际代码可以根据不同的namespace生成到不同的目录
export interface HealthCheckResponse {
  status: ServingStatus;
}

export interface HealthCheckRequest {
  service: string;
}

export interface HelloRequest {
  name: string;
}

export interface HelloReply {
  message: string;
}

// define service metadata

import ds from 'dubbo-serialization'

// TODO 或者对于ecode和decode的过程,IDL生成代码只返回 {path, data} 由invoke来负责底层的调用
export const Mvp = {
  SayHello: { 
     path: "/helloworld.Mvp/SayHello", 
     encode(data: HelloRequest) {
       return ds.encode(data, ` hellorequest在对象path路径 `)
    },
    decode(data: Buffer) {
       return  ds.decode(data, `helloreplay 的path路径 `)
    }
  },
  Check: { 
    path: "/helloworld.Mvp/Check",
    encode(data: HealthCheckRequest) {
       return ds.encode(data, ` HealthCheckRequest在对象path路径 `)
    },
   decode(data: Buffer) {
       return  ds.decode(data, `helloreplay 的path路径 `)
    }
 },
};

// server 端

// 生成抽象类
export abstract class AbstractMvp {
  path = "/helloworld.Greeter";

  methods = {
    SayHello: this.SayHello.bind(this),
    Check: this.Check.bind(this),
  };

  abstract SayHello(req: HelloRequest): Promise<HelloReply>;
  abstract Check(req: HealthCheckRequest): Promise<HealthCheckResponse>;
}

// 生成实现类
export class MvpService extends AbstractMvp {
  SayHello(req: HelloRequest): Promise<HelloReply> {
    throw new Error("Method not implemented.");
  }
  Check(req: HealthCheckRequest): Promise<HealthCheckResponse> {
    throw new Error("Method not implemented.");
  }
}
fengwei5280 commented 1 year ago

基于Mvp,实际invoker的调用方式如下:

// define DubboClientsTstubService
export interface DubboClientsTstubService {
  mvp: IMvp;
}

// index.ts
import DubboClient from './../dubbo'
import stubService from './stubServices'
import {DubboClientsTstubService} from './mvpService'

const dubbo = new DubboClient<DubboClientsTstubService>(stubService)
wawIsready commented 1 year ago

因为protocolbuff 强依赖proto或者proto生成的valid type文件定义,所以IDL生成的代码需要和序列化模块耦合起来 接下来讨论IDL和serialization的协作机制以及代码生成部分。

序列化接口设计

interface Serialization {
  // load 所有的proto文件,获取request类型
  loadProto(protoPath: string): void;

 // encode 请求数据,后置type参数且是可选,为了将来切换其他协议如不需要type类型,可以不传,我们接口可以不改 
  encode<T>(data: T, type?: string): Buffer;

// decode 请求数据,后置type参数且是可选,为了将来切换其他协议如不需要type类型,可以不传,我们接口可以不改 
  decode<T>(data: Buffer, type?: string): T;
}

序列化模块的设计

  • 模块名 dubbo-serialization
  • 实现上述接口方法

IDL 代码生成

针对Mvp的proto定义,对client stub service代码样例:

// define service interface
export interface Mvp {
  SayHello(req: HelloRequest): Promise<HelloReply>;
  Check(req: HealthCheckRequest): Promise<HealthCheckResponse>;
}

// define enum
enum ServingStatus {
  UNKNOWN = 0,
  SERVING = 1,
  NOT_SERVING = 2,
  SERVICE_UNKNOWN = 3, // Used only by the Watch method.
}

// define request && response, 实际代码可以根据不同的namespace生成到不同的目录
export interface HealthCheckResponse {
  status: ServingStatus;
}

export interface HealthCheckRequest {
  service: string;
}

export interface HelloRequest {
  name: string;
}

export interface HelloReply {
  message: string;
}

// define service metadata

import ds from 'dubbo-serialization'

// TODO 或者对于ecode和decode的过程,IDL生成代码只返回 {path, data} 由invoke来负责底层的调用
export const Mvp = {
  SayHello: { 
     path: "/helloworld.Mvp/SayHello", 
     encode(data: HelloRequest) {
       return ds.encode(data, ` hellorequest在对象path路径 `)
    },
    decode(data: Buffer) {
       return  ds.decode(data, `helloreplay 的path路径 `)
    }
  },
  Check: { 
    path: "/helloworld.Mvp/Check",
    encode(data: HealthCheckRequest) {
       return ds.encode(data, ` HealthCheckRequest在对象path路径 `)
    },
   decode(data: Buffer) {
       return  ds.decode(data, `helloreplay 的path路径 `)
    }
 },
};

// server 端

// 生成抽象类
export abstract class AbstractMvp {
  path = "/helloworld.Greeter";

  methods = {
    SayHello: this.SayHello.bind(this),
    Check: this.Check.bind(this),
  };

  abstract SayHello(req: HelloRequest): Promise<HelloReply>;
  abstract Check(req: HealthCheckRequest): Promise<HealthCheckResponse>;
}

// 生成实现类
export class MvpService extends AbstractMvp {
  SayHello(req: HelloRequest): Promise<HelloReply> {
    throw new Error("Method not implemented.");
  }
  Check(req: HealthCheckRequest): Promise<HealthCheckResponse> {
    throw new Error("Method not implemented.");
  }
}

client 里的 path 和 methods 的一一对应关系是不是要和 server 这边的逻辑一致?

wawIsready commented 1 year ago

经讨论确认,dubbo-server 的抽象类生成形式如下所示:

export abstract class AbstractMvp {

metaData:{
    '/helloworld.Greeter/SayHello': this.SayHello.bind(this),
    '/helloworld.Greeter/Check': this.Check.bind(this)
  };

  abstract SayHello(req: HelloRequest): Promise<HelloReply>;
  abstract Check(req: HealthCheckRequest): Promise<HealthCheckResponse>;
}

dubbo-client 模块依此做对应调整。 @hufeng @fengwei5280

godkun commented 1 year ago

调整后的 transport mvp

pr入口:https://github.com/apache/dubbo-js/pull/325

入口

import { DubboClientTransport } from './client'
import { DubboServerTransport } from './server'

export { DubboClientTransport, DubboServerTransport }

client transport 代码

import { debug } from 'debug'
import http2 from 'node:http2'
import { IDubboClientTransport, DubboContext } from './transport'

// init log
const log = debug('dubbo3:transport:client')

export class DubboClientTransport implements IDubboClientTransport {
  // transport 实例
  private transport: any
  private ctx: DubboContext

  constructor(opts: DubboContext) {
    this.ctx = opts
    this.connect()
  }

  get url() {
    return this.ctx.url
  }

  /**
   * 建立连接
   */
  connect() {
    this.transport = http2.connect(this.url)

    this.transport.once('connect', () => {
      log('has connected')
    })
  }

  /**
   * 发送消息
   * @param msg
   */
  async send(msg: DubboContext): Promise<void> {
    this.transport.request(msg)
  }
}

server transport 代码:

import debug from 'debug'
import EventEmitter from 'node:events'
import http2 from 'node:http2'
import { IDubboServerTransport, DubboContext } from './transport'

// init log
const log = debug('dubbo3:transport:client')

export class DubboServerTransport
  extends EventEmitter
  implements IDubboServerTransport
{
  private ctx: DubboContext
  transport: any

  constructor(opts: DubboContext) {
    super()
    this.ctx = opts
    this.transport = this.start()
  }

  get url() {
    return this.ctx.url
  }

  get port() {
    return this.ctx.port
  }

  /**
   * 启动服务端 transport
   * @returns
   */
  start() {
    const server = http2.createServer()
    server.on('stream', (stream, headers) => {
      log(stream)
      stream.on('data', (data) => {
        log(data)
        // TODO:
      })
      stream.on('end', () => {
        log('end...')
        // TODO: 通知 client
      })
      stream.on('error', (error) => {
        log(error)
      })
    })
    server.listen(this.port)
    return server
  }
}

tansport.ts 接口定义:

export interface DubboContext {
  url: string
  body?: Object | null
  port: number
}

export interface IDubboClientTransport {
  send(msg: DubboContext): Promise<any>
}

export interface IDubboServerTransport {
  // start(msg: DubboContext): Promise<any>
}

依赖:

@fengwei5280

fengwei5280 commented 1 year ago

context的数据和定义 @godkun

export default class Context {
    path: string
    method: string
    args: Array<any>
    resolve: Fuction
    reject: Fuction
      // final result
    body: any
}