gRPC 服务搭建教程 - Node.js

219 阅读5分钟

1. 概述

gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 和 Protocol Buffers(protobuf),适用于微服务间通信、实时数据传输等场景。

术语表

术语描述
Protocol BuffersGoogle 开源的一种高效的数据序列化格式,用于结构化数据的编码和解码。
HTTP/2一种现代化的网络传输协议,相较于 HTTP/1.1,它提供了更高的性能和效率。
protoProtocol 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。