1. 概述
gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 和 Protocol Buffers(protobuf),适用于微服务间通信、实时数据传输等场景。
术语表
| 术语 | 描述 |
|---|---|
| Protocol Buffers | Google 开源的一种高效的数据序列化格式,用于结构化数据的编码和解码。 |
| HTTP/2 | 一种现代化的网络传输协议,相较于 HTTP/1.1,它提供了更高的性能和效率。 |
| proto | Protocol Buffers 声明数据结构的类型文件,用于定义消息格式和 RPC 服务。 |
2. 前置准备
相关 npm 包准备:
为了搭建 gRPC 环境并加载 .proto 文件,需要安装以下两个 npm 包:
npm install @grpc/grpc-js @grpc/proto-loader
- @grpc/grpc-js:用于搭建 grpc 环境。
- @grpc/proto-loader:用于加载
.proto文件。 这两个包是 gRPC 在 Node.js 环境中的核心依赖,通过它们可以实现 gRPC 服务的开发和调用。
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.7.15",
说明:网上还可以找到 npm 包名叫 grpc 的,但是使用方式实际是与 @grpc/grpc-js 不同的,注意本案例使用的是 @grpc/grpc
数据生成脚本,模拟海量数据
const fs = require('fs')
const path = require('path')
// 生成随机字符串
function generateRandomString(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
// 生成包含 10 个字段的数据
function generateDataWith10Fields() {
return {
field1: generateRandomString(10),
field2: Math.floor(Math.random() * 1000),
field3: Math.random(),
field4: generateRandomString(5),
field5: Math.floor(Math.random() * 100),
field6: Math.random() > 0.5,
field7: generateRandomString(8),
field8: Math.floor(Math.random() * 10000),
field9: Math.random(),
field10: generateRandomString(12)
};
}
// 生成包含 5 个字段的数据
function generateDataWith5Fields() {
return {
field1: generateRandomString(10),
field2: Math.floor(Math.random() * 1000),
field3: Math.random(),
field4: generateRandomString(5),
field5: Math.floor(Math.random() * 100)
};
}
// 生成一百万条数据
function generateOneMillionData() {
const dataWith10Fields = [];
const dataWith5Fields = [];
for (let i = 0; i < 1000000; i++) {
dataWith10Fields.push(generateDataWith10Fields());
dataWith5Fields.push(generateDataWith5Fields());
}
return { dataWith10Fields, dataWith5Fields };
}
// 调用函数生成数据
const { dataWith10Fields, dataWith5Fields } = generateOneMillionData();
// 打印部分数据以验证
fs.writeFileSync(path.join(__dirname , '/../data/100w-ten.json'), JSON.stringify(dataWith10Fields))
fs.writeFileSync(path.join(__dirname , '/../data/100w-five.json'), JSON.stringify(dataWith5Fields))
// console.log('Data with 10 fields:', dataWith10Fields.slice(0, 10));
// console.log('Data with 5 fields:', dataWith5Fields.slice(0, 10));
3. 环境搭建
3.1 定义数据交换格式
编写 proto 文件 service.proto,定义数据传输格式。(文件名无特殊要求)
syntax = "proto3"; // 声明 proto 文件版本,必须放在文件首部
package service;
service Greeter { // service关键字 定义服务提供者结构体
rpc simpleUpload(Request) returns (Response); //远程调用的本地方法,需要服务端实现接口
rpc streamUpload(stream Request) returns (Response); // 流式上传语法,主要是stream 关键字决定,如果 returns 后响应的消息也是 stream,那么响应也是流式的,可以全双工,可以半双工
}
message Request { // message关键字 定义数据交换的结构
string data = 1; // 每一个属性后面的数字都是编号,在同一个结构体中必须唯一,而不是默认值
}
message Response {
string message = 1; // 消息
string code = 2; // 状态码
string taskId = 3; // 任务 ID
}
小tips:可以安装相关插件,具有代码提示,不然编写代码默认是和 txt 一样没有任何提示的。我使用的是 WebStorm 中的 Protocol Buffers 插件
3.2 创建服务端
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')
const proto_path = __dirname + '/proto/service.proto'
const packageDefinition = protoLoader.loadSync(proto_path, {
//可定义配置参数,自查
})
const service = grpc.loadPackageDefinition(packageDefinition).service // .service 是 proto 文件中声明的包名
// 1.创建服务
const server = new grpc.Server()
// 2.添加服务
server.addService(service.Greeter.service, {
//这里编写刚刚 proto 文件中本地方法接口的实现
simpleUpload(call, callback) {
// 1. 获取请求参数
const req = call.request
const data = JSON.parse(req.data)// req 后面的属性就属于 message 消息体中的属性了
console.log(data)
// 2. 处理业务
console.log('正在处理业务')
// 3. 响应结果
const res = {message: 'ok', code: '200', taskId: Date.now().toString()}
callback(null, res)
},
streamUpload(call, callback) {
// 获取元信息,类似请求头
const info = JSON.parse(call.metadata.get('info') || '{}')
const chunks = []
// 等待客户端传输数据
call.on('data', (data) => {
if(typeof data.data !== 'string') return callback(null, {code:'500', message: '数据传输格式有误'})
// 注意控制数据分片大小,否则 ... 解构表达式容易栈内存溢出
chunks.push(... JSON.parse(data.data))
})
// 数据上传完毕
call.on('end', () => {
// 响应结构,处理业务
console.log('数据格式:', chunks)
console.log('数据元信息', info)
callback(null, {code: '200', message: 'ok', taskId: Date.now().toString()})
})
// 异常处理
call.on('error', err => {
callback(null, {code: '500', message: err.message})
})
}
})
const port = 7777
const host = '0.0.0.0'
// 3.绑定端口
server.bindAsync(`${host}:${port}`,grpc.ServerCredentials.createInsecure(), () => {
// 4.启动服务
server.start()
console.log(`gRPC 服务启动成功,端口号: ${port}`)
})
3.3 创建客户端
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')
const proto_path = __dirname + '/proto/service.proto'
const packageDefinition = protoLoader.loadSync(proto_path, {
//可定义配置参数,自查
})
const service = grpc.loadPackageDefinition(packageDefinition).service // .service 是 proto 文件中声明的包名
const host = 'localhost'
const port = 7777
// 1. 创建客户端
const client = new service.Greeter(`${host}:${port}`, grpc.credentials.createInsecure(), {
// 客户端配置,按需自查
})
const simpleData = [
{id: 1, title: '《被讨厌的勇气》', price: 88.8},
{id: 2, title: '《穷查理宝典》', price: 100},
{id: 3, title: '《认知觉醒》', price: 200},
{id: 4, title: '《痛点》', price: 200},
{id: 5, title: '《复杂》', price: 110},
{id: 6, title: '《人性的弱点》', price: 28}
]
// 2. 调用服务 简单数据传输
client.simpleUpload({data: JSON.stringify(simpleData)}, (err, response) => {
if (err) return console.error(err)
console.log(response)
})
// 3. 调用分片传输服务
const requestMetadata = new grpc.Metadata()
requestMetadata.set('info', JSON.stringify({
filename: '2025_06_30_export',
format: 'csv',
compression: 'gzip'
}))
// 模拟百万数据
const streamData = []//TODO 请自行使用文档前置准备部分提供的数据生成脚本模拟数据
const call = client.streamUpload(requestMetadata, (err, response) => {
if (err) return console.error(err)
console.log(response)
})
// 对数据进行分片
const SHARD_SIZE = 20000;
for (let i = 0; i < streamData.length; i += SHARD_SIZE) {
const shard = streamData.slice(i, i + SHARD_SIZE);
call.write({data: JSON.stringify(shard)})
}
call.end()
4. 结语
至此,简单的 gRPC 环境搭建就完成了,本文中的案例不是纯粹的 Protocol Buffers 作为数据交换格式,采用的是 JSON + Protocal Buffers 在灵活性和高性能之间平衡,因为本人实际的需求上传的数据内容格式不是固定的,JSON 的转换是在服务端和客户端完成,数据在网络传输仍然是 Protocol Buffers,减少了带宽的占用。
补充:gRPC 主要是内部微服务之间的数据传输,如果想在浏览器环境中使用,请使用 grpc-web。