gRPC(gRPC 远程过程调用 (gRPC Remote Procedure Calls))概述

文档:https://grpc.org.cn/docs

  • gRPC 默认采用 protocol buffer ​作为 IDL (Interface Description Lanage) 接口描述语言,服务之间通信的数据序列化和反序列化也是基于 protocol buffer 的,因为 protocol buffer 的特殊性,所以 gRPC 框架是跨语言的通信框架(与编程语言无关性),也就是说用 Java 开发的基于 gRPC 的服务,可以用 GoLang 编程语言调用。
  • gRPC 是基于 http2 协议实现的。
  • gRPC是一种基于CS模式的架构设计,解决了服务端和客户端编程语言或架构设计不一致但需要通信的问题,HTTP也是CS模式的架构设计,常用来和gRPC比较,相比于HTTP通信,gRPC具有以下特征:(1)gRPC可以自定义消息格式,HTTP的格式是固定的,可能存在性能不足的问题;(2)gRPC通过proto buffer进行数据交换,相比于json和XML,他的序列号和反序列号的效率都更高。
  • gRPC有利于创建分布式应用程序和服务,因为它可以远程调用别的地方的函数。

通信模式:请求-响应服务器流客户端流双向流

gRPC 流式传输极简入门指南 - 知乎 (zhihu.com)

相比普通的 RPC 方法(UnaryCall),采用流式传输的 gRPC 接口,主要是多了一个stream关键字。
关键字修饰参数:客户端流式的 gRPC 接口
关键字修饰返回值:服务器端流式的 gRPC 接口
关键字同时修饰参数和返回值:双向流式的 gRPC 接口

文档

利用官方插件GitHub - pseudomuto/protoc-gen-doc: Documentation generator plugin for Google Protocol Buffers可以生成gRPC服务的文档

protocol buffer

proto buffer是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化。
适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

proto3的文档

gRPC-Protocol语法指南 - 知乎 (zhihu.com)

  • proto buffer有数据结构,有枚举类型,还可以消息里面套消息。
  • .proto文件里还可以import 其他.proto。这是它们之间依赖和关联的方式。

说明

下文是使用Deepseek指导我实现本地gRPC服务器Mock时Deepseek的指导。我按着做了,还挺顺利。斜体是我的备注。

实现本地 gRPC 服务器 Mock

我将指导你创建一个简单的 gRPC 服务器 Mock,使用 Node.js 实现,这样你就可以在本地测试 gRPC 功能了。

准备工作

首先,确保你的系统已安装:

  1. Node.js (版本 12 或更高)
  2. npm (通常随 Node.js 一起安装)

创建 gRPC 服务器项目

1. 创建项目目录并初始化

1
2
3
mkdir grpc-mock-server
cd grpc-mock-server
npm init -y

Windows上,我更习惯手动创建目录。

2. 安装必要的依赖

1
npm install @grpc/grpc-js @grpc/proto-loader google-protobuf

3. 创建项目结构

1
2
3
4
5
6
grpc-mock-server/
├── proto/
│ └── user_service.proto
├── server.js
├── package.json
└── README.md

4. 创建 .proto 文件

proto/user_service.proto 中放入之前讨论的 proto 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
syntax = "proto3";

package user;

service UserService {
rpc GetUser (GetUserRequest) returns (UserResponse);
rpc CreateUser (CreateUserRequest) returns (UserResponse);
}

message GetUserRequest {
string user_id = 1;
bool include_contact_info = 2;
}

message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
}

message UserResponse {
string user_id = 1;
string name = 2;
string email = 3;
int32 age = 4;
repeated string tags = 5;
Address address = 6;
oneof preferred_contact_method {
string phone_number = 7;
string telegram_handle = 8;
}
}

message Address {
string street = 1;
string city = 2;
string zip_code = 3;
}

5. 创建 gRPC 服务器实现

创建 server.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

// 加载 proto 文件
const PROTO_PATH = path.join(__dirname, 'proto', 'user_service.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const userProto = grpc.loadPackageDefinition(packageDefinition).user;

// 模拟数据
const mockUsers = {
"user123": {
user_id: "user123",
name: "张三",
email: "zhangsan@example.com",
age: 30,
tags: ["VIP", "早期用户"],
address: {
street: "人民路123号",
city: "北京",
zip_code: "100000"
},
phone_number: "13800138000"
},
"user456": {
user_id: "user456",
name: "李四",
email: "lisi@example.com",
age: 28,
tags: ["新用户"],
address: {
street: "南京路456号",
city: "上海",
zip_code: "200000"
},
telegram_handle: "@lisi"
}
};

// 实现服务方法
function getUser(call, callback) {
const { user_id, include_contact_info } = call.request;

console.log(`GetUser 请求: user_id=${user_id}, include_contact_info=${include_contact_info}`);

if (!user_id) {
return callback({
code: grpc.status.INVALID_ARGUMENT,
message: "用户ID不能为空"
});
}

const user = mockUsers[user_id];
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: `未找到ID为${user_id}的用户`
});
}

// 创建响应对象的副本
const response = { ...user };

// 如果不包含联系信息,移除敏感字段
if (!include_contact_info) {
delete response.email;
delete response.phone_number;
delete response.telegram_handle;
}

callback(null, response);
}

function createUser(call, callback) {
const { name, email, age } = call.request;

console.log(`CreateUser 请求: name=${name}, email=${email}, age=${age}`);

if (!name || !email || !age) {
return callback({
code: grpc.status.INVALID_ARGUMENT,
message: "所有字段都是必需的"
});
}

if (age < 0 || age > 150) {
return callback({
code: grpc.status.INVALID_ARGUMENT,
message: "年龄必须在0-150之间"
});
}

// 生成新用户ID
const newUserId = 'user' + (Object.keys(mockUsers).length + 1);

// 创建新用户
const newUser = {
user_id: newUserId,
name,
email,
age,
tags: ["新用户"],
address: {
street: "",
city: "",
zip_code: ""
}
};

// 保存到模拟数据库
mockUsers[newUserId] = newUser;

console.log(`创建新用户: ${newUserId}`);
callback(null, newUser);
}

// 启动gRPC服务器
function main() {
const server = new grpc.Server();

server.addService(userProto.UserService.service, {
getUser: getUser,
createUser: createUser
});

const port = "50051";
server.bindAsync(
`0.0.0.0:${port}`,
grpc.ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
console.error("服务器启动失败:", err);
return;
}
console.log(`gRPC 服务器运行在 0.0.0.0:${port}`);
console.log("可用方法:");
console.log("- GetUser: 根据ID获取用户信息");
console.log("- CreateUser: 创建新用户");
}
);
}

main();

6. 更新 package.json

package.json 中添加启动脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "grpc-mock-server",
"version": "1.0.0",
"description": "A mock gRPC server for testing",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@grpc/grpc-js": "^1.6.0",
"@grpc/proto-loader": "^0.6.0",
"google-protobuf": "^3.20.0"
}
}

我打开node.js生成的package.json文件,发现里面已经有一些内容了。
于是看了看,将description、main、scripts字段修改为Deepseek提供的版本,其他的没变。
实际的package.json内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "grpc-mock-server",
"version": "1.0.0",
"description": "A mock gRPC server for testing",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Zebrafishinvines",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.8.0",
"google-protobuf": "^4.0.0"
}
}

运行服务器

1
npm start

如果一切正常,你将看到输出:

1
2
3
4
gRPC 服务器运行在 0.0.0.0:50051
可用方法:
- GetUser: 根据ID获取用户信息
- CreateUser: 创建新用户

到目前为止都很顺利。

使用 Postman 测试

  1. 打开 Postman,创建一个新的 gRPC 请求

  2. 输入服务器地址:localhost:50051

  3. 导入 .proto 文件:

    • 点击 “导入” 按钮
    • 选择 “选择文件” 并上传 proto/user_service.proto
    • 导入后,Postman 会自动解析服务和方法
  4. 测试 GetUser 方法:

    • 选择 “GetUser” 方法

    • 在消息体中输入:

      1
      2
      3
      4
      {
      "user_id": "user123",
      "include_contact_info": true
      }
    • 点击 “调用”

      按照预期返回了结果。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      {
      "tags": [
      "VIP",
      "早期用户"
      ],
      "user_id": "user123",
      "name": "张三",
      "email": "zhangsan@example.com",
      "age": 30,
      "address": {
      "street": "人民路123号",
      "city": "北京",
      "zip_code": "100000"
      },
      "phone_number": "13800138000"
      }

    如果消息体中"include_contact_info"字段设为false,就不会传递phone_number。

  5. 测试 CreateUser 方法:

    • 选择 “CreateUser” 方法

    • 在消息体中输入:

      1
      2
      3
      4
      5
      {
      "name": "王五",
      "email": "wangwu@example.com",
      "age": 25
      }
    • 点击 “调用”

      按照预期返回了结果。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      {
      "tags": [
      "新用户"
      ],
      "user_id": "user3",
      "name": "王五",
      "email": "wangwu@example.com",
      "age": 25,
      "address": {
      "street": "",
      "city": "",
      "zip_code": ""
      }
      }

故障排除(实际上并没有用到)

  1. 端口占用错误:如果 50051 端口已被占用,可以在 server.js 中修改端口号
  2. Proto 文件解析错误:确保 proto 文件语法正确
  3. 连接拒绝:确保服务器正在运行,并且 Postman 中输入的地址正确

扩展功能

你可以根据需要扩展这个 mock 服务器:

  1. 添加更多服务方法
  2. 实现持久化数据存储(如使用 JSON 文件)
  3. 添加身份验证中间件
  4. 实现更复杂的业务逻辑

这个 gRPC mock 服务器现在可以在本地运行,并且可以通过 Postman 进行测试了。你可以根据实际需求调整实现细节。