Files
cs-note/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/03-Service Registration 服务注册详解.md
T
2026-05-24 11:42:38 +08:00

279 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [gRPC, Go, Server, Service]
create time: 2026-05-13 23:25
---
# Service Registration — 服务注册详解
## 概述
Service Registration 是把你的业务实现"告诉"gRPC server 的过程。你调用一次 `RegisterXxxServer()`,proto 编译器生成的元数据(`ServiceDesc`)就会被写入 server 内部的 dispatch table——此后每个到达的请求都能被正确路由到你的 handler。
本文聚焦三个核心问题:
1. Proto 到底生成了什么?ServiceDesc 在其中扮演什么角色?
2. 怎么把多个 service 注册到一个 server?底层路由怎么走?
3. 注册函数内部原理是什么?为什么签名是 `ServiceRegistrar` 而不是 `*grpc.Server`?
> [!tip] 前置知识
> 阅读本文建议先了解 [[hhs/gRPC/2. gRPC 核心篇/06-Service 定义与代码生成]](proto 生成机制)和 [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式]](Unimplemented 基类的作用)。
## Proto 生成的三类关键符号
看一段最普通的 proto 定义:
```proto
service UserService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
```
运行 protoc 后,与你接下来要写的**业务代码**直接相关的有三类符号:
| 生成物 | 类型 | 你在业务代码中的用法 |
|--------|------|---------------------|
| `UserServiceServer` | interface | **嵌入 Unimplemented + 实现方法**来创建实现体 |
| `UnimplementedUserServiceServer` | struct | 嵌入到你自己的 struct 中获取默认行为 |
| `RegisterUserServiceServer()` | 函数 | 在 server 启动时调用,完成注册 |
| `UserService_ServiceDesc` | `grpc.ServiceDesc` | 自动注入到 Register 函数中,**你不需要直接操作它** |
### 生成的 Interface —— 封闭接口技巧
```go
type UserServiceServer interface {
CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error)
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
mustEmbedUnimplementedUserServiceServer() // ← 未导出
}
```
`mustEmbedUnimplementedUserServiceServer()` 是**未导出方法**(首字母小写),这就是 Go 社区经典的"封闭接口"技巧——其他任何想满足此接口的结构体必须显式嵌入 `UnimplementedUserServiceServer`,否则编译失败。
> [!question] 为什么这么设计?
> Proto 文件随时可能新增 RPC 方法。如果没有这个强制嵌入规则,开发者可能在升级 proto 后忘记补上新方法,导致线上服务静默返回错误。封闭接口把"漏实现"变成编译错误,把风险挡在开发阶段。
详细的零-Stub 模式机制参见 [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式]]。
### ServiceDesc —— 注册的真正核心
`ServiceDesc`(Service Descriptor)是所有注册逻辑的**元数据枢纽**。它描述了 service 的名称、所有方法的签名、处理函数等完整信息。Register 函数的本质就是把 ServiceDesc 塞进 server 的 internal map:
```go
var UserService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "user.v1.UserService", // 全限定名,用于路由匹配
HandlerType: (*UserServiceServer)(nil), // 接口类型(仅用于类型检查)
Methods: []grpc.MethodDesc{ // Unary / Streaming 方法列表
{
MethodName: "CreateUser",
Handler: _UserCreateUser_Handler, // 实际的分发函数
},
{
MethodName: "GetUser",
Handler: _UserGetUser_Handler,
},
},
Streams: []grpc.StreamDesc{}, // 流式方法(本例没有)
Metadata: "user/v1/user.proto",
}
```
关键点:
- **`ServiceName`** 决定了请求的路由前缀——客户端调用的完整方法名就是 `user.v1.UserService/CreateUser`
- **`Methods`** 里列出了所有注册的服务方法,每个方法对应一个 `_XXX_Handler` 分发函数
- **`HandlerType`** 是个 nil 指针,纯粹用于编译期类型检查——确保传给 Register 的对象确实实现了该 interface
## 注册函数的内部原理
很多人以为 `RegisterXxxServer(s, srv)` 只是简单地把对象存起来。实际上它做了两件事:
```go
// 简化后的源码逻辑
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {
s.RegisterService(&UserService_ServiceDesc, srv)
}
```
一行代码——把 ServiceDesc 和你的实现一起交给 server。但真正的逻辑在 `s.RegisterService()` 里面:
1. **校验**:检查 method name 是否已被注册过(重复注册会导致 panic)
2. **封装**:用你的 `srv` 实例包装 `_XXX_Handler` 分发函数,绑定上下文
3. **存储**:写入 server 内部的 `map[string]*method` dispatch table
### 为什么签名是 ServiceRegistrar 而不是 *grpc.Server?
仔细看生成代码的函数签名:
```go
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer)
```
参数类型是 `grpc.ServiceRegistrar`(一个 interface),不是 `*grpc.Server`(一个 concrete type):
```go
type ServiceRegistrar interface {
RegisterService(desc *ServiceDesc, svc any)
}
```
这看似多余,实际上有两个好处:
- **可测试性**:单元测试中可以 mock `ServiceRegistrar`,无需启动真实的 server 进程
- **扩展性**:任何实现了 `RegisterService` 的结构体都可以接受注册——比如自定义 wrapper、多端口路由器等未来场景
> [!important] 理解层次
> `*grpc.Server` 实现了 `ServiceRegistrar` + 更多方法(如 `Serve`, `Stop`, `GracefulStop`)。注册只需要 "注册服务的能力",不关心 "如何监听网络"——这是典型的单一职责设计。
## 服务端实现的标准模式
结合 proto 生成的符号,标准的业务实现模板如下:
```go
// userService 是你的业务实现
type userService struct {
pb.UnimplementedUserServiceServer // ① 嵌入基类 → 零值安全
userStore UserStore // ② 依赖注入 → 数据库、缓存等
logger *zap.Logger // ③ 基础设施
}
// NewUserService 构造器:集中管理依赖
func NewUserService(store UserStore, logger *zap.Logger) *userService {
return &userService{
userStore: store,
logger: logger,
}
}
// ④ 只实现你需要暴露的方法——其余由 Unimplemented 提供默认拒绝响应
func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
user := &ModelUser{
Name: req.GetName(),
...
}
id, err := s.userStore.Create(ctx, user)
if err != nil {
return nil, status.Errorf(codes.Internal, "create user failed: %v", err)
}
return &pb.CreateUserResponse{Id: id}, nil
}
```
四个要点:
| 步骤 | 目的 | 说明 |
|------|------|------|
| ① 嵌入 Unimplemented | 防止 proto 升级后漏实现新方法 | 详见 [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式]] |
| ② 注入依赖 | 解耦业务逻辑与基础设施 | 通过构造器传入,而非全局变量 |
| ③ 注入日志 | 统一追踪体系 | 使用 zap 或 slog 均可 |
| ④ 选择性实现 | 灵活控制暴露面 | 只能整体注册 service,不能逐个方法注册 |
> [!warning] 常见反模式
> - ❌ 在 struct 中直接用全局变量拿 DB 连接 → 不可测试、线程不安全
> - ❌ 手动实现所有方法而不嵌入 Unimplemented → proto 升级时漏掉新方法
> - ❌ 在一个 service 里塞过多职责 → 考虑拆成独立的 proto service
## 单 Server 多 Service
单个 gRPC server 天然支持多个 service registration,互不影响:
```go
s := grpc.NewServer(opts...)
// 全部挂到同一个 server
pb.RegisterUserServiceServer(s, NewUserService(store))
pb.RegisterOrderServiceServer(s, NewOrderService(store))
pb.RegisterPaymentServiceServer(s, NewPaymentService(store))
s.Serve(lis) // 一个端口对外暴露三个 service
```
每个 service 完全独立:有自己的方法名空间、自己的 handler、自己的错误处理。它们共享同一个 listener 和连接池。
### 路由链路——从请求到你的 handler
一个完整的 RPC 请求到达 server 后的流转路径:
```mermaid
sequenceDiagram
participant C as Client
participant H as HTTP/2 Layer
participant D as Dispatch Table
participant M as Method Handler
participant B as Your Business Logic
C->>H: gRPC request<br/>"/user.v1.UserService/CreateUser"
H->>D: 提取 method path 作为 key
D->>D: 查找 dispatch table
alt 找到匹配 entry
D->>M: _UserCreateUser_Handler(req)
M->>B: 调用 svc.(*userService).CreateUser(ctx, req)
B->>B: 执行业务逻辑 (DB / 缓存 / ...)
B-->>M: response + error
M-->>H: protobuf-encoded response
H-->>C: gRPC response
else 未找到匹配
D-->>H: UNIMPLEMENTED status
H-->>C: error response
end
```
路由 key 是完整的方法名 `/package.ServiceName/MethodName`,来源是 HTTP/2 的 header。server 内部维护了一个 `map[string]*MethodInfo` 结构的 dispatch table,key 就是 method name,value 就是你注册时绑定好的 handler。
### 什么时候需要多 Server?
虽然可以一个 server 挂所有 service,但有些场景你会**有意分开**:
| 场景 | 做法 | 原因 |
|------|------|------|
| 微服务拆分 | 每个服务独立进程 | 独立部署、独立扩缩容 |
| 内外网隔离 | 内网 server 只挂核心 service | 安全隔离,网关聚合 |
| 不同协议 | HTTP + gRPC | 不同 listener |
见 [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/05-Multi-Port 多端口暴露]]。
## Service 的全生命周期
从 proto 定义到线上运行的完整链路:
```mermaid
flowchart TD
A["定义 .proto<br/>service Xxx { ... }"] --> B["protoc 生成代码<br/>_grpc.pb.go"]
B --> C["实现 XxxServer interface<br/>嵌入 Unimplemented"]
C --> D["构造器注入依赖<br/>NewXxxService(...)"]
D --> E["RegisterXxxServer(s, impl)"]
E --> F["s.Serve(lis)<br/>阻塞等待请求"]
F --> G["Client RPC 请求到达"]
G --> H["dispatch table 路由"]
H --> I["执行你的业务逻辑"]
style A fill:#E3F2FD
style B fill:#FFF4CC
style E fill:#A8E6CF
style I fill:#FFD43B,color:#000
```
## 常见问题
> [!question] 我可以只注册部分方法吗?
> 不行。注册的是整个 service(整个 interface),不是单个方法。如果你不想暴露某个方法,要么从 proto 里拆出去单独一个 service,要么在方法内部返回 permission denied。
> [!question] 同一个 service 能注册多次吗?
> 不能。对同一个 method name 调用两次 `RegisterXxxServer()` 会导致 server 启动时 panic。如果你在测试代码中反复创建了 server 实例,记得每次用 `grpc.NewServer()` 创建新的——不要复用旧实例。
> [!warning] ServiceName 必须唯一
> 如果你的 proto 文件中有两个 service 起了相同的 `ServiceName`(全限定名),第二个注册时会 panic。命名时养成习惯:始终带上 package 前缀(如 `user.v1.UserService`)。
> [!tip] 注册顺序不重要
> 无论你先注册 UserService 还是 OrderService,server 都会正确路由。因为 dispatch table 是按 method name 查找的,与注册先后无关。
> [!note] 生产环境:反射开关
> 开发时可以开启 reflection([[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/04-Reflection 开发调试利器]])方便用 grpcurl 调试,生产环境务必关闭——它会暴露完整的 API 结构给外部。
## 关联笔记
- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式]]
- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server]]
- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/04-Reflection 开发调试利器]]
- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/05-Multi-Port 多端口暴露]]
- [[hhs/gRPC/2. gRPC 核心篇/06-Service 定义与代码生成]]