一、准备工作

注意: 本文以 macOS 为例,其他操作系统的安装、使用方法请自行 google。


$ brew install zookeeper
$ zkServer start
ZooKeeper JMX enabled by default
Using config: /usr/local/etc/zookeeper/zoo.cfg
Starting zookeeper ... STARTED
$ npm i egg-init -g


二、初始化项目


先通过下面命令初始化项目

npm init egg --type microservice rpc-demo


初始化好以后,目录结构如下:


.
└── rpc-demo
    ├── README.md
    ├── app
    │   ├── controller
    │   │   └── home.js
    │   └── router.js
    ├── assembly
    │   ├── dubbo-demo-api-1.0-SNAPSHOT-sources.jar
    │   └── dubbo-demo-api-1.0-SNAPSHOT.jar
    ├── config
    │   ├── config.default.js
    │   ├── plugin.js
    │   └── proxy.js
    ├── package.json
    └── test
        └── app
            └── controller
                └── home.test.js


三、定义接口


我们选择 protobuf 的接口定义语言来描述我们的接口,详细可参考官方文档,文件放在 <app_root>/proto/UserService.proto 


syntax = "proto3";

package org.eggjs.rpc;

service UserService {
    rpc echoUser (User) returns (User) {}
}

message User {
    int32 id = 1;
    string name = 2;
    string address = 3;
    int64 salary = 4;
}


这个接口描述定义了一个 service: org.eggjs.rpc.UserService ,它下面有一个 echoUser 方法,而这个方法的入口参数和返回值都是一个 User 类型


Protobuf 接口定义限制:接口方法要求 有且只有一个入口参数和返回值


定义好接口以后,我们讲 UserService.proto 保存到 <app_root>/proto 目录下


.
├── README.md
├── app
│   ├── controller
│   │   └── home.js
│   ├── proxy
│   │   └── UserService.js
│   ├── public
│   └── router.js
├── appveyor.yml
├── config
│   ├── config.default.js
│   ├── plugin.js
│   └── proxy.js
├── logs
├── package.json
├── proto
│   └── UserService.proto
└── test
    └── app
        └── controller
            └── home.test.js


四、实现和暴露接口


接口定义好以后,我们需要实现它,并将它以某种方式暴露给调用者。


我们先来实现上面定义的接口,在 <app_root>/app/rpc 目录下创建一个叫 UserService.js 的文件。


注意:这里的文件名需要和上面 proto 定义里面的 service 同名


UserService 只有一个 echoUser 的方法,所以我们可以简单实现如下:

'use strict';

module.exports = class UserService {
  constructor(ctx) {
    this.ctx = ctx;
  }
  
  async echoUser(user) {
    const { ctx } = this;
    ctx.logger.info('calling echoUser() with: %j', user);
    return user;
  }
};


<app_root>/config/config.default.js 中做相应配置,将服务暴露出去


'use strict';

exports.rpc = {
  registry: {
    address: '127.0.0.1:2181', // 根据实际进行配置
  },
  client: {},
  server: {
    port: 12200,
    namespace: 'org.eggjs.rpc',
    version: '1.0',
    group: 'SOFA',
  },
};



五、调用接口


配置 <app_root>/config/proxy.js,并通过工具来生成调用代码


// <app_root>/config/proxy.js
'use strict';

module.exports = {
  version: '1.0',
  group: 'SOFA',
  services: [{
    appName: 'demo',
    api: {
      UserService: {
        interfaceName: 'org.eggjs.rpc.UserService',
      },
    },
  }],
};



配置好以后,需要通过 egg-rpc-generator 这个工具来生成接口的调用代码,在脚手架里我们已经在 package.json 里的 scripts 里配置好 rpc 命令


{
  "scripts": {
    "start": "npm run init && egg-scripts start --daemon --title=egg-server-rpc-demo",
    "stop": "egg-scripts stop --title=egg-server-rpc-demo",
    "dev": "egg-bin dev",
    "debug": "egg-bin debug",
    "test": "npm run lint -- --fix && npm run test-local",
    "test-local": "egg-bin test",
    "cov": "egg-bin cov",
    "lint": "eslint .",
    "ci": "npm run lint && npm run cov",
    "rpc": "egg-rpc-generator",
    "init": "npm run rpc",
    "autod": "autod"
  }
  ...
}


egg-rpc-generator 内置了三个插件

  • protobuf  根据 proto 文件来生成接口元数据
  • jar2proxy 根据 jar 包来生成接口调用代码
  • jsdoc2jar 根据 jsdoc 信息来生成 jar 包

后面两个插件都是用于和 Java 进行多语言互调的,要求你安装 Java 1.8 环境,如果你没有这样的需求,可以只开启 protobuf 插件,将 rpc 命令修改为: "egg-rpc-generator -p protobuf" 


运行 npm run rpc 


$ npm run rpc
> rpc-demo@1.0.0 rpc /Users/gaoxiaochen/workspace/rpc-demo
> egg-rpc-generator -p protobuf

[EggRpcGenerator] framework: /Users/gaoxiaochen/workspace/rpc-demo/node_modules/egg-cloud, baseDir: /Users/gaoxiaochen/workspace/rpc-demo
[ProtoRPCPlugin] found "org.eggjs.rpc.UserService" in proto file
[ProtoRPCPlugin] save all proto info into "/Users/gaoxiaochen/workspace/rpc-demo/run/proto.json"


正常的话,会在 <app_root>/app/proxy 下生成 UserService.js 文件

// Don't modified this file, it's auto created by egg-rpc-generator

'use strict';

const path = require('path');

/* eslint-disable */
/* istanbul ignore next */
module.exports = app => {
  const consumer = app.rpcClient.createConsumer({
    interfaceName: 'org.eggjs.rpc.UserService',
    targetAppName: 'rpc',
    version: '1.0',
    group: 'SOFA',
    proxyName: 'UserService',
  });

  if (!consumer) {
    // `app.config['rpc.rpc.service.enable'] = false` will disable this consumer
    return;
  }

  app.beforeStart(async() => {
    await consumer.ready();
  });

  class UserService extends app.Proxy {
    constructor(ctx) {
      super(ctx, consumer);
    }

    async echoUser(req) {
      return await consumer.invoke('echoUser', [ req ], { 
        ctx: this.ctx,
      });
    }
  }

  return UserService;
};
/* eslint-enable */


那么,我们现在可以在 controller/ service 里调用生成的 proxy 了,例如:


// <app_root>/app/controller/home.js
'use strict';

const Controller = require('egg-cloud').Controller;

class HomeController extends Controller {
  async index() {
    const ctx = this.ctx;
    ctx.body = await ctx.proxy.userService.echoUser({
      id: 123456,
      name: '宗羽',
      address: '蚂蚁 C 空间',
      salary: 100000000,
    });
  }
}
module.exports = HomeController;


配置 HTTP 路由:


// <app_root>/app/router.js
'use strict';

module.exports = app => {
  const { router, controller } = app;

  router.get('/', controller.home.index);
};


六、启动应用、验证


执行 npm run dev 本地启动应用


$ npm run dev

> rpc-demo@1.0.0 dev /Users/gaoxiaochen/workspace/rpc-demo
> egg-bin dev

2019-06-24 10:45:25,612 INFO 15023 [master] node version v12.4.0
2019-06-24 10:45:25,614 INFO 15023 [master] egg-cloud version 0.2.0
2019-06-24 10:45:26,191 INFO 15023 [master] agent_worker#1:15024 started (575ms)
2019-06-24 10:45:26,796 INFO 15023 [master] egg-cloud started on http://127.0.0.1:7001 (1182ms)


访问 http://127.0.0.1:7001 看到下面的结果,说明成功了

image.png


七、本地开发硬负载


如果你没有 zookeeper 环境,或者你在本地开发的时候希望将服务强制指定到某一个地址(比如本机),那么你可以通过硬负载配置


// <app_root>/config/config.default.js
'use strict';

exports.rpc = {
  // registry: {
  //   address: '127.0.0.1:2181',
  // },
  client: {
    'demo.rpc.service.url': '127.0.0.1',
  },
  server: {
    port: 12200,
    namespace: 'org.eggjs.rpc',
    version: '1.0',
    group: 'SOFA',
  },
};


这里,我注释掉了 registry 的配置,意思是服务发现不走注册中心,那么我需要手动配置服务的地址,于是在 client 节点下按照  "${appName}.rpc.service.url": "${ip}:${port}" 格式来配置,appName 就是上面 proxy.js 里配置的服务提供的者的 appName(所以上面提到它是必填项),后面跟的是服务的地址,可以配 ip 地址或者 vip,如果不带端口号,那么默认是 12200。