4. gRPC 服务开发与部署:定义 Protobuf 文件、生成 Go 代码、实现 gRPC Server 与 Client
本章是微服务通信的核心环节。在微服务架构中,服务间调用需要高效、跨语言且具备强契约约束。gRPC 结合 Protocol Buffers(Protobuf)正是满足这一需求的工业级方案。我们将从定义接口契约开始,逐步完成代码生成、服务端实现与客户端调用,最终形成一个可独立部署的 gRPC 微服务单元。
4.1 整体工作流概览
在动手编码前,先理解从定义到部署的完整链路。下图展示了本章涉及的核心步骤与产物关系:
4.2 定义 Protobuf 文件(.proto)
Protobuf 文件是 gRPC 服务的“接口定义语言”(IDL)。它同时定义了服务端点(Service)和数据结构(Message)。我们以一个用户信息服务为例:
项目结构约定: 建议将 .proto 文件统一放在 api/proto/ 目录下,与业务代码分离。
// api/proto/user.proto
syntax = "proto3";
package userpb;
option go_package = "github.com/yourorg/user-service/api/pb/userpb";
// 用户服务定义
service UserService {
// 获取用户信息
rpc GetUser (GetUserRequest) returns (GetUserResponse);
// 批量获取用户信息(服务端流式)
rpc ListUsers (ListUsersRequest) returns (stream User);
}
// 请求消息
message GetUserRequest {
int64 user_id = 1;
}
message ListUsersRequest {
repeated int64 user_ids = 1;
}
// 响应消息
message User {
int64 user_id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message GetUserResponse {
User user = 1;
}
| 元素 | 说明 | 最佳实践 |
|---|---|---|
syntax = "proto3" |
指定使用 proto3 语法 | 始终使用 proto3,支持更多语言和特性 |
package |
Protobuf 命名空间,防止冲突 | 与目录结构保持一致,如 userpb |
option go_package |
指定生成的 Go 包导入路径 | 使用完整 module 路径,避免导入混乱 |
service |
定义 RPC 方法集合 | 每个服务一个文件,方法名使用动词开头 |
message |
定义结构化数据 | 字段编号 1-15 用 1 字节编码,高频字段优先 |
4.3 使用 protoc 生成 Go 代码
定义好 .proto 文件后,需要通过 protoc 编译器生成 Go 代码。这需要安装以下工具:
- protoc:Protocol Buffers 编译器
- protoc-gen-go:生成 Go 结构体代码的插件
- protoc-gen-go-grpc:生成 gRPC 服务端/客户端代码的插件
安装命令(macOS/Linux):
# 安装 protoc
brew install protobuf
# 安装 Go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
执行代码生成命令:
protoc --proto_path=api/proto \
--go_out=api/pb \
--go_opt=paths=source_relative \
--go-grpc_out=api/pb \
--go-grpc_opt=paths=source_relative \
api/proto/user.proto
生成后的目录结构:
api/pb/
└── userpb/
├── user.pb.go # Message 结构体 + 序列化方法
└── user_grpc.pb.go # 服务端接口 + 客户端 Stub
| 生成文件 | 内容 | 开发者操作 |
|---|---|---|
user.pb.go |
User、GetUserRequest 等结构体定义、Marshal/Unmarshal 方法 | 直接使用,无需修改 |
user_grpc.pb.go |
UserServiceServer 接口、UserServiceClient 接口、注册函数 |
Server 端实现接口;Client 端调用 Stub |
4.4 实现 gRPC Server
生成的 user_grpc.pb.go 中定义了 UserServiceServer 接口。我们需要创建一个结构体实现该接口的所有方法,然后注册到 gRPC 服务器。
// internal/server/user_server.go
package server
import (
"context"
"log"
"github.com/yourorg/user-service/api/pb/userpb"
)
// UserServer 实现 userpb.UserServiceServer 接口
type UserServer struct {
userpb.UnimplementedUserServiceServer // 向前兼容
}
// GetUser 获取单个用户信息
func (s *UserServer) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*userpb.GetUserResponse, error) {
// 模拟从数据库查询
user := &userpb.User{
UserId: req.UserId,
Name: "Alice",
Email: "alice@example.com",
Age: 30,
}
log.Printf("GetUser called: user_id=%d", req.UserId)
return &userpb.GetUserResponse{User: user}, nil
}
// ListUsers 流式返回用户列表
func (s *UserServer) ListUsers(req *userpb.ListUsersRequest, stream userpb.UserService_ListUsersServer) error {
for _, uid := range req.UserIds {
user := &userpb.User{
UserId: uid,
Name: "User_" + fmt.Sprint(uid),
}
if err := stream.Send(user); err != nil {
return err
}
}
return nil
}
启动 gRPC 服务器:
// cmd/server/main.go
package main
import (
"log"
"net"
"google.golang.org/grpc"
"github.com/yourorg/user-service/api/pb/userpb"
"github.com/yourorg/user-service/internal/server"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
userpb.RegisterUserServiceServer(s, &server.UserServer{})
log.Println("gRPC server listening on :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
生产环境注意:
- 务必配置
grpc.KeepaliveParams和grpc.KeepaliveEnforcementPolicy防止连接泄漏。 - 使用
grpc.UnaryInterceptor或grpc.StreamInterceptor注入日志、监控、认证中间件。 - 服务端应支持
graceful shutdown,捕获 SIGTERM 信号后调用s.GracefulStop()。
4.5 实现 gRPC Client
客户端通过生成的 UserServiceClient 接口发起调用。gRPC 客户端支持连接复用和自动重连。
// cmd/client/main.go
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/yourorg/user-service/api/pb/userpb"
)
func main() {
// 建立连接(生产环境应使用安全连接)
conn, err := grpc.Dial("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(10*1024*1024)),
)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := userpb.NewUserServiceClient(conn)
// 一元调用
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &userpb.GetUserRequest{UserId: 1001})
if err != nil {
log.Fatalf("GetUser failed: %v", err)
}
log.Printf("GetUser response: %+v", resp.User)
// 服务端流式调用
streamCtx, streamCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer streamCancel()
stream, err := client.ListUsers(streamCtx, &userpb.ListUsersRequest{
UserIds: []int64{1, 2, 3},
})
if err != nil {
log.Fatalf("ListUsers failed: %v", err)
}
for {
user, err := stream.Recv()
if err != nil {
break
}
log.Printf("Received user: %+v", user)
}
}
4.6 服务端与客户端交互流程
下图展示了 gRPC 调用过程中,客户端、服务端与 Protobuf 序列化层的协作关系:
4.7 部署与验证清单
将 gRPC 服务部署到生产环境前,请逐项检查以下清单:
| 检查项 | 要求 | 验证方式 |
|---|---|---|
| 端口监听 | 仅监听内网 IP 或使用 TLS | netstat -tlnp | grep 50051 |
| 健康检查 | 实现 grpc.health.v1.Health 标准服务 |
grpc_health_probe 工具探测 |
| 中间件 | 日志、恢复、限流、认证拦截器 | 压测时观察拦截器日志 |
| 优雅关闭 | 捕获 SIGTERM 后 GracefulStop() |
发送 SIGTERM,观察已有请求是否完成 |
| 客户端重试 | 配置重试策略(指数退避) | 模拟服务端故障,观察客户端重试日志 |
| 监控指标 | 暴露 gRPC 请求数、延迟、错误率 | Prometheus 指标端点 /metrics |
快速验证命令:
# 使用 grpcurl 工具测试服务端(无需编写客户端代码)
grpcurl -plaintext -d '{"user_id": 1001}' localhost:50051 userpb.UserService/GetUser
4.8 本章小结
通过本章实践,我们完成了 gRPC 微服务的完整开发闭环:
- 契约先行:.proto 文件作为服务间通信的唯一标准,避免接口不一致问题。
- 代码生成:protoc 工具链自动生成类型安全的 Go 代码,减少手写样板代码。
- 服务端实现:通过实现生成的接口,快速接入业务逻辑。
- 客户端调用:利用生成的 Stub,以本地调用方式发起远程请求。
下一阶段,我们将为这个 gRPC 服务添加压测能力,验证其在高并发场景下的性能表现,并建立持续的性能回归闭环。