如何通過NodeJS輕松使用GRPC和協議緩沖區


本文將對GRPC和協議緩沖區進行基本介紹。接下來,我將展示如何在NodeJS應用程序中使用GRPC和協議緩沖區

GRPC是什么

GRPC是一個開源高性能RPC框架,那么RPC到底是做什么的呢?請看下面的例子

function getEmployee() {
  return "ABCD";
}
function greetEmployee()
{
  let employee = getEmployee();
  console.log("Greet",employee) 
} 

在這里,我們有一個getEmployee函數,它返回一個Employee Name,另一個函數greetEmployee,它調用getEmployee並獲取該員工的Name,然后打印一個Greeting

這里的greetEmployee調用getEmployee是一個常規函數調用。

現在,如果getEmployeegreetEmployee函數位於不同的地址空間中,或者它們位於由網絡分隔的2個不同的主機中,則該函數調用稱為“遠程過程調用”(Remote Procedure Call, RPC)。在這里,具有getEmployee函數的System充當服務器,而具有greetEmployee函數的System充當客戶端。

什么是Protocol Buffer (協議緩沖區)

協議緩沖區是默認情況下在GRPC中使用的接口定義語言。

  • 它有助於定義服務器提供的各種服務
  • 它有助於定義系統中使用的有效負載的結構
  • 它有助於對消息進行序列化(轉換為特殊的二進制格式),並通過服務器和客戶端之間的線路進行發送。

在本文后面的部分中,我們將在使用NodeJS應用程序時看到如何使用protocol buffers(協議緩沖區)。

支持哪些不同類型的RPC?

Unary RPC

這是可用的最簡單的RPC。客戶端在此處向服務器發送請求消息。服務器處理該請求,然后將響應消息發送回客戶端

在本文中,這是我們將重點介紹的grpc。

Server Streaming RPC

在此RPC中,客戶端將請求消息發送到服務器,然后服務器以流方式將一系列消息發送回客戶端。

Client Streaming RPC

在此RPC中,客戶端以流方式向服務器發送一系列消息。然后,服務器處理所有這些請求,然后將響應消息發送回客戶端。

Bidirectional Streaming RPC

在此RPC中,客戶端以流方式向服務器發送一系列消息。然后,服務器處理請求,然后以流方式將一系列消息發送回客戶端。

如何在NodeJS中使用GRPC和協議緩沖區

使用以下命令創建一個名為grpc-nodejs-demo的文件夾並在其中初始化nodejs

mkdir grpc-nodejs-demo
cd grpc-nodejs-demo
npm init

這將創建一個package.json文件。

修改package.json文件

將package.json文件替換為以下內容

{
  "name": "grpc-nodejs-demo",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "@grpc/proto-loader": "^0.1.0",
    "grpc": "^1.11.0",
    "lodash": "^4.6.1"
  },
  "author": "Name",
  "license": "MIT"
}

在這里我們添加了3個依賴項

  • @ grpc / proto_loader和grpc依賴項將幫助我們在應用程序中使用GRPC和協議緩沖區
  • lodash是通用實用程序依賴項。這將有助於簡化一些代碼邏輯

package.json文件更新后。運行以下命令以安裝依賴項

npm install

定義協議緩沖區

在此示例中,我們將構建一個將employee ID作為輸入,並提供employee詳細信息作為輸出的服務。

所需的服務接口和有效負載將在協議緩沖區文件中指定。協議緩沖區文件的擴展名為.proto

現在,我們來創建.proto文件。

在項目中創建一個名為proto的文件夾。在proto文件夾中,創建一個名為employee.proto的文件,並向其中添加以下代碼

syntax = "proto3";

package employee;

service Employee {
  rpc getDetails (EmployeeRequest) returns (EmployeeResponse) {}
}


message EmployeeRequest {
  int32 id = 1;
}

message EmployeeResponse{
  EmployeeDetails message = 1;
}
message EmployeeDetails {
  int32 id = 1;
  string email = 2;
  string firstName = 3; 
  string lastName = 4;
}

那么,我們在這里到底做了什么?

syntax="proto3";表示我們要使用Protocol Buffer version 3。

package employee; 表示我們正在創建一個名為employee的程序包,我們將在其中定義我們的services

service Employee {
  rpc getDetails (EmployeeRequest) returns (EmployeeResponse) {}
}

上述腳本表明我們正在創建一個稱為Employee的服務。在此服務中,我們正在創建一個名為getDetails的函數(rpc),該函數接受EmployeeRequest類型的輸入並以EmployeeResponse格式提供響應

接下來,我們需要定義EmployeeRequestEmployeeResponse。這是在以下腳本中完成的

message EmployeeRequest {
  int32 id = 1;
}

message EmployeeResponse{
  EmployeeDetails message = 1;
}
message EmployeeDetails {
  int32 id = 1;
  string email = 2;
  string firstName = 3; 
  string lastName = 4;
}

在這里,我們看到消息EmployeeRequest具有單個類型為int32和名稱id的字段。此處分配的數字1是字段號,它在消息的編碼和解碼過程中提供幫助。定義的每個字段應具有唯一的字段號

我們還看到EmployeeResponse有一個類型為EmployeeDetails的自定義字段,並且名稱消息的字段編號為1。這意味着甚至也必須定義EmployeeDetails,如上所示。

EmployeeDetails具有4個字段,包括類型int32string。它們都有唯一的字段編號(unique field numbers)

Field numbers between 1 -15 use 1 byte of space during encoding. and field numbers from 2 - 2047 uses 2 bytes for encoding and hence will take up more space. So try to design in such a way that the field numbers are between 1 - 15 as much as possible

1 -15之間的編號在編碼過程中使用1個字節的空間。並且2-2047的字段編號使用2個字節進行編碼,因此會占用更多空間。因此,請嘗試以盡可能多的字段編號介於1到15之間的方式進行設計

Creating the GRPC Server

創建一個名為server.js的文件

首先,讓我們包括我們需要的所有庫,並定義.proto文件所在的位置

const PROTO_PATH = __dirname + '/proto/employee.proto';

const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const _ = require('lodash');

接下來,我們需要加載.proto文件。這是使用protoLoader庫的loadSync方法完成的。

let packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    }
);

接下來,從已加載的原始文件包定義中,我們需要獲取所需的包。這是使用以下腳本完成的

let employee_proto = grpc.loadPackageDefinition(packageDefinition).employee;

在這里,我們將employee包放入employee_proto變量中。

employee_proto現在將具有所有原型定義。

接下來,我們需要創建一些虛擬employees數據供服務器使用。創建一個名為data.js的文件,並將以下腳本添加到其中

let employees = [{
    id: 1,
    email: "abcd@abcd.com",
    firstName: "First1",
    lastName: "Last1"   
},
{
    id: 2,
    email: "xyz@xyz.com",
    firstName: "First2",
    lastName: "Last2"   
},
{
    id: 3,
    email: "temp@temp.com",
    firstName: "First3",
    lastName: "Last3"   
},
];

exports.employees = employees;

接下來,我們需要將data.js導入server.js。為此,在server.js中添加以下腳本

function main() {
  let server = new grpc.Server();
  server.addService(employee_proto.Employee.service, {getDetails: getDetails});
  server.bind('0.0.0.0:4500', grpc.ServerCredentials.createInsecure());
  server.start();
}

let server = new grpc.Server();

是創建新GRPC Server的腳本

.proto文件中,我們注意到在Employee Service內部有一個名為getDetails的函數。

server.addService(employee_proto.Employee.service,{getDetails:getDetails});

是我們在其中添加Service實現的腳本。該腳本表明,我們在employee_proto.Employee Service中添加了getDetails函數。然后,我們將此服務添加到服務器。

server.bind('0.0.0.0:4500', grpc.ServerCredentials.createInsecure()); 是指示服務器將在端口4500上啟動且沒有身份驗證的腳本

server.start(); 是實際啟動服務器的腳本

現在待處理的主要事情是實現getDetails函數。下面的腳本顯示了實現

function getDetails(call, callback) {
  callback(null, 
    {
       message: _.find(employees, { id: call.request.id })
    });
}

這里的call具有請求參數,而回callback 是我們需要定義實現的地方。

在callback 內部,我們收到一條消息:_.find(employees,{id:call.request.id}),其中顯示以下內容

  • 從Input獲取員工ID-call.request.id
  • 搜索employees列表以查找具有該ID的員工
  • Return employee詳細信息

這樣就完成了服務器的實現。這是server.js的完整腳本

const PROTO_PATH = __dirname + '/proto/employee.proto';

const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const _ = require('lodash');

let packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
let employee_proto = grpc.loadPackageDefinition(packageDefinition).employee;

let {employees} = require('./data.js');

function getDetails(call, callback) {
  console.log('client call :%j', call)
  callback(null, 
    {
       message: _.find(employees, { id: call.request.id })
    });
}

function main() {
  let server = new grpc.Server();
  server.addService(employee_proto.Employee.service, {getDetails: getDetails});
  server.bind('0.0.0.0:4500', grpc.ServerCredentials.createInsecure());
  server.start();
}

main();

Creating the GRPC Client

創建一個名為client.js的文件 將以下腳本復制到client.js

const PROTO_PATH = __dirname + '/proto/employee.proto';

const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');

let packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    }
);
let employee_proto = grpc.loadPackageDefinition(packageDefinition).employee;

上面的腳本以與server.js中相同的方式將employee包加載到employee_proto變量中

接下來,我們需要一種可以調用RPC的方式。在這種情況下,我們需要能夠調用服務器中實現的getDetails函數。

為此,我們需要在客戶端中創建一個存根(stub)。這是使用以下腳本完成的。

  let client = new employee_proto.Employee('localhost:4500', grpc.credentials.createInsecure());

client Stub將幫助我們調用在服務器上運行的Employee Service中定義的getDetails函數。服務器依次在端口4500上運行。代碼行還指示未使用身份驗證

最后,我們可以使用以下腳本調用getDetails函數

let employeeId = 1;
 client.getDetails({id: employeeId}, function(err, response) {
    console.log('Employee Details for Employee Id:',employeeId,'\n' ,response.message);
  });

如前所述,客戶端存根可以像正常函數調用一樣幫助我們在服務器中調用getDetails函數。為此,我們將employeeId作為輸入。

最后,Response 進入response 變量。然后,我們將打印響應消息。

完整的client.js代碼如下

const PROTO_PATH = __dirname + '/proto/employee.proto';

const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');

let packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
let employee_proto = grpc.loadPackageDefinition(packageDefinition).employee;

function main() {
  let client = new employee_proto.Employee('localhost:4500',
                                       grpc.credentials.createInsecure());
  let employeeId;
  if (process.argv.length >= 3) {
    employeeId = process.argv[2];
  } else {
    employeeId = 1;
  }
  client.getDetails({id: employeeId}, function(err, response) {
    console.log('Employee Details for Employee Id:',employeeId,'\n' ,response.message);
  });
}

main();

Running The Server and Client

打開命令提示符並使用以下命令運行服務器

node server.js

打開一個新的命令提示符,並使用以下命令運行客戶端

node client.js

當我們運行客戶端時。它將打印以下輸出

Employee Details for Employee Id: 1 
 { id: 1,
  email: 'abcd@abcd.com',
  firstName: 'First1',
  lastName: 'Last1' 
 }

運行截圖

image-20210205145039887

原文地址

How to Easily use GRPC and Protocol Buffers with NodeJS

關於GRPC Streams in NodeJS參考作者原文

我的示例源碼地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM