Node gRPC示例项目搭建
搭建项目
初始化项目
创建grpc-test
文件夹,在cmder
中移动到此文件夹。
运行npm init
,输入内容可全部按Enter
键默认选择:
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help json` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (grpc-test)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to C:\Users\Administrator\Desktop\grpc-test\package.json:
{
"name": "grpc-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Is this ok? (yes)
搭建ES6环境
在控制台运行命令:
npm install --save-dev babel-preset-env babel-cli
在根目录下新建一个.babelrc文件写入:
{
"presets": ["env"]
}
创建一个index.js
文件用于启动,首行为:
require('babel-core/register')
// 然后写你的js代码
修改package.json
文件的scripts
节点。
- 定义
start
为babel-node index.js
,可通过npm run start
运行; - 定义
build
为babel src --out-dir dist
,它是将src
文件夹编译到dist
,可通过npm run build
运行。
如下所示:
{
"scripts": {
"start": "babel-node index.js",
"build": "babel src --out-dir dist"
}
}
搭建GRPC环境
安装grpc
和@grpc/proto-loader
:
npm install grpc @grpc/proto-loader --save
proto-loader
用于加载proto
文件以与gRPC
一起使用
有关@grpc/proto-loader
请参考@grpc/proto-loader
目录结构
目录结构如下:
/grpc-test
│ .babelrc
│ index.js
│ package.json
│
├─client // rpc客户端
├─protos // 存放protos文件
├─server // rpc服务
├─store // 数据仓库,用于存放数据。
└─utils // 工具类
开发环境准备
根据官方介绍,共计四种RPC
方式:
- 简单的
RPC
(simple RPC) - 服务器端流式
RPC
(server-side streaming RPC) - 客户端流式
RPC
(client-side streaming RPC) - 双向流式
RPC
(bidirectional streaming RPC)
接下来将会分别体验“简单的RPC
”和“双向流式RPC
方式”
服务启动代码
由于需要示范多个服务,为保证代码的树状结构,可以先定义一个顶层的服务类,然后由四个服务继承之。这样,如果服务之间具有相似度很高的代码时,可以将此代码写在顶层服务类。
我们可以将服务的运行代码写在,这个服务类里。
在server
文件夹下创建一个rpc_server.js
文件,代码如下:
import grpc from 'grpc'
export default class RPCServer {
/**
* 服务的运行方法
*
* @param {*} methods 需要绑定到服务器的方法对象
* @param {*} port 需要绑定的端口。默认50051
* @param {*} host 需要绑定的ip。默认0.0.0.0
* @param {*} callback 服务回调函数,回调时传入`grpc`。需返回在从`proto`文件中读取出的定义后,在定义中的`service`定义下的服务
* @returns server 服务对象
*/
run (methods, port, host, callback) {
const service = callback(grpc)
// 获取一个新服务器
var grpcServer = new grpc.Server();
this.server = grpcServer
// 处理函数绑定到服务方法
grpcServer.addProtoService(service, methods)
// 请在未使用的端口上启动服务器
host = host || '0.0.0.0'
port = port || '50051'
// see https://grpc.github.io/grpc/node/grpc.ServerCredentials.html
// ServerCredentials 服务凭证工厂
// createInsecure 创建不安全的服务器凭证
grpcServer.bind(`${host}:${port}`, grpc.ServerCredentials.createInsecure());
grpcServer.start();
return grpcServer;
}
/**
* 强制关闭已存在的服务器
*/
close () {
if (this.server) {
this.server.forceShutdown()
} else {
throw 'Service not started'
}
}
}
客户端启动代码
同样为了为保证代码的树状结构,创建一个顶层客户端类。
import grpc from 'grpc'
export default class RPCClient {
/**
* 服务的运行方法
*
* @param {*} port 需要绑定的端口。默认50051
* @param {*} host 需要绑定的`ip`。默认0.0.0.0
* @param {*} callback 客户端回调函数,回调时传入`grpc`。需返回从`proto`文件中读取出的定义中的`service`定义
* @returns client 客户端对象
*/
run(port, host, callback) {
host = host || '0.0.0.0'
port = port || '50051'
const ClientObject = callback(grpc)
// see https://grpc.github.io/grpc/node/grpc.ServerCredentials.html
// ServerCredentials 服务凭证工厂
// createInsecure 创建不安全的服务器凭证
const client = new ClientObject(`${host}:${port}`, grpc.credentials.createInsecure())
this.client = client
return client
}
}
读取proto文件
读取proto
文件的代码具有很强的通用性,明显可以写成工具类util
; 但它不是服务需要关注的事情,因此不写在ServerRPC
里。
在utils
目录下新增index.js
,内容如下:
import * as protoLoader from '@grpc/proto-loader'
export default class Utils {
static readPackageDefinition (path) {
// 读取文件的路径
const PROTO_PATH = `${__dirname}/${path}`
// see https://github.com/grpc/grpc-node/tree/master/packages/proto-loader
// 使用protoLoader加载proto文件
const packageDefinition = protoLoader.loadSync(
PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
})
return packageDefinition
}
}
数据存储
由于示例没有采用sql
或nosql
技术,因此需要有一个数据仓库来在线存储数据。
首先创建一些模拟数据,在store
目录下创建一个feature_list.json
文件,模拟一些数据:
[{
"location": {
"latitude": 407838351,
"longitude": -746143763
},
"name": "Patriots Path, Mendham, NJ 07945, USA"
}, {
"location": {
"latitude": 408122808,
"longitude": -743999179
},
"name": "101 New Jersey 10, Whippany, NJ 07981, USA"
}, {
"location": {
"latitude": 413628156,
"longitude": -749015468
},
"name": "U.S. 6, Shohola, PA 18458, USA"
}, {
"location": {
"latitude": 414008389,
"longitude": -743951297
},
"name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA"
}, {
"location": {
"latitude": 409146138,
"longitude": -746188906
},
"name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA"
}]
在store
目录下创建一个index.js
文件,代码如下:
import featureList from './feature_list.json'
// 使用Object.freeze冻结store不可修改
export default Object.freeze({
featureList
})
开始开发
简单的RPC
定义proto文件1
在protos
文件夹下创建一个simple_rpc.proto
文件, 文件内容:
syntax = "proto3";
package simplerpc;
/**
* 一个简单的RPC,由客户端向服务器发送请求并等待响应返回
*/
service SimpleRPC {
rpc GetFeature(Point) returns (Feature) {};
}
// 点
message Point {
// 纬度
int32 latitude = 1;
// 经度
int32 longitude = 2;
}
// 特征
message Feature {
string name = 1;
// 位置
Point location = 2;
}
定义服务1
定义一个服务,此服务只有一个从feature_list
中获取一个feature
的逻辑。
在server
文件夹下创建一个rpc_simple_server.js
文件,内容如下:
import Utils from '../utils'
import store from '../store'
import RPCServer from './rpc_server'
// 从数据仓库中取出数据
const featureList = store.featureList
export default class RPCSimpleServer extends RPCServer {
constructor (port, host, proto) {
super()
this.port = port || '50051'
this.host = host || '0.0.0.0'
this.proto = proto || '../protos/simple_rpc.proto'
}
checkFeature(point) {
// 从数组中匹配一个point对象
const result = featureList.filter((feature) => {
return feature.location.latitude === point.latitude &&
feature.location.longitude === point.longitude
})
return result.length === 1? result[0] : {
name: '',
location: point
}
}
getFeature(call, callback) {
console.log(`[RPCSimpleServer] 服务器获得消息: ${JSON.stringify(call.request)}`)
callback(null, this.checkFeature(call.request))
}
/**
* 启动服务
*/
start() {
return super.run(this, this.port, this.host, (grpc) => {
const packageDefinition = Utils.readPackageDefinition(this.proto)
// simplerpc是proto中的package定义
const simplerpc = grpc.loadPackageDefinition(packageDefinition).simplerpc
// SimpleRPC是proto中的service定义
const service = simplerpc.SimpleRPC.service
return service
})
}
}
定义客户端1
定义一个客户端,用于调用服务器的getFeature
方法。
在client
文件夹下创建一个rpc_simple_client.js
文件,内容如下:
import Utils from '../utils'
import RPCClient from './rpc_client'
export default class RPCSimpleClient extends RPCClient {
constructor (port, host, proto) {
super()
this.port = port || '50051'
this.host = host || '0.0.0.0'
this.proto = proto || '../protos/simple_rpc.proto'
}
getFeature(point) {
this.getClient().getFeature(point, (error, feature) => {
if (error) {
console.error(`error: ${error}`)
} else {
console.log(`[RPCSimpleClient] 服务器返回消息 ${JSON.stringify(feature)}`)
}
})
}
getClient () {
if (this.client) {
return this.client
} else {
this.start()
return this.client
}
}
/**
* 启动客户端
*/
start() {
return super.run(this.port, this.host, (grpc) => {
const packageDefinition = Utils.readPackageDefinition(this.proto)
// simplerpc是proto中的package定义
const simplerpc = grpc.loadPackageDefinition(packageDefinition).simplerpc
// SimpleRPC是proto中的service定义
return simplerpc.SimpleRPC
})
}
}
运行1
修改根目录下的index.js
文件,修改后的内容为:
import 'babel-core/register'
import RPCSimpleServer from './server/rpc_simple_server'
import RPCSimpleClient from './client/rpc_simple_client'
// simple RPC
const simpleServer = new RPCSimpleServer()
const simpleClient = new RPCSimpleClient()
simpleServer.start()
simpleClient.start()
simpleClient.getFeature({
latitude: 409146138,
longitude: -746188906
})
simpleClient.getFeature({
latitude: 0,
longitude: 0
})
可在控制台输入npm run start
运行。
运行结果参考:
[RPCSimpleServer] 服务器获得消息: {"latitude":0,"longitude":0}
[RPCSimpleServer] 服务器获得消息: {"latitude":409146138,"longitude":-746188906}
[RPCSimpleClient] 服务器返回消息 {"name":"","location":{"latitude":0,"longitude":0}}
[RPCSimpleClient] 服务器返回消息 {"name":"Berkshire Valley Management Area Trail, Jefferson, NJ, USA","location":{"latitude":409146138,"longitude":-746188906}}
双向流式RPC
定义proto文件2
在protos
文件夹下创建一个simple_rpc.proto
文件, 文件内容:
syntax = "proto3";
package bidirectionalrpc;
/**
* 一个双向流动的RPC,双方使用读写流发送一系列消息。
*/
service BidirectionalRPC {
rpc listFeature(stream Point) returns (stream Feature) {};
}
// 点
message Point {
// 纬度
int32 latitude = 1;
// 经度
int32 longitude = 2;
}
// 特征
message Feature {
string name = 1;
// 位置
Point location = 2;
}
可以看出与简单RPC
相比,多了两个stream
关键字来定义流。
定义服务2
在server
文件夹下创建一个rpc_bidirectional_server.js
文件,内容如下:
import Utils from '../utils'
import store from '../store'
import RPCServer from './rpc_server'
const featureList = store.featureList
export default class RPCBidirectionalServer extends RPCServer {
constructor (port, host, proto) {
super()
this.port = port || '50052'
this.host = host || '0.0.0.0'
this.proto = proto || '../protos/bidirectional_rpc.proto'
}
checkFeature(point) {
// 从数组中匹配一个point对象
const result = featureList.filter((feature) => {
return feature.location.latitude === point.latitude &&
feature.location.longitude === point.longitude
})
return result.length === 1? result[0] : {
name: '',
location: point
}
}
listFeature(call) {
call.on('data', (point) => {
console.log('[RPCBidirectionalServer] 服务器获得消息:' + JSON.stringify(point))
const result = this.checkFeature(point)
call.write(result)
})
call.on('end', function() {
call.end()
})
}
/**
* 启动服务
*/
start() {
return super.run(this, this.port, this.host, (grpc) => {
const packageDefinition = Utils.readPackageDefinition(this.proto)
// bidirectionalrpc是proto中的package定义
const bidirectionalrpc = grpc.loadPackageDefinition(packageDefinition).bidirectionalrpc
// BidirectionalRPC是proto中的service定义
const service = bidirectionalrpc.BidirectionalRPC.service
return service
})
}
}
定义客户端2
定义一个客户端,用于调用服务器的listFeature
方法。
在client
文件夹下创建一个rpc_bidirectional_client.js
文件,内容如下:
import Utils from '../utils'
import RPCClient from './rpc_client'
export default class RPCBidirectionalClient extends RPCClient {
constructor(port, host, proto) {
super()
this.port = port || '50052'
this.host = host || '0.0.0.0'
this.proto = proto || '../protos/bidirectional_rpc.proto'
}
listFeature(points) {
const call = this.getClient().listFeature();
// 输出返回消息
call.on('data', function (feature) {
console.log('[RPCBidirectionalClient] 客户端获得消息:' + JSON.stringify(feature))
})
// 发送数组内消息
for (let i = 0; i < points.length; i++) {
const point = points[i];
console.log('[RPCBidirectionalClient] 客户端发送消息 ' + JSON.stringify(point));
call.write(point);
}
call.on('end', function() {
call.end()
})
}
getClient() {
if (this.client) {
return this.client
} else {
this.start()
return this.client
}
}
/**
* 启动客户端
*/
start() {
return super.run(this.port, this.host, (grpc) => {
const packageDefinition = Utils.readPackageDefinition(this.proto)
// bidirectionalrpc是proto中的package定义
const bidirectionalrpc = grpc.loadPackageDefinition(packageDefinition).bidirectionalrpc
// BidirectionalRPC是proto中的service定义
return bidirectionalrpc.BidirectionalRPC
})
}
}
运行2
修改根目录下的index.js
文件,修改后的内容为:
import 'babel-core/register'
import RpcBidirectionalServer from './server/rpc_bidirectional_server'
import RpcBidirectionalClient from './client/rpc_bidirectional_client'
// server-side streaming RPC
var points = [{
"latitude": 408122808,
"longitude": -743999179
},
{
"latitude": 414008389,
"longitude": -743951297
},
{
"latitude": 0,
"longitude": 0
}
]
const rpcBidirectionalServer = new RpcBidirectionalServer()
const rpcBidirectionalClient = new RpcBidirectionalClient()
rpcBidirectionalServer.start()
rpcBidirectionalClient.start()
rpcBidirectionalClient.listFeature(points)
可在控制台输入npm run start
运行
运行结果参考:
[RPCBidirectionalClient] 客户端发送消息 {"latitude":408122808,"longitude":-743999179}
[RPCBidirectionalClient] 客户端发送消息 {"latitude":414008389,"longitude":-743951297}
[RPCBidirectionalClient] 客户端发送消息 {"latitude":0,"longitude":0}
[RPCBidirectionalServer] 服务器获得消息:{"latitude":408122808,"longitude":-743999179}
[RPCBidirectionalClient] 客户端获得消息:{"name":"101 New Jersey 10, Whippany, NJ 07981, USA","location":{"latitude":408122808,"longitude":-743999179}}
[RPCBidirectionalServer] 服务器获得消息:{"latitude":414008389,"longitude":-743951297}
[RPCBidirectionalClient] 客户端获得消息:{"name":"Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA","location":{"latitude":414008389,"longitude":-743951297}}
[RPCBidirectionalServer] 服务器获得消息:{"latitude":0,"longitude":0}
[RPCBidirectionalClient] 客户端获得消息:{"name":"","location":{"latitude":0,"longitude":0}}
参考内容
protoLoader配置对照表
摘自官方配置对照表
字段名称 | 有效值 | 描述 |
---|---|---|
keepCase | true 或 false | 保留字段名称。默认是将它们更改为驼峰大小写。 |
longs | String 或 Number | 用于表示long 值的类型。默认为Long 对象类型。 |
enums | String | 用于表示enum 值的类型。默认为numeric 值。 |
bytes | Array 或 String | 用于表示bytes 值的类型。默认为Buffer 。 |
defaults | true 或 false | 输出Object 时设置默认值。默认为false 。 |
arrays | true 或 false | 为空Array 设置缺省数组值(不受defaults 选项影响)。默认为false 。 |
objects | true 或 false | 为空Object 设置缺省数组值(不受defaults 选项影响)。默认为false 。 |
oneofs | true 或 false | 将虚拟oneof 属性设置为当前字段的名称。默认为false 。 |
includeDirs | 字符串数组 | 导入.proto 文件的搜索路径列表。 |
注:
protocol-buffers
中的oneof
用于定义字段最多可以设置一次,因此一个新的oneof
字段将替换掉旧的字段。关于oneof
选项可参考oneof和Oneof Fields页面
package参考
项目的package.json
内容为:
{
"name": "grpc-test",
"version": "1.0.0",
"description": "prpc测试项目",
"main": "index.js",
"scripts": {
"start": "babel-node index.js",
"build": "babel src --out-dir dist"
},
"author": "zoujiawei6",
"license": "ISC",
"dependencies": {
"@grpc/proto-loader": "^0.5.0",
"grpc": "^1.20.3"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0"
}
}