跳转到内容

gRPC工作原理

gRPC 是一种技术,它让您可以在 客户端 程序中,像调用本地函数一样调用 服务器 上的方法,从而简化分布式应用程序和服务的创建。

这是 gRPC 的 默认接口定义语言 (IDL)数据序列化格式

  • IDL: 用于定义服务接口 (service) 和消息结构 (message)。它是一种语言中立的格式。
  • 序列化: Protobuf 将数据结构高效地序列化为紧凑的二进制格式。相比 JSON 或 XML,它通常更小、更快、更节省带宽。

gRPC 的底层传输协议基于 HTTP/2 标准。这带来了关键优势:

  • 多路复用 (Multiplexing): 允许在 一个 TCP 连接 上同时发送和接收多个独立的 RPC 请求/响应流,避免了 HTTP/1.x 中的队头阻塞问题。
  • 双向流 (Bidirectional Streaming): 客户端和服务器可以同时独立地发送消息流。
  • 头部压缩 (Header Compression): 使用 HPACK 算法压缩 HTTP 头部元数据,减少网络开销。

1.3. 客户端存根(Stub/Client)与服务端(Server)

Section titled “1.3. 客户端存根(Stub/Client)与服务端(Server)”

这是 gRPC 工作流的核心:

  • 服务定义: 开发者使用 Protobuf 定义服务接口。
  • 代码生成: 使用 protoc 工具 and gRPC 插件,根据服务定义自动生成客户端和服务端代码(包括数据结构、客户端存根接口和服务端基类)。
  • 服务端: 实现 生成的服务基类中定义的方法,并运行一个 gRPC 服务器来处理客户端调用。
  • 客户端: 使用生成的 存根 (Stub) 来调用远程方法,调用体验与本地函数类似。

2. ⚙️ 二、gRPC 工作原理(一次 RPC 调用)

Section titled “2. ⚙️ 二、gRPC 工作原理(一次 RPC 调用)”

一次 gRPC 调用是这样发生的:

  1. 客户端调用: 客户端应用程序调用本地 存根 中封装的方法,并传入请求参数(Protobuf 消息)。
  2. 序列化: 客户端存根利用 Protobuf 将请求参数对象 序列化 为二进制字节流。
  3. 传输: gRPC 核心库将序列化的请求通过底层的 HTTP/2 连接发送到服务器。
  4. 接收与反序列化: 服务器接收到字节流后,服务器端的 gRPC 核心库将其 反序列化 回 Protobuf 消息对象。
  5. 业务逻辑: 服务器将请求消息对象传递给开发者实现的 服务方法
  6. 响应: 服务器执行业务逻辑后,返回响应消息对象。该对象被服务器端的 gRPC 核心库 序列化
  7. 返回: 序列化的响应通过 HTTP/2 返回给客户端。
  8. 客户端接收: 客户端存根接收到字节流后,反序列化 为响应消息对象,并返回给客户端应用代码。

3. ✨ 三、 gRPC 的四种服务调用类型

Section titled “3. ✨ 三、 gRPC 的四种服务调用类型”

gRPC 支持四种基本的 RPC 调用类型,全部都基于 HTTP/2 的流式传输能力:

  • 模式: 客户端发送 一个请求,服务器返回 一个响应

  • 特点: 这是最常见的 RPC 模式,类似于传统的函数调用或 RESTful API 请求-响应模型。

  • Protobuf 语法示例:

    rpc SayHello (HelloRequest) returns (HelloResponse);

3.2. 服务器流式 RPC (Server Streaming RPC)

Section titled “3.2. 服务器流式 RPC (Server Streaming RPC)”
  • 模式: 客户端发送 一个请求,服务器返回一个 消息序列(数据流)。

  • 特点: 适用于需要向客户端推送大量数据,或者实时更新数据的场景(例如,获取股票报价、长时间日志流)。

  • Protobuf 语法示例:

    rpc GetFeatures (Rectangle) returns (stream Feature);

3.3. 客户端流式 RPC (Client Streaming RPC)

Section titled “3.3. 客户端流式 RPC (Client Streaming RPC)”
  • 模式: 客户端发送一个 消息序列(数据流),服务器接收完所有消息后,返回 一个响应

  • 特点: 适用于客户端需要发送大量数据给服务器的场景(例如,上传大型文件或发送一系列传感器数据)。

  • Protobuf 语法示例:

    rpc RecordRoute (stream Point) returns (RouteSummary);

3.4. 双向流式 RPC (Bidirectional Streaming RPC)

Section titled “3.4. 双向流式 RPC (Bidirectional Streaming RPC)”
  • 模式: 客户端和服务器都使用独立的读写流来发送 消息序列。两个流独立操作。

  • 特点: 适用于低延迟、实时通信的场景(例如,实时聊天应用、双向数据同步)。客户端和服务器可以以任意顺序读写消息,消息顺序在各自的流中保持不变。

  • Protobuf 语法示例:

    rpc RouteChat (stream RouteNote) returns (stream RouteNote);

  • 高性能: 基于 HTTP/2 的多路复用和 Protobuf 的高效二进制序列化,使得 gRPC 在性能上通常优于基于 HTTP/1.x + JSON 的服务。
  • 多语言支持: gRPC 支持大多数主流编程语言(C++, Java, Python, Go, Node.js, C# 等),非常适合 微服务架构 中多语言服务间的通信。
  • 强类型契约: Protobuf 提供的严格接口定义,在编译时就能捕获类型错误,保证了服务间的 强契约,提高了系统的健壮性和可维护性。
  • 易用性: 自动生成的客户端和服务端代码,大大减少了开发者编写底层网络和序列化代码的工作量。

在您的 .proto 文件中,除了定义数据结构(message)之外,还需要使用 service 关键字来定义 gRPC 服务

syntax = "proto3";
// 同样定义包名
package my_project.services;
// 引入之前定义的数据结构
import "user_data.proto"; // 假设您的 UserProfile 在 user_data.proto 中
// 1. 定义数据结构(请求和响应)
message GetUserRequest {
string user_id = 1;
}
message GetUserResponse {
// 导入并使用 UserProfile 消息
my_project.data.UserProfile profile = 1;
}
// 2. 定义服务接口
service UserService {
// 定义 RPC 方法,指定请求 and 响应消息
rpc GetUser (GetUserRequest) returns (GetUserResponse);
// 也可以定义流式 RPC
rpc ListUsers (stream GetUserRequest) returns (stream GetUserResponse);
}

Protobuf 编译器 protoc 在结合 gRPC 使用时,需要一个特殊的 gRPC Python 插件 来生成额外的服务代码。

您需要安装 gRPC 相关的 Python 工具包:

Terminal window
pip install grpcio grpcio-tools

更新您的自动化构建脚本(如 Makefile)以包含 gRPC 插件。

更新后的 Makefile 示例片段:

# ... 之前定义的变量保持不变 ...
# 定义 gRPC 插件路径
GRPC_PLUGIN_PATH = $(shell which grpc_python_plugin)
# 确保输出目录存在
$(shell mkdir -p $(PYTHON_OUT_DIR))
# 查找所有的 .proto 文件
PROTO_FILES = $(wildcard $(PROTO_DIR)/*.proto)
.PHONY: proto
proto:
$(PROTOC) -I=$(PROTO_DIR) \
--python_out=$(PYTHON_OUT_DIR) \
--grpc_python_out=$(PYTHON_OUT_DIR) \
$(PROTO_FILES)
@echo "✅ Protobuf and gRPC Python code generation complete."

运行此命令后,除了生成数据结构文件(*_pb2.py),还会生成 gRPC 服务文件*_pb2_grpc.py)。


生成的 gRPC 文件包含:

  1. 客户端存根(Client Stubs): 供客户端调用远程服务的代码。
  2. 服务基类(Service Base Classes): 供服务器端实现 RPC 方法的抽象基类。

您需要继承生成的基类并实现 RPC 方法。

# 导入生成的代码
from my_project.generated import user_service_pb2_grpc as service_grpc
from my_project.generated import user_service_pb2 as service_pb2
from my_project.generated import user_data_pb2 as data_pb2 # UserProfile 消息
class UserServicer(service_grpc.UserServiceServicer):
"""实现 UserService 服务的所有 RPC 方法"""
def GetUser(self, request: service_pb2.GetUserRequest, context):
print(f"Received request for user_id: {request.user_id}")
# 1. 执行业务逻辑 (例如:查询数据库)
# ...
# 2. 构造 Protobuf 响应消息
profile = data_pb2.UserProfile(
user_id=request.user_id,
username="gRPC User",
created_at=1704067200
)
# 3. 返回响应
return service_pb2.GetUserResponse(profile=profile)
# 启动 gRPC 服务器的代码 (通常在 main.py 中)
import grpc
from concurrent import futures
def serve():
# 创建 gRPC 服务器
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 将服务实现注册到服务器
service_grpc.add_UserServiceServicer_to_server(UserServicer(), server)
# 绑定端口并启动
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
# if __name__ == '__main__':
# serve()

客户端使用存根连接服务器并调用方法。

# 导入生成的代码
from my_project.generated import user_service_pb2_grpc as service_grpc
from my_project.generated import user_service_pb2 as service_pb2
def run_client():
# 创建一个连接通道 (Channel)
with grpc.insecure_channel('localhost:50051') as channel:
# 创建一个客户端存根 (Stub)
stub = service_grpc.UserServiceStub(channel)
# 构造请求消息
request = service_pb2.GetUserRequest(user_id='u-456')
# 调用 RPC 方法
try:
response = stub.GetUser(request)
# 直接使用返回的 Protobuf 消息
print(f"Response received:")
print(f" User ID: {response.profile.user_id}")
print(f" Username: {response.profile.username}")
except grpc.RpcError as e:
print(f"An error occurred: {e.details()}")
# if __name__ == '__main__':
# run_client()

如上所示,gRPC 生成的代码完美支持 Python 类型提示。在您的服务器端实现中,明确指定 requestcontext 的类型,以及方法的返回值类型,这极大地提高了代码的 可读性健壮性

在 gRPC 中,错误应该通过抛出 grpc.RpcError 并设置 context.set_code()context.set_details() 来处理,而不是抛出常规的 Python 异常。

from grpc import StatusCode
class UserServicer(...):
def GetUser(self, request, context):
if request.user_id == "invalid":
# 优雅地设置 gRPC 状态码和详细信息
context.set_code(StatusCode.NOT_FOUND)
context.set_details("User with provided ID was not found.")
return service_pb2.GetUserResponse() # 返回一个空的响应
# ... 正常逻辑

如果您的项目还需要支持 浏览器端的 JavaScript/TypeScript 调用,可以结合使用 gRPC-Web。它允许 Web 浏览器使用 Protobuf 和服务定义来调用 gRPC 服务,扩展了 gRPC 的应用范围。