本文详细介绍如何在 Node.js 中集成 gRPC 和 Protocol Buffers。内容涵盖 RPC 概念、四种 RPC 模式,以及使用 .proto 文件定义服务。通过完整代码示例,演示如何配置依赖、编写服务端逻辑与客户端调用,实现高效的服务间通信。
本文将简要介绍 gRPC 和 Protocol Buffers 的基础知识,并展示如何在 Node.js 应用中结合使用这两者。
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 属于常规函数调用。
如果 getEmployee 和 greetEmployee 函数位于不同的地址空间,或者运行在由网络分隔的两台不同主机上,这种调用方式就被称为“远程过程调用”(Remote Procedure Call, RPC)。在此场景中,拥有 getEmployee 函数的系统充当服务器,而拥有 greetEmployee 函数的系统则充当客户端。
什么是 Protocol Buffer(协议缓冲区)
Protocol Buffer 是 gRPC 默认使用的接口定义语言。
- 它有助于定义服务器所提供的各种服务。
- 它有助于定义系统中使用的有效负载(payload)结构。
- 它能够将消息序列化为特殊的二进制格式,并在服务器与客户端之间通过网络线路进行传输。
在本文的后续部分,我们将看到在 Node.js 应用中使用 Protocol Buffers 的具体方法。
支持哪些不同类型的 RPC?
Unary RPC(简单请求/响应)
这是最基础的一种 RPC 模式。客户端向服务器发送一条请求消息,服务器处理该请求后,将响应消息发回给客户端。
在本文中,我们将重点介绍这种 gRPC 模式。
Server Streaming RPC(服务器流式 RPC)
在此模式下,客户端发送一条请求消息给服务器,随后服务器以流式方式将一系列消息发送回客户端。
Client Streaming RPC(客户端流式 RPC)
在此模式下,客户端以流式方式向服务器发送一系列消息。服务器处理完所有请求后,将一条响应消息发回给客户端。
Bidirectional Streaming RPC(双向流式 RPC)
在此模式下,客户端以流式方式向服务器发送一系列消息。服务器处理这些请求后,再以流式方式将一系列响应消息发回给客户端。
如何在 Node.js 中使用 gRPC 和 Protocol Buffers
使用以下命令创建一个名为 grpc-nodejs-demo 的文件夹,并在其中初始化 Node.js 项目:
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"
}在这里,我们添加了三个依赖项。
- @grpc/proto-loader 和 grpc 依赖项将帮助我们在应用程序中使用 gRPC 和 Protocol Buffers。
- lodash 是一个通用的实用工具库,用于简化部分代码逻辑。
更新 package.json 文件后,运行以下命令以安装依赖项:
npm install定义协议缓冲区
在本示例中,我们将构建一个服务:接收 employee ID 作为输入,并返回员工详细信息作为输出。
所需的服务接口和有效负载将在协议缓冲区文件中指定,该文件的扩展名为 .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 的程序包,我们将在其中定义服务。
service Employee {
rpc getDetails (EmployeeRequest) returns (EmployeeResponse) {}
}上述脚本表明我们正在创建一个名为 Employee 的服务。在此服务中,我们定义了一个名为 getDetails 的函数(rpc),该函数接受类型为 EmployeeRequest 的输入,并以 EmployeeResponse 格式提供响应。
接下来,我们需要定义 EmployeeRequest 和 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;
}在这里,我们看到消息 EmployeeRequest 具有单个字段,类型为 int32,名称为 id。此处分配的数字 1 是字段号,它在消息的编码和解码过程中提供帮助。定义的每个字段都必须具有 唯一的字段号。
我们还看到 EmployeeResponse 包含一个类型为 EmployeeDetails 的自定义字段,且名称消息的字段编号为 1。这意味着也必须如上所示定义 EmployeeDetails。
EmployeeDetails 包含 4 个字段,包括类型 int32 和 string。它们都有唯一的字段编号(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 之间的方式进行设计。
创建 gRPC 服务器
创建一个名为 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 现在将包含所有原型定义。
接下来,我们需要创建一些虚拟 employee 数据供服务器使用。创建一个名为 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 的员工
- 返回员工详细信息
这样就完成了服务器的实现。这是 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();创建 gRPC 客户端
创建一个名为 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();运行服务器和客户端
打开命令提示符并使用以下命令运行服务器:
node server.js打开一个新的命令提示符,并使用以下命令运行客户端:
node client.js当我们运行客户端时,它将打印以下输出:
Employee Details for Employee Id: 1 { id: 1, email: 'abcd@abcd.com', firstName: 'First1', lastName: 'Last1' }
运行截图

原文地址
How to Easily use GRPC and Protocol Buffers with NodeJS
关于 gRPC Streams in NodeJS 参考作者原文
我的示例源码地址
到此这篇关于如何通过 NodeJS 轻松使用 gRPC 和协议缓冲区的文章就介绍到这了,更多相关 NodeJS 使用 gRPC 和协议缓冲区内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!