4. 一元RPC:概念、定义与实现
一元RPC(Unary RPC)是gRPC中最基础、最常用的通信模式。它遵循经典的请求-响应模型:客户端发送一个请求消息,服务端处理并返回一个响应消息。这种模式与传统的HTTP REST接口在语义上最为接近,但底层基于HTTP/2和Protocol Buffers,具有更高的性能和更严格的类型约束。
核心特征: 一元RPC是“一问一答”式的同步通信。客户端发起一次调用,必须等待服务端返回响应后才能继续执行后续逻辑。它天然适合查询、提交、验证等操作场景。
4.1 一元RPC概念与适用场景
在微服务架构中,一元RPC通常用于以下场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 数据查询 | 客户端请求单个资源或聚合数据 | 根据用户ID查询用户详情 |
| 命令执行 | 客户端触发一个确定性的操作 | 创建订单、更新库存 |
| 身份验证 | 提交凭证并获取令牌 | 登录接口 |
| 简单计算 | 输入参数,返回计算结果 | 汇率转换、价格计算 |
一元RPC的通信流程可以概括为以下步骤:
- 客户端构建请求:将业务参数填充到Protobuf定义的消息结构中。
- 序列化与传输:gRPC框架将请求消息序列化为二进制数据,通过HTTP/2发送。
- 服务端反序列化:服务端接收数据,还原为请求消息对象。
- 业务处理:服务端执行具体的业务逻辑。
- 响应构建与返回:服务端将结果封装为响应消息,序列化后返回。
- 客户端反序列化:客户端接收响应,还原为响应消息对象。
4.2 Proto定义一元方法
在gRPC中,所有服务接口和消息结构都通过Protocol Buffers(protobuf)文件定义。一元RPC的方法定义遵循以下语法:
// 定义服务
service UserService {
// 一元RPC方法:请求-响应模式
rpc GetUser (GetUserRequest) returns (GetUserResponse);
}
// 请求消息
message GetUserRequest {
int64 user_id = 1;
}
// 响应消息
message GetUserResponse {
int64 user_id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
关键语法规则:
- service:关键字,用于定义一组RPC方法。
- rpc:关键字,声明一个RPC方法。
- 方法名:遵循驼峰命名法,首字母大写。
- 参数:必须是一个protobuf消息类型,不能是基本类型。
- 返回值:必须是一个protobuf消息类型,不能是基本类型。
- 消息字段:每个字段有唯一的编号(tag),用于二进制编码。
最佳实践: 建议为每个RPC方法定义独立的请求和响应消息,即使某些字段相同。这有利于接口演进和向后兼容。
4.3 服务端实现
服务端实现的核心是创建一个结构体,实现protobuf生成的接口。以下是完整的服务端代码示例:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "path/to/your/protobuf/package"
)
// 定义服务结构体
type userServiceServer struct {
pb.UnimplementedUserServiceServer
}
// 实现 GetUser 方法
func (s *userServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// 模拟从数据库查询用户
user := &pb.GetUserResponse{
UserId: req.GetUserId(),
Name: "张三",
Email: "zhangsan@example.com",
Age: 28,
}
log.Printf("收到请求: userId=%d, 返回用户: %s", req.GetUserId(), user.GetName())
return user, nil
}
func main() {
// 创建TCP监听
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
// 创建gRPC服务器
s := grpc.NewServer()
// 注册服务
pb.RegisterUserServiceServer(s, &userServiceServer{})
log.Println("服务端启动,监听端口 :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
服务端实现要点:
| 步骤 | 说明 | 代码位置 |
|---|---|---|
| 1. 定义结构体 | 创建空结构体,嵌入 UnimplementedUserServiceServer |
type userServiceServer struct |
| 2. 实现接口方法 | 方法签名必须与生成的接口完全一致 | func (s *userServiceServer) GetUser(...) |
| 3. 处理上下文 | 通过 ctx 处理超时、取消等控制 |
方法参数 ctx context.Context |
| 4. 注册服务 | 将实现注册到gRPC服务器 | pb.RegisterUserServiceServer(s, &userServiceServer{}) |
| 5. 启动服务 | 调用 s.Serve(lis) 开始监听 |
s.Serve(lis) |
注意: 嵌入
UnimplementedUserServiceServer 是gRPC的推荐做法。即使未来接口新增方法,你的服务也能保持编译通过,避免强制实现所有方法。
4.4 客户端实现
客户端实现相对简单,核心是建立连接、创建存根(stub)、发起调用。以下是完整的客户端代码示例:
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "path/to/your/protobuf/package"
)
func main() {
// 建立连接
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
// 创建客户端存根
client := pb.NewUserServiceClient(conn)
// 设置超时上下文
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 构建请求
req := &pb.GetUserRequest{
UserId: 1001,
}
// 发起一元RPC调用
resp, err := client.GetUser(ctx, req)
if err != nil {
log.Fatalf("调用失败: %v", err)
}
// 使用响应
log.Printf("获取用户成功: ID=%d, 姓名=%s, 邮箱=%s, 年龄=%d",
resp.GetUserId(), resp.GetName(), resp.GetEmail(), resp.GetAge())
}
客户端实现要点:
- 连接管理:使用
grpc.Dial建立连接,生产环境建议使用连接池或长连接复用。 - 安全传输:开发环境使用
insecure.NewCredentials(),生产环境必须配置TLS。 - 超时控制:始终使用带超时的上下文,避免客户端无限等待。
- 资源释放:使用
defer conn.Close()确保连接正确关闭。 - 错误处理:检查RPC调用的错误返回值,处理网络异常、服务端错误等情况。
4.5 一元RPC的优缺点
| 维度 | 优点 | 缺点 |
|---|---|---|
| 易用性 | 编程模型简单,与函数调用类似 | 不适合流式数据场景 |
| 性能 | 二进制序列化,传输效率高 | 大数据量时内存占用高 |
| 错误处理 | 标准的错误码机制,易于排查 | 无法部分成功/部分失败 |
| 兼容性 | 接口变更友好,支持向后兼容 | 请求响应必须完整传输 |