This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/03-Service Registration 服务注册详解.md
T
2026-05-13 23:43:37 +08:00

12 KiB
Raw Blame History

tags, create time
tags create time
gRPC
Go
Server
Service
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 定义:

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 —— 封闭接口技巧

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:

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) 只是简单地把对象存起来。实际上它做了两件事:

// 简化后的源码逻辑
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?

仔细看生成代码的函数签名:

func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer)

参数类型是 grpc.ServiceRegistrar(一个 interface),不是 *grpc.Server(一个 concrete type):

type ServiceRegistrar interface {
    RegisterService(desc *ServiceDesc, svc any)
}

这看似多余,实际上有两个好处:

  • 可测试性:单元测试中可以 mock ServiceRegistrar,无需启动真实的 server 进程
  • 扩展性:任何实现了 RegisterService 的结构体都可以接受注册——比如自定义 wrapper、多端口路由器等未来场景

[!important] 理解层次 *grpc.Server 实现了 ServiceRegistrar + 更多方法(如 Serve, Stop, GracefulStop)。注册只需要 "注册服务的能力",不关心 "如何监听网络"——这是典型的单一职责设计。

服务端实现的标准模式

结合 proto 生成的符号,标准的业务实现模板如下:

// 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,互不影响:

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 后的流转路径:

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 定义到线上运行的完整链路:

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 定义与代码生成