Node gRPC示例项目搭建

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节点。

  • 定义startbabel-node index.js,可通过npm run start运行;
  • 定义buildbabel 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
  }
}

数据存储

由于示例没有采用sqlnosql技术,因此需要有一个数据仓库来在线存储数据。

首先创建一些模拟数据,在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配置对照表

摘自官方配置对照表

字段名称有效值描述
keepCasetruefalse保留字段名称。默认是将它们更改为驼峰大小写。
longsStringNumber用于表示long值的类型。默认为Long对象类型。
enumsString用于表示enum值的类型。默认为numeric值。
bytesArrayString用于表示bytes值的类型。默认为Buffer
defaultstruefalse输出Object时设置默认值。默认为false
arraystruefalse为空Array设置缺省数组值(不受defaults选项影响)。默认为false
objectstruefalse为空Object设置缺省数组值(不受defaults选项影响)。默认为false
oneofstruefalse将虚拟oneof属性设置为当前字段的名称。默认为false
includeDirs字符串数组导入.proto文件的搜索路径列表。

注:protocol-buffers中的oneof用于定义字段最多可以设置一次,因此一个新的oneof字段将替换掉旧的字段。关于oneof选项可参考oneofOneof 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"
  }
}

在VxWorks操作系统中,可以使用gRPC框架来实现跨网络的远程过程调用。gRPC是一个高性能、通用的RPC框架,支持多种编程语言和平台。 以下是在VxWorks中使用gRPC示例步骤: 1. 定义gRPC接口:使用Protocol Buffers语言(proto)定义远程过程接口和消息结构。proto文件描述了远程过程的参数和返回值类型以及消息结构的布局。 2. 编写proto文件:创建一个proto文件,描述远程过程接口和消息结构。例如,可以使用Protocol Buffers语言编写proto文件。 3. 生成代码:使用protoc工具将proto文件编译为对应的语言代码。在VxWorks中,可以使用protoc工具来生成C++代码。 4. 实现服务器端:在服务器端编写实际的远程过程实现代码。这些代码将包含在VxWorks应用程序中,并在服务器上运行。服务器端代码将注册远程过程,并提供实现逻辑。 5. 实现客户端:在客户端编写调用远程过程的代码。这些代码将包含在VxWorks应用程序中,并在客户端上运行。客户端代码将通过调用本地的接口函数来触发gRPC调用。 6. 构建和链接:将服务器端和客户端的代码与VxWorks操作系统一起构建和链接,生成可执行文件。 7. 启动服务器和客户端:在VxWorks系统中,启动服务器和客户端应用程序。服务器应处于监听状态,等待来自客户端的gRPC调用请求。 8. 远程过程调用:客户端应用程序通过调用本地接口函数来触发gRPC调用。gRPC框架将负责将调用请求传递给服务器端,并将响应返回给客户端。 9. 处理错误和异常情况:在RPC调用过程中,需要处理错误和异常情况,例如网络故障、超时等。gRPC提供了相应的机制来处理这些情况。 需要注意的是,以上步骤是通用的gRPC使用方式,适用于多种操作系统和平台。在VxWorks中使用gRPC时,可以参考VxWorks的官方文档和相关资料,了解更多关于在VxWorks中实现gRPC的详细步骤和指导。VxWorks提供了一些特定的API和工具,如protoc工具和VxWorks的网络通信机制,用于简化和支持gRPC的使用。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值