├── .autod.conf.js ├── .eslintignore ├── .eslintrc ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── History.md ├── LICENSE ├── README.md ├── README.zh_CN.md ├── agent.js ├── app.js ├── app └── extend │ ├── agent.js │ ├── application.js │ └── application.unittest.js ├── config ├── config.default.js └── config.unittest.js ├── lib ├── base_proxy.js ├── client.js ├── mock_connection.js └── server.js ├── package.json └── test ├── custom_registry.test.js ├── fixtures └── apps │ ├── custom-registry │ ├── app │ │ ├── router.js │ │ └── rpc │ │ │ └── ProtoService.js │ ├── config │ │ ├── config.default.js │ │ └── proxy.js │ ├── lib │ │ ├── client.js │ │ └── registry.js │ ├── package.json │ └── proto │ │ └── ProtoService.proto │ ├── hardload │ ├── app │ │ ├── proxy │ │ │ └── ProtoService.js │ │ ├── router.js │ │ └── rpc │ │ │ └── ProtoService.js │ ├── config │ │ ├── config.default.js │ │ └── proxy.js │ ├── package.json │ └── proto │ │ └── ProtoService.proto │ ├── jar2proxy │ ├── app │ │ └── rpc │ │ │ └── DemoService.js │ ├── assembly │ │ ├── dubbo-demo-api-1.0-SNAPSHOT-sources.jar │ │ └── dubbo-demo-api-1.0-SNAPSHOT.jar │ ├── config │ │ ├── apiMeta.json │ │ ├── config.default.js │ │ └── proxy.js │ └── package.json │ ├── mock │ ├── assembly │ │ ├── dubbo-demo-api-1.0-SNAPSHOT-sources.jar │ │ └── dubbo-demo-api-1.0-SNAPSHOT.jar │ ├── config │ │ ├── config.default.js │ │ └── proxy.js │ └── package.json │ ├── rpcserver │ ├── app │ │ └── rpc │ │ │ ├── HelloService.js │ │ │ └── MathService.js │ ├── config │ │ └── config.default.js │ └── package.json │ ├── self-publish │ ├── app │ │ └── rpc │ │ │ └── HelloService.js │ ├── config │ │ └── config.default.js │ └── package.json │ └── sofarpc │ ├── app │ └── rpc │ │ └── ProtoService.js │ ├── config │ ├── config.default.js │ └── proxy.js │ ├── package.json │ └── proto │ └── ProtoService.proto ├── hardload.test.js ├── index.test.js ├── init.sh ├── jar2proxy.test.js ├── mock.test.js ├── self_publish.test.js └── server.test.js /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | devdep: [ 12 | 'egg', 13 | 'egg-bin', 14 | 'autod', 15 | 'autod-egg', 16 | 'eslint', 17 | 'eslint-config-egg', 18 | 'egg-rpc-generator', 19 | 'webstorm-disable-index', 20 | ], 21 | exclude: [ 22 | './test/fixtures', 23 | './docs', 24 | './coverage', 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | coverage 3 | examples/**/app/public 4 | logs 5 | run 6 | zookeeper-3.4.6/ 7 | coverage 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ##### Checklist 12 | 13 | 14 | - [ ] `npm test` passes 15 | - [ ] tests and/or benchmarks are included 16 | - [ ] documentation is changed or added 17 | - [ ] commit message follows commit guidelines 18 | 19 | ##### Affected core subsystem(s) 20 | 21 | 22 | 23 | ##### Description of change 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .* 64 | !.github 65 | !.eslintignore 66 | !.eslintrc 67 | !.gitignore 68 | !.travis.yml 69 | !.autod.conf.js 70 | *.bin 71 | 72 | run 73 | test/fixtures/apps/**/proxy 74 | test/fixtures/apps/**/proxy_class 75 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | - '10' 6 | before_install: 7 | - 'wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.6/zookeeper-3.4.6.tar.gz' 8 | - 'tar xf zookeeper-3.4.6.tar.gz' 9 | - 'mv zookeeper-3.4.6/conf/zoo_sample.cfg zookeeper-3.4.6/conf/zoo.cfg' 10 | - './zookeeper-3.4.6/bin/zkServer.sh start' 11 | install: 12 | - npm i npminstall && npminstall --registry=https://registry.npm.taobao.org 13 | script: 14 | - npm run ci 15 | after_script: 16 | - npminstall codecov && codecov 17 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.3.1 / 2019-01-17 3 | ================== 4 | 5 | **fixes** 6 | * [[`3d8b65e`](http://github.com/eggjs/egg-rpc/commit/3d8b65e4856bfd7049bf4262649e9c2e95f8ca16)] - fix: add apiMeta to rpc server (gxcsoccer <>) 7 | 8 | 1.3.0 / 2018-11-07 9 | ================== 10 | 11 | **features** 12 | * [[`fe76cd5`](http://github.com/eggjs/egg-rpc/commit/fe76cd58ad08a8defd3ac1c8082b84391c5e6f40)] - feat: registry class support app (leoner <>) 13 | 14 | 1.2.0 / 2018-11-06 15 | ================== 16 | 17 | **features** 18 | * [[`065dd45`](http://github.com/eggjs/egg-rpc/commit/065dd45647f4825c9c7ec5d391215d8e7c94643c)] - feat: add mockProxy api (gxcsoccer <>) 19 | 20 | 1.1.0 / 2018-11-03 21 | ================== 22 | 23 | **features** 24 | * [[`21f8b90`](http://github.com/eggjs/egg-rpc/commit/21f8b904fd3c72394b6ff81bba6da454e19b0d5b)] - feat: auto load proxy class (gxcsoccer <>) 25 | 26 | 1.0.0 / 2018-11-02 27 | ================== 28 | 29 | **others** 30 | ,fatal: 没有发现名称,无法描述任何东西。 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 eggjs core team and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egg-rpc 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][travis-image]][travis-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![David deps][david-image]][david-url] 7 | [![Known Vulnerabilities][snyk-image]][snyk-url] 8 | [![npm download][download-image]][download-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/egg-rpc-base.svg?style=flat-square 11 | [npm-url]: https://npmjs.org/package/egg-rpc-base 12 | [travis-image]: https://img.shields.io/travis/eggjs/egg-rpc.svg?style=flat-square 13 | [travis-url]: https://travis-ci.org/eggjs/egg-rpc 14 | [codecov-image]: https://img.shields.io/codecov/c/github/eggjs/egg-rpc.svg?style=flat-square 15 | [codecov-url]: https://codecov.io/github/eggjs/egg-rpc?branch=master 16 | [david-image]: https://img.shields.io/david/eggjs/egg-rpc.svg?style=flat-square 17 | [david-url]: https://david-dm.org/eggjs/egg-rpc 18 | [snyk-image]: https://snyk.io/test/npm/egg-rpc-base/badge.svg?style=flat-square 19 | [snyk-url]: https://snyk.io/test/npm/egg-rpc-base 20 | [download-image]: https://img.shields.io/npm/dm/egg-rpc-base.svg?style=flat-square 21 | [download-url]: https://npmjs.org/package/egg-rpc-base 22 | 23 | egg-rpc plugin for egg framework 24 | 25 | ## Install 26 | 27 | ```bash 28 | $ npm i egg-rpc-base --save 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Enable the plugin 34 | 35 | Change `${app_root}/config/plugin.js` to enable egg-rpc plugin: 36 | 37 | ```js 38 | exports.rpc = { 39 | enable: true, 40 | package: 'egg-rpc-base', 41 | }; 42 | ``` 43 | 44 | ### RPC Service Discovery 45 | 46 | By default, We use zookeeper for service discovery. Therefore you need to configure the zk address: 47 | 48 | ```js 49 | // ${app_root}/config/config.${env}.js 50 | config.rpc = { 51 | registry: { 52 | address: '127.0.0.1:2181', // configure your real zk address 53 | }, 54 | }; 55 | ``` 56 | 57 | We plan to provide more implementations of service discovery. And also you can implement it by yourself, you can follow this [article](https://github.com/eggjs/egg-sofa-rpc/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0%E5%AE%9E%E7%8E%B0) 58 | 59 | ### RPC Client 60 | 61 | By using the plugin, you can call rpc services provided by other system. 62 | 63 | #### 1. Get the RPC Interface Definition 64 | 65 | We use [protobuf interface definition lanaguge](https://developers.google.com/protocol-buffers/docs/proto3) to describe the RPC service. So you need to get the \*.proto files and put them into `${app_root}/proto` folder 66 | 67 | #### 2. Global RPC Client Configuration 68 | 69 | Configure RPC Client information in ${app_root}/config/config.{env}.js: 70 | 71 | ```js 72 | // ${app_root}config/config.${env}.js 73 | exports.rpc = { 74 | client: { 75 | responseTimeout: 3000, 76 | }, 77 | }; 78 | ``` 79 | 80 | - `responseTimeout`(optional): RPC timeout in milliseconds, default value is 3000 81 | 82 | #### 3. Configure the Interface in proxy.js 83 | 84 | `${app_root}/config/proxy.js` is a very important config file for rpc client, you should configure the services you needed, then executing the `egg-rpc-generator` tool to generate the proxy files. 85 | 86 | Let's see a simple example of proxy.js. It declare a interface named: `org.eggjs.rpc.test.ProtoService` provided by `rpc-demo` application 87 | 88 | ```js 89 | 'use strict'; 90 | 91 | module.exports = { 92 | services: [{ 93 | appName: 'rpc-demo', 94 | api: { 95 | ProtoService: 'org.eggjs.rpc.test.ProtoService', 96 | }, 97 | }], 98 | }; 99 | ``` 100 | 101 | Refer this [acticle](https://github.com/eggjs/egg-sofa-rpc/wiki/RPC-%E4%BB%A3%E7%90%86%EF%BC%88Proxy%EF%BC%89%E9%85%8D%E7%BD%AE) for more details 102 | 103 | #### 4. Generate the Proxy 104 | 105 | Run `egg-rpc-generator` to generate the proxy files. After running success, it will generate a file: `ProtoService.js` under `${app_root}/app/proxy` 106 | 107 | ```bash 108 | $ egg-rpc-generator 109 | 110 | [EggRpcGenerator] framework: /egg-rpc-demo/node_modules/egg, baseDir: /egg-rpc-demo 111 | [ProtoRPCPlugin] found "org.eggjs.rpc.test.ProtoService" in proto file 112 | [ProtoRPCPlugin] save all proto info into "/egg-rpc-demo/run/proto.json" 113 | ``` 114 | 115 | #### 5. Call the Service 116 | 117 | You can call the RPC service by using `ctx.proxy.proxyName`. The proxyName is `key` value of api object you configure in proxy.js. In our example, it's ProtoService, and proxyName using lower camelcase, so it's `ctx.proxy.protoService` 118 | 119 | ```js 120 | 'use strict'; 121 | 122 | const Controller = require('egg').Controller; 123 | 124 | class HomeController extends Controller { 125 | async index() { 126 | const { ctx } = this; 127 | const res = await ctx.proxy.protoService.echoObj({ 128 | name: 'gxcsoccer', 129 | group: 'A', 130 | }); 131 | ctx.body = res; 132 | } 133 | } 134 | 135 | module.exports = HomeController; 136 | ``` 137 | 138 | As above, you can call remote service as a local method. 139 | 140 | ### RPC Server 141 | 142 | By using the plugin, you also can publish your own RPC service to other system. 143 | 144 | #### 1. Define the RPC Interface 145 | 146 | Writing the \*.proto files, and put them into `${app_root}/proto` folder 147 | 148 | ``` 149 | # ProtoService.proto 150 | syntax = "proto3"; 151 | 152 | package org.eggjs.rpc.test; 153 | option java_multiple_files = true; // 可选 154 | option java_outer_classname = "ProtoServiceModels"; // 可选 155 | 156 | service ProtoService { 157 | rpc echoObj (EchoRequest) returns (EchoResponse) {} 158 | } 159 | 160 | message EchoRequest { 161 | string name = 1; 162 | Group group = 2; 163 | } 164 | 165 | message EchoResponse { 166 | int32 code = 1; 167 | string message = 2; 168 | } 169 | 170 | enum Group { 171 | A = 0; 172 | B = 1; 173 | } 174 | ``` 175 | 176 | #### 2. Global RPC Server Configuration 177 | 178 | Configure RPC Server information in ${app_root}/config/config.{env}.js: 179 | 180 | ```js 181 | module.exports = { 182 | rpc: { 183 | server: { 184 | namespace: 'org.eggjs.rpc.test', 185 | }, 186 | }, 187 | }, 188 | ``` 189 | 190 | - `namespace`(required): the default namespace of all rpc service 191 | - `selfPublish`(optional): whether every node process publish service independent, default is true 192 | - `port`(optional): the TCP port will be listen on 193 | - `maxIdleTime`(optional): server will disconnect the socket if idle for long time 194 | - `responseTimeout`(optional): Number of milliseconds to wait for a response to begin arriving back from the remote system after sending a request 195 | - `codecType`(optional): recommended serialization method,default is protobuf 196 | 197 | #### 3. Implemenation the RPC Interface 198 | 199 | Put your implementation code under `${app_root}/app/rpc` folder 200 | 201 | ```js 202 | // ${app_root}/app/rpc/ProtoService.js 203 | exports.echoObj = async function(req) { 204 | return { 205 | code: 200, 206 | message: 'hello ' + req.name + ', you are in ' + req.group, 207 | }; 208 | }; 209 | ``` 210 | 211 | #### 4. RPC Unittest in Eggjs 212 | 213 | RPC Client Unittest (mock) 214 | 215 | ```js 216 | 'use strict'; 217 | 218 | const mm = require('egg-mock'); 219 | const assert = require('assert'); 220 | 221 | describe('test/mock.test.js', () => { 222 | let app; 223 | before(async function() { 224 | app = mm.app({ 225 | baseDir: 'apps/mock', 226 | }); 227 | await app.ready(); 228 | }); 229 | afterEach(mm.restore); 230 | after(async function() { 231 | await app.close(); 232 | }); 233 | 234 | it('should app.mockProxy ok', async function() { 235 | app.mockProxy('DemoService', 'sayHello', async function(name) { 236 | await sleep(1000); 237 | 238 | return 'hello ' + name + ' from mock'; 239 | }); 240 | 241 | const ctx = app.createAnonymousContext(); 242 | const res = await ctx.proxy.demoService.sayHello('gxcsoccer'); 243 | assert(res === 'hello gxcsoccer from mock'); 244 | }); 245 | }); 246 | ``` 247 | 248 | RPC Server Unittest 249 | 250 | ```js 251 | 'use strict'; 252 | 253 | const mm = require('egg-mock'); 254 | 255 | describe('test/index.test.js', () => { 256 | let app; 257 | before(async function() { 258 | app = mm.app({ 259 | baseDir: 'apps/rpcserver', 260 | }); 261 | await app.ready(); 262 | }); 263 | after(async function() { 264 | await app.close(); 265 | }); 266 | 267 | it('should invoke HelloService', done => { 268 | app.rpcRequest('org.eggjs.rpc.test.HelloService') 269 | .invoke('hello') 270 | .send([ 'gxcsoccer' ]) 271 | .expect('hello gxcsoccer', done); 272 | }); 273 | }); 274 | ``` 275 | 276 | For more details of `app.rpcRequest`, you can refer to this [acticle](https://github.com/eggjs/egg-sofa-rpc/wiki/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95-RPC-%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%96%B9%E6%B3%95) 277 | 278 | ## Docs 279 | 280 | - [RPC in Nodejs Part One](https://github.com/alipay/sofa-rpc-node/wiki/%E8%81%8A%E8%81%8A-Nodejs-RPC%EF%BC%88%E4%B8%80%EF%BC%89) 281 | - [Cross-Language Interoperability between Eggjs & SOFA](https://github.com/eggjs/egg-sofa-rpc/wiki/Eggjs-%E5%92%8C-SOFA-%E7%9A%84%E8%B7%A8%E8%AF%AD%E8%A8%80%E4%BA%92%E8%B0%83) 282 | - [Custom Service Discovery in Eggjs](https://github.com/eggjs/egg-sofa-rpc/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0%E5%AE%9E%E7%8E%B0) 283 | - [RPC Proxy Configuration in Eggjs](https://github.com/eggjs/egg-sofa-rpc/wiki/RPC-%E4%BB%A3%E7%90%86%EF%BC%88Proxy%EF%BC%89%E9%85%8D%E7%BD%AE) 284 | - [RPC Unittest in Eggjs](https://github.com/eggjs/egg-sofa-rpc/wiki/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95-RPC-%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%96%B9%E6%B3%95) 285 | 286 | ## How to Contribute 287 | 288 | Please let us know how can we help. Do check out [issues](https://github.com/eggjs/egg/issues) for bug reports or suggestions first. 289 | 290 | To become a contributor, please follow our [contributing guide](https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md). 291 | 292 | ## License 293 | 294 | [MIT](LICENSE) 295 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | # egg-rpc 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][travis-image]][travis-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![David deps][david-image]][david-url] 7 | [![Known Vulnerabilities][snyk-image]][snyk-url] 8 | [![npm download][download-image]][download-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/egg-rpc-base.svg?style=flat-square 11 | [npm-url]: https://npmjs.org/package/egg-rpc-base 12 | [travis-image]: https://img.shields.io/travis/eggjs/egg-rpc.svg?style=flat-square 13 | [travis-url]: https://travis-ci.org/eggjs/egg-rpc 14 | [codecov-image]: https://img.shields.io/codecov/c/github/eggjs/egg-rpc.svg?style=flat-square 15 | [codecov-url]: https://codecov.io/github/eggjs/egg-rpc?branch=master 16 | [david-image]: https://img.shields.io/david/eggjs/egg-rpc.svg?style=flat-square 17 | [david-url]: https://david-dm.org/eggjs/egg-rpc 18 | [snyk-image]: https://snyk.io/test/npm/egg-rpc-base/badge.svg?style=flat-square 19 | [snyk-url]: https://snyk.io/test/npm/egg-rpc-base 20 | [download-image]: https://img.shields.io/npm/dm/egg-rpc-base.svg?style=flat-square 21 | [download-url]: https://npmjs.org/package/egg-rpc-base 22 | 23 | egg-rpc 插件是为 egg 提供调用和发布 RPC 服务的能力 24 | 25 | ## 安装 26 | 27 | ```bash 28 | $ npm i egg-rpc-base --save 29 | ``` 30 | 31 | ## 用法 32 | 33 | ### 开启插件 34 | 35 | 通过 `${app_root}/config/plugin.js` 配置启动 egg-rpc 插件: 36 | 37 | ```js 38 | exports.rpc = { 39 | enable: true, 40 | package: 'egg-rpc-base', 41 | }; 42 | ``` 43 | 44 | ### RPC 服务发现 45 | 46 | 默认的服务发现依赖于 `zookeeper`,所以你需要配置一个 zk 的地址 47 | 48 | ```js 49 | // ${app_root}/config/config.${env}.js 50 | config.rpc = { 51 | registry: { 52 | address: '127.0.0.1:2181', // 根据实际情况配置 53 | }, 54 | }; 55 | ``` 56 | 57 | 后续我们还会提供更多的服务发现实现,你也可以根据自己的需求实现自己的 registry,详细可以参考:[自定义服务发现实现](https://github.com/eggjs/egg-sofa-rpc/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0%E5%AE%9E%E7%8E%B0) 58 | 59 | ### RPC 客户端 60 | 61 | 该插件提供调用其他系统暴露的 rpc 接口的能力 62 | 63 | #### 1. 获取接口定义 64 | 65 | 以 protobuf 为例,将 \*.proto 文件放置到 `${app_root}/proto` 目录下 66 | 67 | #### 2. 全局配置 68 | 69 | 可以在 `${app_root}/config/config.${env}.js` 做一些全局性的配置 70 | 71 | ```js 72 | // ${app_root}config/config.${env}.js 73 | exports.rpc = { 74 | client: { 75 | responseTimeout: 3000, 76 | }, 77 | }; 78 | ``` 79 | 80 | - `responseTimeout`(可选): RPC 的超时时长,默认为 3 秒 81 | 82 | #### 3. 申明要调用的接口 83 | 84 | RPC 客户端还有一个重要的配置文件是:`${app_root}/config/proxy.js`,你需要把你调用的服务配置到里面,然后通过 `egg-rpc-generator` 工具帮你生成本地调用代码。 85 | 86 | 让我们看一个最简单的配置,它的基本含义是:我需要调用 `rpc-demo` 应用暴露的 `org.eggjs.rpc.test.ProtoService` 这个服务。 87 | 88 | ```js 89 | 'use strict'; 90 | 91 | module.exports = { 92 | services: [{ 93 | appName: 'rpc-demo', 94 | api: { 95 | ProtoService: 'org.eggjs.rpc.test.ProtoService', 96 | }, 97 | }], 98 | }; 99 | ``` 100 | 101 | 详细的配置可以参考 [RPC 代理(Proxy)配置](https://github.com/eggjs/egg-sofa-rpc/wiki/RPC-%E4%BB%A3%E7%90%86%EF%BC%88Proxy%EF%BC%89%E9%85%8D%E7%BD%AE) 102 | 103 | #### 4. 生成本地调用代理 104 | 105 | 配置好了,运行 `egg-rpc-generator` 生成本地调用代理文件。运行成功后,会在 `${app_root}/app/proxy` 目录下生成一个 `ProtoSerivce.js` 文件 106 | 107 | ```bash 108 | $ egg-rpc-generator 109 | 110 | [EggRpcGenerator] framework: /egg-rpc-demo/node_modules/egg, baseDir: /egg-rpc-demo 111 | [ProtoRPCPlugin] found "org.eggjs.rpc.test.ProtoService" in proto file 112 | [ProtoRPCPlugin] save all proto info into "/egg-rpc-demo/run/proto.json" 113 | ``` 114 | 115 | #### 5. 调用服务 116 | 117 | 通过 `ctx.proxy.proxyName` 来访问生成的 proxy 代码,proxyName 就是上面 proxy.js 配置的 api 键值对中的 key。例如:上面配置的 ProtoService,但是需要特别注意的是 proxyName 会自动转成小驼峰形式,所以就是 `ctx.proxy.protoService`。 118 | 119 | ```js 120 | 'use strict'; 121 | 122 | const Controller = require('egg').Controller; 123 | 124 | class HomeController extends Controller { 125 | async index() { 126 | const { ctx } = this; 127 | const res = await ctx.proxy.protoService.echoObj({ 128 | name: 'gxcsoccer', 129 | group: 'A', 130 | }); 131 | ctx.body = res; 132 | } 133 | } 134 | 135 | module.exports = HomeController; 136 | ``` 137 | 138 | 和调用本地方法体验一模一样。 139 | 140 | 141 | ### RPC 服务端 142 | 143 | 该插件还可以暴露 SOFARPC 接口给其他应用调用 144 | 145 | #### 1. 定义接口 146 | 147 | 同样以 protobuf 为例,将接口定义放置到 `${app_root}/proto` 目录下。 148 | ``` 149 | # ProtoService.proto 150 | syntax = "proto3"; 151 | 152 | package org.eggjs.rpc.test; 153 | option java_multiple_files = true; // 可选 154 | option java_outer_classname = "ProtoServiceModels"; // 可选 155 | 156 | service ProtoService { 157 | rpc echoObj (EchoRequest) returns (EchoResponse) {} 158 | } 159 | 160 | message EchoRequest { 161 | string name = 1; 162 | Group group = 2; 163 | } 164 | 165 | message EchoResponse { 166 | int32 code = 1; 167 | string message = 2; 168 | } 169 | 170 | enum Group { 171 | A = 0; 172 | B = 1; 173 | } 174 | ``` 175 | 176 | #### 2. 全局配置 177 | 178 | 在 `${app_root}/config/config.${env}.js` 做一些配置 179 | 180 | ```js 181 | module.exports = { 182 | rpc: { 183 | server: { 184 | namespace: 'org.eggjs.rpc.test', 185 | }, 186 | }, 187 | }, 188 | ``` 189 | 190 | - `namespace`(必选): 接口的命名空间,所有的暴露的接口默认都在该命名空间下 191 | - `selfPublish`(可选): 是否每个 worker 进程独立暴露服务。nodejs 多进程模式下,如果多个进程共享一个端口,在 RPC 这种场景可能造成负载不均,所以 selfPublish 默认为 true,代表每个进程独立监听端口和发布服务 192 | - `port`(可选): 服务监听的端口(注意:在 selfPublish=true 时,监听的端口是基于这个配置生成的) 193 | - `maxIdleTime`(可选): 客户端连接如果在该配置时长内没有任何流量,则主动断开连接 194 | - `responseTimeout`(可选): 服务端建议的超时时长,具体的超时还是以客户端配置为准 195 | - `codecType`(可选): 推荐的序列化方式,默认为 protobuf 196 | 197 | #### 3. 接口实现 198 | 199 | 在 `${app_root}/app/rpc` 目录下放置接口的具体实现代码 200 | ```js 201 | // ${app_root}/app/rpc/ProtoService.js 202 | exports.echoObj = async function(req) { 203 | return { 204 | code: 200, 205 | message: 'hello ' + req.name + ', you are in ' + req.group, 206 | }; 207 | }; 208 | ``` 209 | 210 | #### 4. 测试 RPC 接口 211 | 212 | 调用服务的单元测试方式,使用 `app.mockProxy` API 来 mock 服务 213 | 214 | ```js 215 | 'use strict'; 216 | 217 | const mm = require('egg-mock'); 218 | const assert = require('assert'); 219 | 220 | describe('test/mock.test.js', () => { 221 | let app; 222 | before(async function() { 223 | app = mm.app({ 224 | baseDir: 'apps/mock', 225 | }); 226 | await app.ready(); 227 | }); 228 | afterEach(mm.restore); 229 | after(async function() { 230 | await app.close(); 231 | }); 232 | 233 | it('should app.mockProxy ok', async function() { 234 | app.mockProxy('DemoService', 'sayHello', async function(name) { 235 | await sleep(1000); 236 | 237 | return 'hello ' + name + ' from mock'; 238 | }); 239 | 240 | const ctx = app.createAnonymousContext(); 241 | const res = await ctx.proxy.demoService.sayHello('gxcsoccer'); 242 | assert(res === 'hello gxcsoccer from mock'); 243 | }); 244 | }); 245 | ``` 246 | 247 | 在单元测试中,我们还可以通过 `app.rpcRequest` 接口来方便的测试我们自己暴露的 RPC 服务,例如: 248 | 249 | ```js 250 | 'use strict'; 251 | 252 | const mm = require('egg-mock'); 253 | 254 | describe('test/index.test.js', () => { 255 | let app; 256 | before(async function() { 257 | app = mm.app({ 258 | baseDir: 'apps/rpcserver', 259 | }); 260 | await app.ready(); 261 | }); 262 | after(async function() { 263 | await app.close(); 264 | }); 265 | 266 | it('should invoke HelloService', done => { 267 | app.rpcRequest('org.eggjs.rpc.test.HelloService') 268 | .invoke('hello') 269 | .send([ 'gxcsoccer' ]) 270 | .expect('hello gxcsoccer', done); 271 | }); 272 | }); 273 | ``` 274 | 275 | 详细 `app.rpcRequest` 的 api 可以参考:[单元测试 RPC 服务的方法](https://github.com/eggjs/egg-sofa-rpc/wiki/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95-RPC-%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%96%B9%E6%B3%95) 276 | 277 | 278 | ## 相关文档 279 | 280 | - [《聊聊 Nodejs RPC(一)》](https://github.com/alipay/sofa-rpc-node/wiki/%E8%81%8A%E8%81%8A-Nodejs-RPC%EF%BC%88%E4%B8%80%EF%BC%89) 281 | - [Eggjs 和 SOFA 的跨语言互调](https://github.com/eggjs/egg-sofa-rpc/wiki/Eggjs-%E5%92%8C-SOFA-%E7%9A%84%E8%B7%A8%E8%AF%AD%E8%A8%80%E4%BA%92%E8%B0%83) 282 | - [自定义服务发现实现](https://github.com/eggjs/egg-sofa-rpc/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0%E5%AE%9E%E7%8E%B0) 283 | - [RPC 代理(Proxy)配置](https://github.com/eggjs/egg-sofa-rpc/wiki/RPC-%E4%BB%A3%E7%90%86%EF%BC%88Proxy%EF%BC%89%E9%85%8D%E7%BD%AE) 284 | - [单元测试 RPC 服务的方法](https://github.com/eggjs/egg-sofa-rpc/wiki/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95-RPC-%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%96%B9%E6%B3%95) 285 | 286 | ## 贡献代码 287 | 288 | 请告知我们可以为你做些什么,不过在此之前,请检查一下是否有[已经存在的Bug或者意见](https://github.com/eggjs/egg/issues)。 289 | 290 | 如果你是一个代码贡献者,请参考[代码贡献规范](https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md)。 291 | 292 | ## 开源协议 293 | 294 | [MIT](LICENSE) 295 | -------------------------------------------------------------------------------- /agent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = agent => { 4 | agent.beforeStart(async function() { 5 | // 可能不需要 rpcRegistry,比如直连的情况 6 | if (!agent.rpcRegistry) return; 7 | 8 | await agent.rpcRegistry.ready(); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = app => { 6 | // 载入到 app.proxyClasses 7 | app.loader.loadToContext(path.join(app.config.baseDir, 'app/proxy'), 'proxy', { 8 | call: true, 9 | caseStyle: 'lower', 10 | fieldClass: 'proxyClasses', 11 | }); 12 | 13 | const paths = app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app/rpc')); 14 | app.loader.loadToApp(paths, 'rpcServices', { 15 | call: true, 16 | caseStyle: 'camel', // 首字母不变 17 | }); 18 | 19 | // 如果有 app/rpc 服务,则自动启动 server 20 | if (app.config.rpc.server.autoServe && Object.keys(app.rpcServices).length) { 21 | app.beforeStart(async function() { 22 | await app.rpcServer.start(); 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/extend/agent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _rpcRegistry = Symbol.for('egg#rpcRegistry'); 4 | 5 | module.exports = { 6 | get rpcRegistry() { 7 | if (!this[_rpcRegistry]) { 8 | const options = this.config.rpc.registry; 9 | if (!options) return null; 10 | const registryClass = this.config.rpc.registryClass; 11 | this[_rpcRegistry] = new registryClass(Object.assign({ 12 | logger: this.coreLogger, 13 | cluster: this.cluster, 14 | app: this, 15 | }, options)); 16 | this[_rpcRegistry].on('error', err => { this.coreLogger.error(err); }); 17 | this.beforeClose(async () => { 18 | await this[_rpcRegistry].close(); 19 | }); 20 | } 21 | return this[_rpcRegistry]; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /app/extend/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EggRpcClient = require('../../lib/client'); 4 | const EggRpcServer = require('../../lib/server'); 5 | const ProxyBase = require('../../lib/base_proxy'); 6 | 7 | // Symbols 8 | const _rpcRegistry = Symbol.for('egg#rpcRegistry'); 9 | const _rpcClient = Symbol.for('egg#rpcClient'); 10 | const _rpcServer = Symbol.for('egg#rpcServer'); 11 | 12 | module.exports = { 13 | get Proxy() { 14 | return ProxyBase; 15 | }, 16 | get rpcRegistry() { 17 | if (!this[_rpcRegistry]) { 18 | const options = this.config.rpc.registry; 19 | if (!options) return null; 20 | 21 | const registryClass = this.config.rpc.registryClass; 22 | this[_rpcRegistry] = new registryClass(Object.assign({ 23 | logger: this.coreLogger, 24 | cluster: this.cluster, 25 | app: this, 26 | }, options)); 27 | this[_rpcRegistry].on('error', err => { this.coreLogger.error(err); }); 28 | this.beforeClose(async () => { 29 | await this[_rpcRegistry].close(); 30 | }); 31 | } 32 | return this[_rpcRegistry]; 33 | }, 34 | get rpcClient() { 35 | if (!this[_rpcClient]) { 36 | this[_rpcClient] = new EggRpcClient(this); 37 | this[_rpcClient].on('error', err => { this.coreLogger.error(err); }); 38 | this.beforeClose(async () => { 39 | await this[_rpcClient].close(); 40 | }); 41 | } 42 | return this[_rpcClient]; 43 | }, 44 | get rpcServer() { 45 | if (!this[_rpcServer]) { 46 | this[_rpcServer] = new EggRpcServer(this); 47 | this[_rpcServer].on('error', err => { this.coreLogger.error(err); }); 48 | this.beforeClose(async () => { 49 | await this[_rpcServer].close(); 50 | this.coreLogger.info('[egg-rpc] rpc server is closed'); 51 | }); 52 | } 53 | return this[_rpcServer]; 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /app/extend/application.unittest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('sofa-rpc-node').test; 4 | const MockConnection = require('../../lib/mock_connection'); 5 | 6 | const mockAddress = require('url').parse('mock://127.0.0.1:12200', true); 7 | 8 | function normalizeProxyName(name) { 9 | return name[0].toLowerCase() + name.substring(1); 10 | } 11 | 12 | module.exports = { 13 | /** 14 | * rpc 服务测试 helper 15 | * ```js 16 | * app.rpcRequest('helloService') 17 | * .invoke('plus') 18 | * .send([ 1, 2 ]) 19 | * .type('number') 20 | * .expect(3, done); 21 | * ``` 22 | * @param {String} serviceName - rpc 服务全称 23 | * @return {Request} req 24 | */ 25 | rpcRequest(serviceName) { 26 | // 自动填充 namespace 27 | if (this.config.rpc && this.config.rpc.server && 28 | this.config.rpc.server.namespace && 29 | !serviceName.startsWith('com.') && 30 | !serviceName.startsWith(this.config.rpc.server.namespace)) { 31 | serviceName = `${this.config.rpc.server.namespace}.${serviceName}`; 32 | } 33 | return request(this.rpcServer).service(serviceName); 34 | }, 35 | 36 | /** 37 | * mock 客户端 proxy 38 | * 39 | * @param {String} proxyName - proxy 的名字 40 | * @param {String} methodName - 方法名 41 | * @param {Function} fn - mock 的具体逻辑实现 42 | */ 43 | mockProxy(proxyName, methodName, fn) { 44 | const consumers = this.rpcClient._consumerCache; 45 | let target; 46 | for (const consumer of consumers.values()) { 47 | if (normalizeProxyName(consumer.options.proxyName) === normalizeProxyName(proxyName)) { 48 | target = consumer; 49 | break; 50 | } 51 | } 52 | if (!target) { 53 | this.logger.warn('[egg-rpc] app.mockProxy(proxyName, methodName, fn), proxyName: %s not found', proxyName); 54 | return; 55 | } 56 | 57 | if (!target.__mockMethods__) { 58 | this.mm(target, '__mockMethods__', new Map()); 59 | 60 | const getConnection = target.getConnection; 61 | this.mm(target, 'getConnection', async function(...args) { 62 | const req = args[0]; 63 | if (this.__mockMethods__.has(req.methodName)) { 64 | return this.__mockMethods__.get(req.methodName); 65 | } 66 | return await getConnection.apply(this, args); 67 | }); 68 | } 69 | 70 | target.__mockMethods__.set(methodName, new MockConnection({ 71 | address: mockAddress, 72 | logger: this.logger, 73 | protocol: this.config.rpc.client.protocol, 74 | handler: async (...args) => { 75 | return this.toAsyncFunction(fn).apply(null, args); 76 | }, 77 | })); 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const antpb = require('antpb'); 6 | const protocol = require('sofa-bolt-node'); 7 | const { ZookeeperRegistry } = require('sofa-rpc-node').registry; 8 | 9 | module.exports = appInfo => { 10 | let proto; 11 | let classMap; 12 | let apiMeta; 13 | const protoPath = path.join(appInfo.baseDir, 'run/proto.json'); 14 | const proxyClassDir = path.join(appInfo.baseDir, 'app/proxy_class'); 15 | const apiMetaPath = path.join(appInfo.baseDir, 'config/apiMeta.json'); 16 | // 加载 proto 17 | if (fs.existsSync(protoPath)) { 18 | proto = antpb.fromJSON(require(protoPath)); 19 | } 20 | // 加载 classMap 21 | if (fs.existsSync(proxyClassDir)) { 22 | classMap = new Proxy({}, { 23 | get(target, className) { 24 | let map = target[className]; 25 | if (!map) { 26 | const args = className.split('.'); 27 | args.unshift(proxyClassDir); 28 | args[args.length - 1] = args[args.length - 1] + '.js'; 29 | const classfile = path.join.apply(null, args); 30 | if (fs.existsSync(classfile)) { 31 | map = target[className] = require(classfile)[className]; 32 | } 33 | } 34 | return map; 35 | }, 36 | }); 37 | } 38 | protocol.setOptions({ proto, classMap }); 39 | 40 | if (fs.existsSync(apiMetaPath)) { 41 | apiMeta = require(apiMetaPath); 42 | } 43 | return { 44 | rpc: { 45 | registryClass: ZookeeperRegistry, 46 | registry: null, 47 | client: { 48 | protocol, 49 | responseTimeout: 3000, 50 | }, 51 | server: { 52 | apiMeta, 53 | protocol, 54 | port: 12200, 55 | idleTime: 5000, 56 | killTimeout: 30000, 57 | maxIdleTime: 90 * 1000, 58 | responseTimeout: 3000, 59 | codecType: 'protobuf', 60 | selfPublish: true, 61 | // 下面配置针对新的 rpc 服务发布方式 62 | namespace: null, 63 | version: '1.0', 64 | group: 'SOFA', 65 | uniqueId: null, 66 | autoServe: true, // 如果发现有暴露服务,则自定启动 server 67 | }, 68 | }, 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /config/config.unittest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.rpc = { 4 | client: { 5 | allowMock: true, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /lib/base_proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * BaseProxy, proxy 基类,封装 proxy 的通用逻辑 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Proxy 基类,封装 proxy 的通用逻辑。 9 | * 你可以通过继承此基类来编写 proxy 10 | * @example 11 | * ```js 12 | * // app/proxy/user.js 13 | * module.exports = function(app) { 14 | * return class UserProxy extends app.Proxy { 15 | * constructor(ctx) { 16 | * super(ctx); 17 | * } 18 | * 19 | * // 定义方法 20 | * } 21 | * }; 22 | */ 23 | class Proxy { 24 | 25 | /** 26 | * Constructs the object. 27 | * 28 | * @param {Context} ctx The context 29 | * @param {Consumer} consumer The consumer 30 | * @constructor 31 | */ 32 | constructor(ctx, consumer) { 33 | this.ctx = ctx; 34 | this.app = ctx.app; 35 | this._consumer = consumer; 36 | } 37 | } 38 | 39 | module.exports = Proxy; 40 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const { RpcClient } = require('sofa-rpc-node').client; 5 | 6 | class EggRpcClient extends RpcClient { 7 | constructor(app) { 8 | const globalConfig = app.config.rpc && app.config.rpc.client; 9 | super(Object.assign({ 10 | logger: app.coreLogger, 11 | registry: app.rpcRegistry, 12 | }, globalConfig)); 13 | this.app = app; 14 | this.globalConfig = globalConfig; 15 | } 16 | 17 | createConsumer(options, consumerClass) { 18 | const targetAppName = options.targetAppName; 19 | assert(targetAppName, '[egg-rpc:client] createConsumer(options, consumerClass) options must config targetAppName'); 20 | 21 | const globalConfig = this.globalConfig; 22 | // 支持 `app.config['targetAppName.rpc.service.options'] = { ... }` 23 | options = Object.assign({ app: this.app }, globalConfig, globalConfig[targetAppName + '.rpc.service.options'], options); 24 | 25 | // 读取 responseTimeout 优先级如下 26 | // 1. 通过 config 的 ${targetAppName}.rpc.service.timeout 读取 27 | // 2. 尝试从 consumer 配置 responseTimeout 读取 28 | // 3. 尝试从 app.config.rpc.responseTimeout 读取 29 | // 4. 默认 5000ms 超时 30 | const timeoutKey = targetAppName + '.rpc.service.timeout'; 31 | const timeout = options[timeoutKey]; 32 | options.responseTimeout = Number(timeout) || options.responseTimeout || this.options.responseTimeout || 5000; 33 | 34 | // 硬负载 35 | if (!options.serverHost) { 36 | const testUrl = options[targetAppName + '.rpc.service.url']; 37 | const proxyName = options.proxyName; 38 | const proxyTestUrl = proxyName ? options['proxy.' + proxyName + '.rpc.service.url'] : null; 39 | options.serverHost = proxyTestUrl || testUrl; 40 | } 41 | return super.createConsumer(options, consumerClass); 42 | } 43 | } 44 | 45 | module.exports = EggRpcClient; 46 | -------------------------------------------------------------------------------- /lib/mock_connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pump = require('pump'); 4 | const PassThrough = require('stream').PassThrough; 5 | const Base = require('sofa-rpc-node').client.RpcConnection; 6 | 7 | class MockConnection extends Base { 8 | _connect() { 9 | this._socket = new PassThrough(); 10 | const opts = { 11 | sentReqs: this._sentReqs, 12 | classCache: this.options.classCache || new Map(), 13 | address: this.address, 14 | }; 15 | opts.classCache.enableCompile = true; 16 | this._encoder = this.protocol.encoder(opts); 17 | this._decoder = this.protocol.decoder(opts); 18 | this._decoder.on('request', req => { 19 | this.options.handler.apply(null, req.data.args) 20 | .then(result => { 21 | this._handleResponse({ 22 | packetId: req.packetId, 23 | packetType: 'response', 24 | data: { error: null, appResponse: result }, 25 | meta: { size: 0, start: 0, rt: 0 }, 26 | }); 27 | }) 28 | .catch(err => { 29 | this._handleResponse({ 30 | packetId: req.packetId, 31 | packetType: 'response', 32 | data: { error: err, appResponse: null }, 33 | meta: { size: 0, start: 0, rt: 0 }, 34 | }); 35 | }); 36 | }); 37 | pump(this._encoder, this._socket, this._decoder); 38 | this.ready(true); 39 | } 40 | 41 | heartbeat() {} 42 | } 43 | 44 | module.exports = MockConnection; 45 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const is = require('is-type-of'); 5 | const assert = require('assert'); 6 | const cluster = require('cluster'); 7 | const { RpcServer } = require('sofa-rpc-node').server; 8 | 9 | class EggRpcServer extends RpcServer { 10 | constructor(app) { 11 | const options = app.config.rpc && app.config.rpc.server; 12 | const selfPublish = options.selfPublish && cluster.isWorker; 13 | 14 | let registry = app.rpcRegistry; 15 | if (selfPublish && app.rpcRegistry) { 16 | registry = new app.rpcRegistry.DataClient(app.rpcRegistry.options); 17 | } 18 | 19 | const apiMeta = options.apiMeta || {}; 20 | const classMap = {}; 21 | for (const interfaceName in apiMeta) { 22 | const meta = apiMeta[interfaceName]; 23 | const classMaps = meta.classMaps || {}; 24 | for (const clazz in classMaps) { 25 | assert(!classMap[clazz], `[egg-rpc-server] ${clazz} duplicate definition`); 26 | classMap[clazz] = classMaps[clazz]; 27 | } 28 | } 29 | 30 | super(Object.assign({ 31 | appName: app.name, 32 | // 如果是 selfPublish 单独创建 registry 连接来发布服务 33 | registry, 34 | classMap, 35 | logger: app.coreLogger, 36 | }, options)); 37 | 38 | this.app = app; 39 | this.selfPublish = selfPublish; 40 | this.apiMeta = apiMeta; 41 | 42 | // 等 app 已经 ready 后才向注册中心注册服务 43 | app.ready(err => { 44 | if (!err) { 45 | this.load(); 46 | this.publish(); 47 | this.logger.info('[egg-rpc#server] publish all rpc services after app ready'); 48 | } 49 | }); 50 | } 51 | 52 | get listenPorts() { 53 | let port = this.options.port; 54 | if (this.selfPublish) { 55 | port = port + cluster.worker.id; 56 | this.publishPort = port; 57 | return [ port, this.options.port ]; 58 | } 59 | return [ port ]; 60 | } 61 | 62 | load() { 63 | const { app } = this; 64 | const { namespace } = app.config.rpc.server; 65 | 66 | // load apiMeta 67 | for (const name in app.rpcServices) { 68 | let delegate = app.rpcServices[name]; 69 | 70 | const interfaceName = `${namespace}.${name}`; 71 | const service = Object.assign({ interfaceName }, app.config.rpc.server); 72 | if (delegate.interfaceName || delegate.namespace) { 73 | service.interfaceName = delegate.interfaceName ? delegate.interfaceName : `${delegate.namespace}.${name}`; 74 | service.uniqueId = delegate.uniqueId || ''; 75 | service.version = delegate.version || service.version; 76 | service.group = delegate.group || service.group; 77 | } 78 | 79 | if (is.class(delegate)) { 80 | delegate = wrap(app, delegate); 81 | } else { 82 | for (const key of Object.keys(delegate)) { 83 | delegate[key] = app.toAsyncFunction(delegate[key]); 84 | } 85 | } 86 | this.addService(service, delegate); 87 | } 88 | } 89 | 90 | /** 91 | * @param {HSFRequest} req 92 | * - @param {Object} requestProps 93 | * - @param {Number} packetId 94 | * - @param {String} packetType 95 | * - @param {String} methodName 96 | * - @param {String} serverSignature interfaceName 97 | * - @param {Array} args 98 | * @param {HSFResponse} res 99 | * - @param {Function} send 100 | * @return {Context} ctx 101 | */ 102 | createContext(req, res) { 103 | assert(req && req.data, '[egg-rpc#server] req && req.data is required'); 104 | const reqData = req.data; 105 | const { serverSignature, methodName, args } = reqData; 106 | const httpReq = { 107 | method: 'RPC', 108 | url: '/rpc/' + serverSignature + '/' + methodName, 109 | headers: {}, 110 | socket: res.socket, 111 | }; 112 | const ctx = this.app.createContext(httpReq, new http.ServerResponse(httpReq)); 113 | ctx.params = args; 114 | return ctx; 115 | } 116 | 117 | addService(service, delegate) { 118 | assert(service && delegate, '[egg-rpc#server] addService(service, delegate) service & delegate is required'); 119 | if (is.string(service)) { 120 | service = { 121 | interfaceName: service, 122 | version: this.options.version, 123 | group: this.options.group, 124 | }; 125 | } 126 | // 将 app 传入 127 | service.app = this.app; 128 | service.apiMeta = this.apiMeta[service.interfaceName]; 129 | return super.addService(service, delegate); 130 | } 131 | 132 | _handleUncaughtError() { 133 | if (this.selfPublish) { 134 | this.unPublish(); 135 | this.logger.warn('[egg-rpc#server] unPublish all rpc services for uncaughtException in this process %s', process.pid); 136 | } else { 137 | this.logger.warn('[egg-rpc#server] rpc server is down, cause by uncaughtException in this process %s', process.pid); 138 | } 139 | } 140 | } 141 | 142 | function wrap(app, Class) { 143 | const proto = Class.prototype; 144 | const result = {}; 145 | for (const key of Object.getOwnPropertyNames(proto)) { 146 | if (!is.asyncFunction(proto[key]) && !is.generatorFunction(proto[key])) { 147 | continue; 148 | } 149 | 150 | proto[key] = app.toAsyncFunction(proto[key]); 151 | result[key] = async function(...args) { 152 | const instance = new Class(this); 153 | return await instance[key](...args); 154 | }; 155 | } 156 | return result; 157 | } 158 | 159 | module.exports = EggRpcServer; 160 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg-rpc-base", 3 | "version": "1.3.1", 4 | "description": "rpc base plugin for eggjs", 5 | "eggPlugin": { 6 | "name": "rpc" 7 | }, 8 | "keywords": [ 9 | "egg", 10 | "eggPlugin", 11 | "egg-plugin" 12 | ], 13 | "dependencies": { 14 | "antpb": "^1.0.0", 15 | "is-type-of": "^1.2.1", 16 | "pump": "^3.0.0", 17 | "sofa-bolt-node": "^1.1.1", 18 | "sofa-rpc-node": "^1.6.2" 19 | }, 20 | "devDependencies": { 21 | "autod": "^3.0.1", 22 | "autod-egg": "^1.1.0", 23 | "egg": "^2.14.2", 24 | "egg-bin": "^4.10.0", 25 | "egg-mock": "^3.21.0", 26 | "egg-rpc-generator": "^1.2.0", 27 | "eslint": "^5.12.0", 28 | "eslint-config-egg": "^7.1.0", 29 | "mz-modules": "^2.1.0", 30 | "webstorm-disable-index": "^1.2.0" 31 | }, 32 | "engines": { 33 | "node": ">=8.0.0" 34 | }, 35 | "scripts": { 36 | "autod": "autod", 37 | "lint": "eslint . --ext .js", 38 | "cov": "sh test/init.sh && TEST_TIMEOUT=20000 egg-bin cov", 39 | "test": "npm run lint && npm run test-local", 40 | "test-local": "sh test/init.sh && egg-bin test", 41 | "pkgfiles": "egg-bin pkgfiles --check", 42 | "ci": "npm run autod -- --check && npm run pkgfiles && npm run lint && npm run cov", 43 | "contributors": "contributors -f plain -o AUTHORS" 44 | }, 45 | "files": [ 46 | "app", 47 | "config", 48 | "lib", 49 | "app.js", 50 | "agent.js" 51 | ], 52 | "ci": { 53 | "version": "8, 10" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "git+https://github.com/eggjs/egg-rpc.git" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/eggjs/egg/issues" 61 | }, 62 | "homepage": "https://github.com/eggjs/egg-rpc#readme", 63 | "author": "gxcsoccer ", 64 | "license": "MIT" 65 | } 66 | -------------------------------------------------------------------------------- /test/custom_registry.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const mm = require('egg-mock'); 5 | const assert = require('assert'); 6 | const sleep = require('mz-modules/sleep'); 7 | const rimraf = require('mz-modules/rimraf'); 8 | 9 | describe('test/custom_registry.test.js', () => { 10 | 11 | async function cleanDir() { 12 | await Promise.all([ 13 | rimraf(path.join(__dirname, 'fixtures/apps/custom-registry/app/proxy')), 14 | rimraf(path.join(__dirname, 'fixtures/apps/custom-registry/logs')), 15 | rimraf(path.join(__dirname, 'fixtures/apps/custom-registry/run')), 16 | ]); 17 | } 18 | 19 | let app; 20 | before(async function() { 21 | app = mm.app({ 22 | baseDir: 'apps/custom-registry', 23 | }); 24 | await app.ready(); 25 | await sleep(1000); 26 | }); 27 | after(async function() { 28 | await app.close(); 29 | await cleanDir(); 30 | }); 31 | 32 | it('should invoke ok', async function() { 33 | const ctx = app.createAnonymousContext(); 34 | const res = await ctx.proxy.protoService.echoObj({ 35 | name: 'gxcsoccer', 36 | group: 'B', 37 | }); 38 | console.log(res); 39 | assert.deepEqual(res, { 40 | code: 200, 41 | message: 'hello gxcsoccer from Node.js', 42 | }); 43 | }); 44 | 45 | it('should app.rpcRequest ok', done => { 46 | app.rpcRequest('com.alipay.sofa.rpc.test.ProtoService') 47 | .invoke('echoObj') 48 | .send([{ 49 | name: 'gxcsoccer', 50 | group: 'B', 51 | }]) 52 | .expect({ 53 | code: 200, 54 | message: 'hello gxcsoccer from Node.js', 55 | }, done); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/fixtures/apps/custom-registry/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | app.get('/', async function(ctx) { 5 | ctx.body = await ctx.proxy.protoService.echoObj({ 6 | name: 'zongyu', 7 | group: 'B', 8 | }); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/custom-registry/app/rpc/ProtoService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.echoObj = async function(req) { 4 | return { 5 | code: 200, 6 | message: 'hello ' + req.name + ' from Node.js', 7 | }; 8 | }; 9 | 10 | exports.interfaceName = 'com.alipay.sofa.rpc.test.ProtoService'; 11 | exports.version = '1.0'; 12 | exports.group = 'SOFA'; 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/custom-registry/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.rpc = { 4 | registryClass: require('../lib/registry'), 5 | registry: {}, 6 | }; 7 | 8 | exports.keys = '123456'; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/custom-registry/config/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | services: [{ 5 | appName: 'sofarpc', 6 | api: { 7 | ProtoService: 'com.alipay.sofa.rpc.test.ProtoService', 8 | }, 9 | }], 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/custom-registry/lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('sdk-base'); 4 | 5 | class SimpleRegistry extends Base { 6 | constructor(options) { 7 | super(options); 8 | this.ready(true); 9 | } 10 | 11 | async register() {} 12 | 13 | async unRegister() {} 14 | 15 | subscribe(config, listener) { 16 | setImmediate(() => { 17 | listener([ 18 | 'bolt://127.0.0.1:12200', 19 | ]); 20 | }); 21 | } 22 | 23 | unSubscribe() {} 24 | 25 | close() { 26 | return Promise.resolve() 27 | } 28 | } 29 | 30 | module.exports = SimpleRegistry; 31 | -------------------------------------------------------------------------------- /test/fixtures/apps/custom-registry/lib/registry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RegistryBase = require('sofa-rpc-node/lib/registry/base'); 4 | const DataClient = require('./client'); 5 | 6 | class CustomeRegistry extends RegistryBase { 7 | get DataClient() { 8 | return DataClient; 9 | } 10 | } 11 | 12 | module.exports = CustomeRegistry 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/custom-registry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-registry" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/custom-registry/proto/ProtoService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.alipay.sofa.rpc.test; 4 | 5 | // 可选 6 | option java_multiple_files = false; 7 | 8 | service ProtoService { 9 | rpc echoObj (EchoRequest) returns (EchoResponse) {} 10 | } 11 | 12 | message EchoRequest { 13 | string name = 1; 14 | Group group = 2; 15 | } 16 | 17 | message EchoResponse { 18 | int32 code = 1; 19 | string message = 2; 20 | } 21 | 22 | enum Group { 23 | A = 0; 24 | B = 1; 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/apps/hardload/app/proxy/ProtoService.js: -------------------------------------------------------------------------------- 1 | // Don't modified this file, it's auto created by egg-rpc-generator 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /* eslint-disable */ 8 | /* istanbul ignore next */ 9 | module.exports = app => { 10 | const consumer = app.rpcClient.createConsumer({ 11 | interfaceName: 'com.alipay.sofa.rpc.test.ProtoService', 12 | targetAppName: 'sofarpc', 13 | version: '1.0', 14 | group: 'SOFA', 15 | proxyName: 'ProtoService', 16 | }); 17 | 18 | if (!consumer) { 19 | // `app.config['sofarpc.rpc.service.enable'] = false` will disable this consumer 20 | return; 21 | } 22 | 23 | app.beforeStart(async() => { 24 | await consumer.ready(); 25 | }); 26 | 27 | class ProtoService extends app.Proxy { 28 | constructor(ctx) { 29 | super(ctx, consumer); 30 | } 31 | 32 | async echoObj(req) { 33 | return await consumer.invoke('echoObj', [ req ], { 34 | ctx: this.ctx, 35 | }); 36 | } 37 | } 38 | 39 | return ProtoService; 40 | }; 41 | /* eslint-enable */ 42 | -------------------------------------------------------------------------------- /test/fixtures/apps/hardload/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | app.get('/', async function(ctx) { 5 | ctx.body = await ctx.proxy.protoService.echoObj({ 6 | name: 'zongyu', 7 | group: 'B', 8 | }); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/hardload/app/rpc/ProtoService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.echoObj = async function(req) { 4 | return { 5 | code: 200, 6 | message: 'hello ' + req.name + ' from Node.js', 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/hardload/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.rpc = { 4 | client: { 5 | ['sofarpc.rpc.service.url']: '127.0.0.1:12200', 6 | }, 7 | server: { 8 | // selfPublish: false, 9 | namespace: 'com.alipay.sofa.rpc.test', 10 | }, 11 | }; 12 | 13 | exports.keys = '123456'; 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/hardload/config/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | services: [{ 5 | appName: 'sofarpc', 6 | api: { 7 | ProtoService: 'com.alipay.sofa.rpc.test.ProtoService', 8 | }, 9 | }], 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/hardload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardload" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/hardload/proto/ProtoService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.alipay.sofa.rpc.test; 4 | 5 | // 可选 6 | option java_multiple_files = false; 7 | 8 | service ProtoService { 9 | rpc echoObj (EchoRequest) returns (EchoResponse) {} 10 | } 11 | 12 | message EchoRequest { 13 | string name = 1; 14 | Group group = 2; 15 | } 16 | 17 | message EchoResponse { 18 | int32 code = 1; 19 | string message = 2; 20 | } 21 | 22 | enum Group { 23 | A = 0; 24 | B = 1; 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/apps/jar2proxy/app/rpc/DemoService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.sayHello = async function(name) { 4 | return 'hello ' + name; 5 | }; 6 | 7 | exports.echoPerson = async function(p) { 8 | return { 9 | $class: 'eggjs.demo.Person', 10 | $: p, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/jar2proxy/assembly/dubbo-demo-api-1.0-SNAPSHOT-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggjs/egg-rpc/01411454e4185169097ab0d54115939a8b650a4e/test/fixtures/apps/jar2proxy/assembly/dubbo-demo-api-1.0-SNAPSHOT-sources.jar -------------------------------------------------------------------------------- /test/fixtures/apps/jar2proxy/assembly/dubbo-demo-api-1.0-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggjs/egg-rpc/01411454e4185169097ab0d54115939a8b650a4e/test/fixtures/apps/jar2proxy/assembly/dubbo-demo-api-1.0-SNAPSHOT.jar -------------------------------------------------------------------------------- /test/fixtures/apps/jar2proxy/config/apiMeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "eggjs.demo.DemoService": { 3 | "appName": "jar2proxy", 4 | "canonicalName": "eggjs.demo.DemoService", 5 | "codeSource": "jar2proxy-facade-1.0.0.jar", 6 | "methods": [{ 7 | "name": "sayHello", 8 | "parameterTypes": [ 9 | "java.lang.String" 10 | ], 11 | "returnType": "java.lang.String" 12 | }, 13 | { 14 | "name": "echoPerson", 15 | "parameterTypes": [ 16 | "eggjs.demo.Person" 17 | ], 18 | "returnType": "eggjs.demo.Person" 19 | } 20 | ], 21 | "classMaps": { 22 | "eggjs.demo.Person": { 23 | "id": { 24 | "type": "int", 25 | "defaultValue": 0 26 | }, 27 | "name": { 28 | "type": "java.lang.String" 29 | }, 30 | "address": { 31 | "type": "java.lang.String" 32 | }, 33 | "salary": { 34 | "type": "int", 35 | "defaultValue": 0 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/fixtures/apps/jar2proxy/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.rpc = { 4 | client: { 5 | 'jar2proxy.rpc.service.url': '127.0.0.1:12200', 6 | }, 7 | server: { 8 | version: '1.0.0', 9 | group: 'HSF', 10 | codecType: 'hessian2', 11 | namespace: 'eggjs.demo', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/jar2proxy/config/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | group: 'HSF', 5 | version: '1.0.0', 6 | services: [{ 7 | appName: 'jar2proxy', 8 | api: { 9 | DemoService: { 10 | interfaceName: 'eggjs.demo.DemoService', 11 | }, 12 | }, 13 | dependency: [{ 14 | groupId: 'org.apache.dubbo', 15 | artifactId: 'dubbo-demo-api', 16 | version: '1.0-SNAPSHOT', 17 | }], 18 | }], 19 | }; 20 | -------------------------------------------------------------------------------- /test/fixtures/apps/jar2proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jar2proxy" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mock/assembly/dubbo-demo-api-1.0-SNAPSHOT-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggjs/egg-rpc/01411454e4185169097ab0d54115939a8b650a4e/test/fixtures/apps/mock/assembly/dubbo-demo-api-1.0-SNAPSHOT-sources.jar -------------------------------------------------------------------------------- /test/fixtures/apps/mock/assembly/dubbo-demo-api-1.0-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggjs/egg-rpc/01411454e4185169097ab0d54115939a8b650a4e/test/fixtures/apps/mock/assembly/dubbo-demo-api-1.0-SNAPSHOT.jar -------------------------------------------------------------------------------- /test/fixtures/apps/mock/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.rpc = {}; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mock/config/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | group: 'HSF', 5 | version: '1.0.0', 6 | services: [{ 7 | appName: 'jar2proxy', 8 | api: { 9 | DemoService: { 10 | interfaceName: 'eggjs.demo.DemoService', 11 | }, 12 | }, 13 | dependency: [{ 14 | groupId: 'org.apache.dubbo', 15 | artifactId: 'dubbo-demo-api', 16 | version: '1.0-SNAPSHOT', 17 | }], 18 | }], 19 | }; 20 | -------------------------------------------------------------------------------- /test/fixtures/apps/mock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/rpcserver/app/rpc/HelloService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sleep = require('mz-modules/sleep'); 4 | 5 | module.exports = function(app) { 6 | const exports = {}; 7 | 8 | exports.interfaceName = 'com.alipay.nodejs.HelloService'; 9 | exports.version = '1.0'; 10 | exports.group = 'SOFA'; 11 | 12 | 13 | exports.hello = function*(name) { 14 | yield sleep(200); 15 | return 'hello ' + name; 16 | }; 17 | 18 | return exports; 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/apps/rpcserver/app/rpc/MathService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class MathService { 4 | constructor(ctx) { 5 | this.ctx = ctx; 6 | } 7 | 8 | async plus(a, b) { 9 | return a + b; 10 | } 11 | } 12 | 13 | module.exports = MathService; 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/rpcserver/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.rpc = { 4 | registry: { 5 | address: '127.0.0.1:2181', 6 | }, 7 | server: { 8 | port: 12200, 9 | codecType: 'hessian2', 10 | selfPublish: true, 11 | // 下面配置针对新的 rpc 服务发布方式 12 | namespace: 'com.alipay.nodejs.rpc', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/fixtures/apps/rpcserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rpcserver" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/self-publish/app/rpc/HelloService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 加法计数服务 5 | * @hsf 6 | * @public 7 | * @param {Number} a - 被加数 8 | * @param {Number} b - 加数 9 | * @return {Number} 结果 10 | */ 11 | exports.plus = async function(a, b) { 12 | return a + b; 13 | }; 14 | 15 | exports.error = async function() { 16 | setTimeout(() => { 17 | throw new Error('uncaughtException'); 18 | }, 1000); 19 | return 'ok'; 20 | }; 21 | -------------------------------------------------------------------------------- /test/fixtures/apps/self-publish/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.rpc = { 4 | registry: { 5 | address: '127.0.0.1:2181', 6 | }, 7 | client: { 8 | responseTimeout: 3000, 9 | }, 10 | server: { 11 | port: 12200 + Number(process.versions.node.split('.')[0]), 12 | idleTime: 5000, 13 | killTimeout: 30000, 14 | maxIdleTime: 90 * 1000, 15 | responseTimeout: 3000, 16 | codecType: 'hessian2', 17 | selfPublish: true, 18 | // 下面配置针对新的 rpc 服务发布方式 19 | namespace: 'com.alipay.nodejs.selfPublish', 20 | version: '1.0', 21 | group: 'SOFA', 22 | uniqueId: null, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /test/fixtures/apps/self-publish/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "self-publish" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/sofarpc/app/rpc/ProtoService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.echoObj = async function(req) { 4 | req = req.toObject({ enums: String }); 5 | return { 6 | code: 200, 7 | message: 'hello ' + req.name + ', you are in ' + req.group, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/apps/sofarpc/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.rpc = { 4 | registry: { 5 | address: '127.0.0.1:2181', 6 | }, 7 | server: { 8 | // selfPublish: false, 9 | namespace: 'com.alipay.sofa.rpc.test', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixtures/apps/sofarpc/config/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | services: [{ 5 | appName: 'sofarpc', 6 | api: { 7 | ProtoService: 'com.alipay.sofa.rpc.test.ProtoService', 8 | }, 9 | }], 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/sofarpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sofarpc" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/sofarpc/proto/ProtoService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.alipay.sofa.rpc.test; 4 | 5 | // 可选 6 | option java_multiple_files = false; 7 | 8 | service ProtoService { 9 | rpc echoObj (EchoRequest) returns (EchoResponse) {} 10 | } 11 | 12 | message EchoRequest { 13 | string name = 1; 14 | Group group = 2; 15 | } 16 | 17 | message EchoResponse { 18 | int32 code = 1; 19 | string message = 2; 20 | } 21 | 22 | enum Group { 23 | A = 0; 24 | B = 1; 25 | } 26 | -------------------------------------------------------------------------------- /test/hardload.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('egg-mock'); 4 | 5 | describe('test/hardload.test.js', () => { 6 | let app; 7 | before(async function() { 8 | app = mm.app({ 9 | baseDir: 'apps/hardload', 10 | }); 11 | await app.ready(); 12 | }); 13 | after(async function() { 14 | await app.close(); 15 | }); 16 | afterEach(mm.restore); 17 | 18 | it('should invoke with serverHost', async function() { 19 | await app.httpRequest() 20 | .get('/') 21 | .expect({ 22 | code: 200, 23 | message: 'hello zongyu from Node.js', 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const mm = require('egg-mock'); 5 | const assert = require('assert'); 6 | const sleep = require('mz-modules/sleep'); 7 | const rimraf = require('mz-modules/rimraf'); 8 | 9 | describe('test/index.test.js', () => { 10 | 11 | async function cleanDir() { 12 | await Promise.all([ 13 | rimraf(path.join(__dirname, 'fixtures/apps/sofarpc/app/proxy')), 14 | rimraf(path.join(__dirname, 'fixtures/apps/sofarpc/logs')), 15 | rimraf(path.join(__dirname, 'fixtures/apps/sofarpc/run')), 16 | ]); 17 | } 18 | 19 | let app; 20 | before(async function() { 21 | app = mm.app({ 22 | baseDir: 'apps/sofarpc', 23 | }); 24 | await app.ready(); 25 | await sleep(1000); 26 | }); 27 | afterEach(mm.restore); 28 | after(async function() { 29 | await app.close(); 30 | await cleanDir(); 31 | }); 32 | 33 | it('should invoke ok', async function() { 34 | const ctx = app.createAnonymousContext(); 35 | const res = await ctx.proxy.protoService.echoObj({ 36 | name: 'gxcsoccer', 37 | group: 'B', 38 | }); 39 | console.log(res); 40 | assert.deepEqual(res, { 41 | code: 200, 42 | message: 'hello gxcsoccer, you are in B', 43 | }); 44 | }); 45 | 46 | it('should app.rpcRequest ok', done => { 47 | app.rpcRequest('ProtoService') 48 | .invoke('echoObj') 49 | .send([{ 50 | name: 'gxcsoccer', 51 | group: 'B', 52 | }]) 53 | .expect({ 54 | code: 200, 55 | message: 'hello gxcsoccer, you are in B', 56 | }, done); 57 | }); 58 | 59 | it('should app.mockProxy ok', async function() { 60 | app.mockProxy('protoService', 'echoObj', async function() { 61 | await sleep(100); 62 | return { 63 | code: 200, 64 | message: 'hello gxcsoccer, this is by app.mockProxy', 65 | }; 66 | }); 67 | 68 | const ctx = app.createAnonymousContext(); 69 | const res = await ctx.proxy.protoService.echoObj({ 70 | name: 'gxcsoccer', 71 | group: 'B', 72 | }); 73 | console.log(res); 74 | assert.deepEqual(res, { 75 | code: 200, 76 | message: 'hello gxcsoccer, this is by app.mockProxy', 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GEN=${PWD}/node_modules/.bin/egg-rpc-generator 4 | 5 | # test dir 6 | 7 | DIR=${PWD}/test/fixtures/apps 8 | NAMES="sofarpc hardload custom-registry jar2proxy mock" 9 | 10 | for NAME in $NAMES 11 | do 12 | echo "Create ${DIR}/${NAME} proxy" 13 | $GEN -b ${DIR}/${NAME} 14 | echo "------------------------------------------------" 15 | done 16 | 17 | echo "All done" 18 | -------------------------------------------------------------------------------- /test/jar2proxy.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('egg-mock'); 4 | const assert = require('assert'); 5 | 6 | describe('test/jar2proxy.test.js', () => { 7 | let app; 8 | before(async function() { 9 | app = mm.app({ 10 | baseDir: 'apps/jar2proxy', 11 | }); 12 | await app.ready(); 13 | }); 14 | afterEach(mm.restore); 15 | after(async function() { 16 | await app.close(); 17 | }); 18 | 19 | it('should has apiMeta', () => { 20 | assert(app.rpcServer); 21 | assert(app.rpcServer.apiMeta); 22 | assert(app.rpcServer.classMap); 23 | assert.deepEqual(app.rpcServer.apiMeta, require('./fixtures/apps/jar2proxy/config/apiMeta.json')); 24 | assert(app.rpcServer.services && app.rpcServer.services.has('eggjs.demo.DemoService:1.0.0')); 25 | const service = app.rpcServer.services.get('eggjs.demo.DemoService:1.0.0'); 26 | assert.deepEqual(service.apiMeta, app.rpcServer.apiMeta['eggjs.demo.DemoService']); 27 | }); 28 | 29 | it('should invoke sayHello', done => { 30 | app.rpcRequest('eggjs.demo.DemoService') 31 | .invoke('sayHello') 32 | .send([ 'gxcsoccer' ]) 33 | .expect('hello gxcsoccer', done); 34 | }); 35 | 36 | it('should invoke echoPerson', done => { 37 | app.rpcRequest('eggjs.demo.DemoService') 38 | .invoke('echoPerson') 39 | .send([{ 40 | name: '宗羽', 41 | address: 'C 空间', 42 | id: 68955, 43 | salary: 10000000, 44 | }]) 45 | .expect({ 46 | name: '宗羽', 47 | address: 'C 空间', 48 | id: 68955, 49 | salary: 10000000, 50 | }, done); 51 | }); 52 | 53 | it('should app.mockProxy check encode error', async function() { 54 | app.mockProxy('DemoService', 'echoPerson', function() { 55 | return { 56 | name: '宗羽 by mock', 57 | address: 'C 空间', 58 | id: 68955, 59 | salary: 10000000, 60 | }; 61 | }); 62 | app.mockProxy('not-existService', 'xxx', function() {}); 63 | app.mockProxy('DemoService', 'echoPerson1', function() { 64 | return { 65 | name: '宗羽 by mock', 66 | address: 'C 空间', 67 | id: 68955, 68 | salary: 10000000, 69 | }; 70 | }); 71 | 72 | const ctx = app.createAnonymousContext(); 73 | const res = await ctx.proxy.demoService.echoPerson({ 74 | name: '宗羽', 75 | address: 'C 空间', 76 | id: 68955, 77 | salary: 10000000, 78 | }); 79 | assert.deepEqual(res, { 80 | name: '宗羽 by mock', 81 | address: 'C 空间', 82 | id: 68955, 83 | salary: 10000000, 84 | }); 85 | 86 | const r = await ctx.proxy.demoService.sayHello('gxcsoccer'); 87 | assert(r === 'hello gxcsoccer'); 88 | 89 | try { 90 | await ctx.proxy.demoService.echoPerson({ 91 | name: '宗羽', 92 | address: 'C 空间', 93 | id: 68955, 94 | salary: '10000000', 95 | }); 96 | assert(false); 97 | } catch (err) { 98 | assert(err.message === 'hessian writeInt expect input type is `int32`, but got `string` : "10000000" '); 99 | } 100 | 101 | app.mockProxy('DemoService', 'sayHello', function() { 102 | throw new Error('mock error'); 103 | }); 104 | 105 | try { 106 | await ctx.proxy.demoService.sayHello('gxcsoccer'); 107 | assert(false); 108 | } catch (err) { 109 | assert(err.message.includes('mock error')); 110 | } 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/mock.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('egg-mock'); 4 | const assert = require('assert'); 5 | 6 | describe('test/mock.test.js', () => { 7 | let app; 8 | before(async function() { 9 | app = mm.app({ 10 | baseDir: 'apps/mock', 11 | }); 12 | await app.ready(); 13 | }); 14 | afterEach(mm.restore); 15 | after(async function() { 16 | await app.close(); 17 | }); 18 | 19 | it('should app.mockProxy check encode error', async function() { 20 | app.mockProxy('DemoService', 'echoPerson', function() { 21 | return { 22 | name: '宗羽 by mock', 23 | address: 'C 空间', 24 | id: 68955, 25 | salary: 10000000, 26 | }; 27 | }); 28 | 29 | const ctx = app.createAnonymousContext(); 30 | const res = await ctx.proxy.demoService.echoPerson({ 31 | name: '宗羽', 32 | address: 'C 空间', 33 | id: 68955, 34 | salary: 10000000, 35 | }); 36 | assert.deepEqual(res, { 37 | name: '宗羽 by mock', 38 | address: 'C 空间', 39 | id: 68955, 40 | salary: 10000000, 41 | }); 42 | 43 | try { 44 | await ctx.proxy.demoService.echoPerson({ 45 | name: '宗羽', 46 | address: 'C 空间', 47 | id: 68955, 48 | salary: '10000000', 49 | }); 50 | assert(false); 51 | } catch (err) { 52 | assert(err.message === 'hessian writeInt expect input type is `int32`, but got `string` : "10000000" '); 53 | } 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/self_publish.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('egg-mock'); 4 | const assert = require('assert'); 5 | const cluster = require('cluster'); 6 | const sleep = require('mz-modules/sleep'); 7 | const { RpcClient } = require('sofa-rpc-node').client; 8 | const { ZookeeperRegistry } = require('sofa-rpc-node').registry; 9 | 10 | const logger = console; 11 | 12 | describe('test/self_publish.test.js', () => { 13 | let app; 14 | let client; 15 | let registry; 16 | const interfaceName = 'com.alipay.nodejs.selfPublish.HelloService'; 17 | const port = 12200 + Number(process.versions.node.split('.')[0]); 18 | 19 | describe('app.cluster()', () => { 20 | before(async function() { 21 | mm.env('production'); 22 | registry = new ZookeeperRegistry({ 23 | address: '127.0.0.1:2181', 24 | logger, 25 | }); 26 | app = mm.cluster({ 27 | baseDir: 'apps/self-publish', 28 | workers: 4, 29 | }); 30 | await app.ready(); 31 | 32 | client = new RpcClient({ 33 | registry, 34 | logger, 35 | }); 36 | await client.ready(); 37 | 38 | registry.subscribe({ interfaceName }, val => { 39 | if (val.length === 4) { 40 | registry.emit('init_address', val); 41 | } 42 | }); 43 | const addressList = await registry.await('init_address'); 44 | assert(addressList.every(url => url.port !== port)); 45 | }); 46 | afterEach(mm.restore); 47 | after(async function() { 48 | await app.close(); 49 | await registry.close(); 50 | await sleep(2000); 51 | }); 52 | 53 | it('should publish self worker', async function() { 54 | let count = 4; 55 | let ret; 56 | const consumer = client.createConsumer({ interfaceName }); 57 | await consumer.ready(); 58 | while (count--) { 59 | ret = await consumer.invoke('plus', [ 1, 2 ]); 60 | assert(ret === 3); 61 | } 62 | 63 | const directConsumer = client.createConsumer({ 64 | interfaceName, 65 | serverHost: `127.0.0.1:${port}`, 66 | }); 67 | await directConsumer.ready(); 68 | // 原始端口也需要监听 69 | ret = await directConsumer.invoke('plus', [ 1, 2 ]); 70 | assert(ret === 3); 71 | }); 72 | 73 | it(`should app ready failed cause ${port} is used`, async function() { 74 | mm(cluster, 'isWorker', true); 75 | mm(cluster, 'worker', { id: 10 }); 76 | let app; 77 | try { 78 | app = mm.app({ 79 | baseDir: 'apps/self-publish', 80 | }); 81 | await app.ready(); 82 | assert(false, 'should not run here'); 83 | } catch (err) { 84 | assert(err.message.includes('listen EADDRINUSE')); 85 | } 86 | if (app) { 87 | await app.close(); 88 | } 89 | }); 90 | 91 | it('should publish again if app worker uncaughtException', async function() { 92 | registry.subscribe({ interfaceName }, val => { 93 | registry.emit('service_address', val); 94 | }); 95 | 96 | let addressList = await registry.await('service_address'); 97 | assert(addressList.length === 4); 98 | 99 | const consumer = client.createConsumer({ interfaceName }); 100 | [ addressList ] = await Promise.all([ 101 | registry.await('service_address'), 102 | consumer.invoke('error', []), 103 | ]); 104 | 105 | assert(addressList.length === 3); 106 | addressList = await registry.await('service_address'); 107 | assert(addressList.length === 4); 108 | 109 | let count = 4; 110 | let ret; 111 | while (count--) { 112 | ret = await consumer.invoke('plus', [ 1, 2 ]); 113 | assert(ret === 3); 114 | } 115 | }); 116 | }); 117 | 118 | describe('app.mm()', () => { 119 | it('start 防重入', async function() { 120 | mm(cluster, 'isWorker', true); 121 | mm(cluster, 'worker', { id: 1000 }); 122 | const app = mm.app({ 123 | baseDir: 'apps/self-publish', 124 | }); 125 | await app.ready(); 126 | await app.rpcServer.start(); 127 | 128 | await app.close(); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/server.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('egg-mock'); 4 | 5 | describe('test/index.test.js', () => { 6 | let app; 7 | before(async function() { 8 | app = mm.app({ 9 | baseDir: 'apps/rpcserver', 10 | }); 11 | await app.ready(); 12 | }); 13 | after(async function() { 14 | await app.close(); 15 | }); 16 | 17 | it('should invoke HelloService', done => { 18 | app.rpcRequest('com.alipay.nodejs.HelloService') 19 | .invoke('hello') 20 | .send([ 'gxcsoccer' ]) 21 | .expect('hello gxcsoccer', done); 22 | }); 23 | 24 | it('should invoke MathService', done => { 25 | app.rpcRequest('com.alipay.nodejs.rpc.MathService') 26 | .invoke('plus') 27 | .send([ 1, 2 ]) 28 | .expect(3, done); 29 | }); 30 | }); 31 | --------------------------------------------------------------------------------