From 1ef38fe7e6c7fb31f2df3e7ac4c5fe1cc3e3e590 Mon Sep 17 00:00:00 2001
From: hhs <386998068@qq.com>
Date: Wed, 13 May 2026 23:43:37 +0800
Subject: [PATCH] vault backup: 2026-05-13 23:43:37
---
.../3. 服务端实现/08-Server 搭建与注册.md | 430 +-----------------
.../00-Unimplemented 零-Stub 模式.md | 232 ++++++++++
.../01-Hello World 最小可运行 Server.md | 214 +++++++++
.../02-ServerOption 生产级配置速查.md | 360 +++++++++++++++
.../03-Service Registration 服务注册详解.md | 278 +++++++++++
.../04-Reflection 开发调试利器.md | 196 ++++++++
.../05-Multi-Port 多端口暴露.md | 154 +++++++
.../06-Graceful Shutdown 优雅关闭.md | 251 ++++++++++
8 files changed, 1710 insertions(+), 405 deletions(-)
create mode 100644 hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md
create mode 100644 hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server.md
create mode 100644 hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/02-ServerOption 生产级配置速查.md
create mode 100644 hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/03-Service Registration 服务注册详解.md
create mode 100644 hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/04-Reflection 开发调试利器.md
create mode 100644 hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/05-Multi-Port 多端口暴露.md
create mode 100644 hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/06-Graceful Shutdown 优雅关闭.md
diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册.md
index 60969ee..4e58725 100644
--- a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册.md
+++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册.md
@@ -1,5 +1,5 @@
---
-tags: [gRPC, Go, Server, Production]
+tags: [gRPC, Go, Server, Index]
create time: 2026-05-11 16:00
---
@@ -7,420 +7,40 @@ create time: 2026-05-11 16:00
## 概述
-gRPC server 搭建本身很简单——`grpc.NewServer()` 加一行 `RegisterxxxServer()` 就够了。但在生产环境中你需要考虑的东西很多:TLS 配置、优雅关闭、服务发现集成、多端口暴露、reflection 开关、健康检查。我们从一个最简单的 hello world 开始,逐步构建一个 production-ready 的 server。
+这一组笔记从"如何跑起来"出发,逐步叠加生产环境需要的各种配置——直到你能直接复制一个完整的 server bootstrap 投入生产。按顺序阅读,不要跳。
-> [!question] 为什么生产环境需要关心 graceful shutdown?
-> gRPC 基于 HTTP/2,连接是长连接。如果直接 kill 进程,所有正在处理的请求会突然断掉,client 端收到的是 TCP RST 而非一个干净的 finish。这在金融或订单系统中可能导致重复扣款、状态不一致。
+## 学习笔记(按推荐顺序)
-## 最小可运行 Server
+### 入门
-```go
-// cmd/server/main.go
-package main
+| # | 笔记 | 读它如果... |
+|---|------|------------|
+| [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server]] | **第一课**:三段式模板,从零搭起一个能跑的 server | 你是 gRPC 新手,想先看到"能跑的代码" |
-import (
- "log"
- "net"
+### 核心概念
- "go.uber.org/zap"
- "google.golang.org/grpc"
- "google.golang.org/grpc/reflection"
+| # | 笔记 | 读它如果... |
+|---|------|------------|
+| [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/02-ServerOption 生产级配置速查]] | TLS、消息大小、keepalive、拦截器——高频选项逐一讲解 | 想知道每个参数到底该设多大、为什么这么设 |
+| [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/03-Service Registration 服务注册详解]] | Proto 生成了什么、单 server 多 service、底层路由机制 | 想理解 `RegisterXxxServer()` 背后发生了什么 |
+| [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/04-Reflection 开发调试利器]] | 用 grpcurl 无需 proto 就能查 API,生产环境为什么关掉 | 想知道开发时怎么快速调试 |
+| [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/05-Multi-Port 多端口暴露]] | 一个端口 vs 多个端口的正确写法,内外网分离模式 | 需要内网外网分开暴露,或同时监听 IPv4 + IPv6 |
- pb "your/proto/gen/go"
-)
+### 进阶
-func main() {
- logged, _ := zap.NewProduction()
- zap.ReplaceGlobals(logged)
- defer logged.Sync()
+| # | 笔记 | 读它如果... |
+|---|------|------------|
+| [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/06-Graceful Shutdown 优雅关闭]] | Signal 处理、超时兜底、ctx 驱动模式、常见坑 | 要上线了,不想因为 kill 进程导致数据不一致 |
- lis, err := net.Listen("tcp", ":50051")
- if err != nil {
- log.Fatalf("failed to listen: %v", err)
- }
+### 深入专题
- s := grpc.NewServer()
- pb.RegisterUserServiceServer(s, &userService{})
-
- // Reflection for debugging — production 应关闭
- reflection.Register(s)
-
- zap.L().Sugar().Infow("serving", "addr", lis.Addr())
-
- if err := s.Serve(lis); err != nil {
- log.Fatalf("failed to serve: %v", err)
- }
-}
-
-// userService 实现 pb.UserServiceServer interface
-type userService struct {
- pb.UnimplementedUserServiceServer // 嵌入 zero-stub,天然支持未来 method 新增
-}
-```
-
-核心就三步:监听端口 → 创建 server → 启动。**但** `UnimplementedUserServiceServer` 这个嵌入类型很重要——它让你无需为每个 method 写空壳,proto 文件新增 RPC method 时编译期自动报错提醒,而不是静默忽略。
-
-## Server Options(ServerOption 精选)
-
-`grpc.NewServer(...)` 接受任意数量的 `ServerOption`(函数式选项模式)。以下是高频选项速查表:
-
-| Option | 用途 | 默认值 | 建议 |
-|--------|------|--------|------|
-| `grpc.Creds(credentials)` | TLS / mTLS 认证 | 明文 | **生产必配** |
-| `grpc.MaxRecvMsgSize(n int)` | 单条接收消息上限 | 4MB | 按需调大,注意内存 |
-| `grpc.MaxSendMsgSize(n int)` | 单条发送消息上限 | 4MB | 配合 MaxRecv 对称设置 |
-| `grpc.MaxConcurrentStreams(n uint32)` | 单 stream 最大并发 HEADERS | 100 | 防 DoS,放宽到 1024+ |
-| `grpc.KeepaliveParams(kp)` | keepalive 心跳参数 | 2h idle timeout | LB 后必须调整 |
-| `grpc.ChainUnaryInterceptor(ints...)` | Unary 拦截器链 | 无 | 日志 / 鉴权 / 追踪 |
-| `grpc.StatsHandler(h stats.Handler)` | OpenTelemetry 等 stats 注入 | 无 | 链路追踪 / metrics |
-
-```go
-s := grpc.NewServer(
- grpc.Creds(credentials.NewTLS(tlsConfig)),
- grpc.MaxRecvMsgSize(16*1024*1024), // 16MB
- grpc.MaxConcurrentStreams(1024), // 放宽并发流限制
- grpc.KeepaliveParams(keepalive.ServerParameters{
- MaxConnectionIdle: 15 * time.Minute,
- KeepaliveTime: 20 * time.Second,
- KeepaliveTimeout: 5 * time.Second,
- MinTimeBetweenPings: 10 * time.Second,
- PingWithoutCallsAllowed: true,
- }),
- grpc.ChainUnaryInterceptor(logging.Unary(), auth.Unary()),
- // grpc.StatsHandler(otelgrpc.NewServerHandler()), // OpenTelemetry Go SDK
-)
-```
-
-> [!tip] keepalive 参数调优经验
-> 经过 LB(如 Envoy 或 Nginx)时,默认的 2 小时 idle timeout 会让 LB 提前切断连接,导致 client 侧出现 "connection reset" 错误。把 `MaxConnectionIdle` 设短到 15~20 分钟更合理——让 server 主动重建连接,确保两端对连接生命周期有一致认知。
->
-> 直连无 LB 场景可以保持默认值,减少不必要的连接重建开销。
-
-### 拦截器链执行顺序
-
-拦截器以**尾递归**方式组合——先注册的 interceptor 包裹后注册的,形成洋葱模型:
-
-```mermaid
-flowchart LR
- Client["Client Request"] --> Logging["Logging Interceptor
(最外层)"]
- Logging --> Auth["Auth Interceptor
(内层)"]
- Auth --> Recovery["Recovery Interceptor
(最内层)"]
- Recovery --> Handler["Actual Handler"]
-
- style Client fill:#E3F2FD
- style Handler fill:#FFF3E0
-```
-
-```go
-// 请求到达顺序:Logging → Auth → Recovery → Handler
-// 响应返回顺序:Handler → Recovery → Auth → Logging
-grpc.ChainUnaryInterceptor(
- logging.UnaryInterceptor(), // 第 1 层(最外)
- auth.UnaryInterceptor(), // 第 2 层
- recovery.UnaryInterceptor(), // 第 3 层(最内)
-)
-```
-
-**设计建议:**
-- **外层**做横切关注点:日志、追踪、metrics
-- **中层**做业务安全校验:鉴权、限流
-- **内层**做兜底逻辑:panic recover、超时控制
-
-## Service Registration
-
-### 什么是 Service Registration
-
-每次 proto 文件中的 `service` 块都会生成两个 Go 类型:
-
-1. **`Server` interface** —— 定义了你必须实现的 RPC method 集合
-2. **`RegisterServer(server, impl)` 函数** —— 将实现注册到 server 的 method dispatch table
-
-```go
-// 你定义的 .proto:
-// service UserService {
-// rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
-// rpc GetUser(GetUserRequest) returns (GetUserResponse);
-// }
-
-// 生成的接口(简化示意):
-type UserServiceServer interface {
- CreateUser(context.Context, *pb.CreateUserRequest) (*pb.CreateUserResponse, error)
- GetUser(context.Context, *pb.GetUserRequest) (*pb.GetUserResponse, error)
- // UnimplementedUserServiceServer 提供 default implementation (Unimplemented)
- mustEmbedUnimplementedUserServiceServer()
-}
-
-// 注册函数签名:
-func RegisterUserServiceServer(s *grpc.Server, srv UserServiceServer)
-```
-
-### 单 server 注册多 service
-
-不需要为每个 service 创建独立 `grpc.Server`。单个 `grpc.Server` 实例天然支持多个 service registration:
-
-```go
-s := grpc.NewServer(opts...)
-
-pb.RegisterUserServiceServer(s, NewUserService())
-pb.RegisterOrderServiceServer(s, NewOrderService())
-pb.RegisterPaymentServiceServer(s, NewPaymentService())
-
-s.Serve(lis) // 一个 listener 对外暴露三个 service
-```
-
-底层通过 method name 路由(例如 `:method/user.v1.UserService/CreateUser`),各 service 完全隔离互不干扰。
-
-### reflection:开发调试 vs 生产关闭
-
-Reflection 允许外部工具在运行时查询已注册的 service descriptor,无需 `.proto` 文件:
-
-```go
-import "google.golang.org/grpc/reflection"
-
-reflection.Register(s) // 注册 gRPC Reflection v1alpha 服务
-```
-
-**开发阶段好处:**
-- `grpcurl` 无需 `.proto` 即可列出可用 API
-- IDE 自动补全 gRPC call
-- 快速验证某个 service 是否成功注册
-
-**生产环境关闭原因:**
-- 暴露了完整的 API schema 信息(方法名、消息结构),增加攻击面
-- 额外占用内存保存所有 descriptor protobuf
-- 客户端可通过 reflection 探测内部方法名(即使是未公开的)
-
-```go
-if os.Getenv("ENV") != "production" {
- reflection.Register(s) // 仅非生产环境开启
-}
-```
-
-## Multi-Port / Multi-Network
-
-常见需求:内网接口和外网接口分开监听,或者同时暴露 IPv4 和 IPv6:
-
-```go
-srv := grpc.NewServer(opts...)
-pb.RegisterUserServiceServer(srv, svc)
-
-lis1, _ := net.Listen("tcp", ":50051") // 内网
-lis2, _ := net.Listen("tcp", ":50052") // 外网
-
-go func() { _ = srv.Serve(lis1) }()
-go func() { _ = srv.Serve(lis2) }()
-
-<-ctx.Done()
-```
-
-> [!warning] 同一个 `grpc.Server` 不能在同一时刻被多个 goroutine 同时调用 `Serve()`(不安全)
-> 上面的写法在 gRPC-Go 中实际会导致 race condition。如果你的业务是按服务拆分到不同端口的需求,应该创建独立的 server 实例:
-
-```go
-// 正确做法:每个端口使用独立的 grpc.Server 实例
-srv1 := grpc.NewServer(opts...)
-pb.RegisterUserServiceServer(srv1, userSvc)
-
-srv2 := grpc.NewServer(opts...)
-pb.RegisterUserServiceServer(srv2, userSvc)
-pb.RegisterOrderServiceServer(srv2, orderSvc) // 外网额外暴露 OrderService
-
-go func() { _ = srv1.Serve(net.Listen("tcp", ":50051")) }() // 内网:只暴露 User
-go func() { _ = srv2.Serve(net.Listen("tcp", ":50052")) }() // 外网:暴露 User + Order
-
-<-ctx.Done()
-srv1.Stop()
-srv2.Stop()
-```
-
-这样每个 server 实例管理自己的 listener 和连接池,彼此隔离互不影响。按服务粒度差异化暴露端口是微服务架构中的常见模式——内网服务只暴露核心 RPC,网关层再聚合多服务。
-
-## Graceful Shutdown(重要!)
-
-这是最容易踩坑的部分。gRPC 提供了两种停止方法:
-
-```go
-// 优雅停止:拒绝新连接 + 等待活跃流完成
-s.GracefulStop()
-
-// 立即停止:直接断开所有连接(未完成请求会报 error)
-s.Stop()
-```
-
-典型的生产模式是结合 signal handling + context 超时控制:
-
-```go
-func run(ctx context.Context, s *grpc.Server) error {
- go func() {
- <-ctx.Done()
- log.Println("shutdown signal received")
- s.GracefulStop()
- }()
-
- return s.Serve(lis)
-}
-
-// 30s 超时: GracefulStop 后最多等 30s,超时则强制退出
-ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
-defer cancel()
-
-if err := run(ctx, s); err != nil {
- return err
-}
-return nil
-```
-
-**为什么需要 30s 超时?** `GracefulStop` 没有内置超时机制——如果某个 handler goroutine 因为缺少 `ctx.Done()` 检测而永远不返回,它会阻塞整个关闭流程。超时兜底确保进程最终能退出(操作系统会重新发送 SIGKILL)。
-
-GracefulStop 的执行流程如下:
-
-```mermaid
-flowchart TD
- SIGTERM["SIGTERM Signal"] --> Handler["Signal Handler"]
- Handler --> StopNew["停止接收新连接
新请求返回 UNAVAILABLE"]
- StopNew --> Drain["排空活跃 Stream
等待 handler 返回"]
- Drain --> Check{"所有流
全部完成?"}
- Check -->|"超时"| Force["强制关闭
可能丢失未完成请求"]
- Check -->|"是"| CleanExit["干净退出
所有回调执行完毕"]
-
- style CleanExit fill:#00D866,color:#fff
- style Force fill:#EE5A24,color:#fff
-```
-
-> [!danger] GracefulStop 不是万能的
-> 如果你的 handler 中没有正确检测 `ctx.Done()`,handler goroutine 永远不会结束,`GracefulStop` 最终也会卡住。所以 streaming handler 里必须遵循 ctx 驱动模式——见下一篇文章。
-
-## 完整生产级模板
-
-汇总成一个可直接复用的 bootstrap 骨架:
-
-```go
-package main
-
-import (
- "context"
- "fmt"
- "log"
- "net"
- "os"
- "os/signal"
- "syscall"
- "time"
-
- "go.uber.org/zap"
- "google.golang.org/grpc"
- grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1"
- "google.golang.org/grpc/health"
- "google.golang.org/grpc/keepalive"
- "google.golang.org/grpc/reflection"
-
- pb "your/proto/gen/go"
-)
-
-func NewGRPCServer(opts ...grpc.ServerOption) (*grpc.Server, error) {
- // 初始化 logger(defer Sync 由调用方管理生命周期)
- logger, _ := zap.NewProduction()
- zap.ReplaceGlobals(logger)
-
- creds, err := loadTLS()
- if err != nil {
- return nil, fmt.Errorf("tls: %w", err)
- }
-
- allOpts := []grpc.ServerOption{
- grpc.Creds(creds),
- grpc.MaxRecvMsgSize(16 * 1024 * 1024),
- grpc.MaxConcurrentStreams(1024),
- grpc.KeepaliveParams(keepalive.ServerParameters{
- MaxConnectionIdle: 15 * time.Minute,
- KeepaliveTime: 20 * time.Second,
- KeepaliveTimeout: 5 * time.Second,
- }),
- grpc.ChainUnaryInterceptor(
- logging.UnaryInterceptor(),
- auth.UnaryInterceptor(),
- ),
- }
- allOpts = append(allOpts, opts...)
-
- srv := grpc.NewServer(allOpts...)
-
- // register services
- pb.RegisterUserServiceServer(srv, NewUserService())
- pb.RegisterOrderServiceServer(srv, NewOrderService())
-
- // health check
- hs := health.NewServer()
- hs.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
- grpc_health_v1.RegisterHealthServer(srv, hs)
-
- // reflection: dev only
- if os.Getenv("ENV") != "production" {
- reflection.Register(srv)
- }
-
- return srv, nil
-}
-
-func main() {
- ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
- defer stop()
-
- srv, err := NewGRPCServer()
- if err != nil {
- log.Fatal(err)
- }
-
- lis, err := net.Listen("tcp", ":50051")
- if err != nil {
- log.Fatal(err)
- }
-
- errCh := make(chan error, 1)
- go func() {
- errCh <- srv.Serve(lis)
- }()
-
- select {
- case err := <-errCh:
- return
- case <-ctx.Done():
- // GracefulShutdown with timeout
- shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
-
- done := make(chan struct{})
- go func() {
- srv.GracefulStop()
- close(done)
- }()
-
- select {
- case <-done:
- log.Println("server stopped gracefully")
- case <-shutdownCtx.Done():
- log.Println("shutdown timeout, forcing stop")
- srv.Stop()
- }
- }
-}
-```
-
-这个模板涵盖了:TLS、大小限制、keepalive、拦截器链、健康检查、reflection 条件开关、signal handling、graceful shutdown + 超时兜底。复制粘贴后即可投入生产使用。
-
-## 关键概念对照表
-
-| 概念 | 对应 API | 一句话总结 |
-|------|---------|-----------|
-| Server 创建 | `grpc.NewServer(...)` | 传入 ServerOption 配置行为 |
-| Service 注册 | `RegisterXxxServer(s, impl)` | 将实现绑定到 server dispatch table |
-| 零-stub 兼容 | 嵌入 `UnimplementedXxxServer` | proto 新增 method 编译期自动告警 |
-| 健康检查 | `health.NewServer()` | Kubernetes probe / SLA 监控对接 |
-| 调试反射 | `reflection.Register(s)` | 仅限开发环境 |
-| 优雅关闭 | `GracefulStop()` + 超时 | 等活跃请求完成,超时无情斩断 |
+| # | 笔记 | 说明 |
+|---|------|------|
+| [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式]] | UnimplementedXXXServer 的设计原理和封闭接口技巧,建议在学完 Hello World 后阅读 | 零-Stub 模式的深度原理解析 |
+| [[hhs/gRPC/3. 服务端实现/09-Streaming Handler]] | Streaming RPC 的 handler 编写范式 | 流式处理的完整指南 |
## 关联笔记
-- [[hhs/gRPC/3. 服务端实现/09-Streaming Handler]]
- [[hhs/gRPC/3. 服务端实现/10-健康检查与反射]]
-- [[hhs/gRPC/1. 基础概念/02-gRPC 核心术语]]
+- [[hhs/gRPC/5. 中间件与拦截器/14-Unary 与 Stream 拦截器]]
+- [[hhs/gRPC/4. 客户端开发/11-Client 连接与 Dial]]
diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md
new file mode 100644
index 0000000..e47e15b
--- /dev/null
+++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md
@@ -0,0 +1,232 @@
+---
+tags: [gRPC, Go, Server, Best Practice]
+create time: 2026-05-13 14:30
+---
+
+# Unimplemented 零-Stub 模式
+
+## 概述
+
+`UnimplementedXxxServer` 是 protoc-gen-go 为每个 gRPC service 自动生成的"空实现"结构体。通过嵌入它到自定义的 Service 结构体中,开发者只需编写实际需要的方法,其余方法自动获得 `codes.Unimplemented` 默认响应——这是一种兼顾开发便利性与接口契约安全的 Go 语言惯用模式。
+
+> [!question] 如果没有这个机制会怎样?
+> 想象你实现了三个 RPC method,后来 proto 新增了一个,编译器不报错——你的新方法永远不会被调用,直到线上某个 client 发出请求后才暴露出 Bug。这就是"静默失败"陷阱。
+
+## Protobuf 生成的代码结构
+
+当你写一个 proto service:
+
+```proto
+service UserService {
+ rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
+ rpc GetUser(GetUserRequest) returns (GetUserResponse);
+}
+```
+
+protoc 会生成三类关键代码:
+
+| 生成物 | 作用 |
+|--------|------|
+| `UserServiceServer` interface | 你必须实现的方法集合 |
+| `UnimplementedUserServiceServer` struct | 所有方法都返回 `Unimplemented` 错误的默认实现 |
+| `RegisterUserServiceServer()` 函数 | 将你的实现注册到 server 分发表 |
+
+重点看 `UserServiceServer` interface 的定义:
+
+```go
+type UserServiceServer interface {
+ CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error)
+ GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
+ mustEmbedUnimplementedUserServiceServer() // ← 封闭性守卫
+}
+```
+
+`mustEmbedUnimplementedUserServiceServer()` 是一个**无参数的空方法**。它的存在让 interface 成为封闭类型——其他任何想满足此接口的类型都必须显式嵌入 `UnimplementedUserServiceServer`,否则编译期直接拦截。这是 Go 社区著名的"封闭接口"(unexported method trick)反模式之一,但在 protobuf 场景下它是有意为之的设计。
+
+## 为什么必须嵌入
+
+### 1. 减少样板代码
+
+嵌入后只需要实现你需要的那些方法:
+
+```go
+type userService struct {
+ pb.UnimplementedUserServiceServer // 自动获得默认 Unimplemented 响应
+}
+
+func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
+ // 只写实际需要的业务逻辑
+ if err := s.store.Create(ctx, req); err != nil {
+ return nil, status.Errorf(codes.Internal, "create user failed: %v", err)
+ }
+ return &pb.CreateUserResponse{Id: generatedId}, nil
+}
+
+// GetUser can be omitted — UnimplementedUserServiceServer returns the default error:
+// status.Error(codes.Unimplemented, "method GetUser not implemented")
+```
+
+### 2. Proto 新增 RPC Method → 编译期告警
+
+这是该模式最大的价值。假设后续你在 proto 里加了一个方法:
+
+```proto
+service UserService {
+ rpc CreateUser(...) returns (...);
+ rpc GetUser(...) returns (...);
+ rpc DeleteUser(...) returns (...); // ← 新增
+}
+```
+
+重新运行 protoc 后,`UserServiceServer` interface 会多出 `DeleteUser` 签名。**此时编译就会失败**:
+
+```
+cannot use &userService{} as UserServiceServer:
+missing method DeleteUser in receiver type userService
+```
+
+从"线上偶发 Bug"变成了"提交前就发现"——把风险前置到了编译期。
+
+### 3. 强制接口契约意识
+
+如果不用 embed,你可以"选择性实现"——只有某些方法、其他全忽略。这会导致服务方和客户端对接口理解不一致。Zero-stub 模式强迫每次新增 method 时都做一次有意识的决策:**实现它**,还是保持 Unimplemented?
+
+## 与反射型语言的对比
+
+在 Java 等语言中,gRPC 通常要求你继承一个基类(如 `UserServiceImplBase`),效果类似:
+
+```java
+public class UserServiceImpl extends UserServiceImplBase {
+ @Override
+ public void createUser(..., StreamObserver observer) {
+ // 只重写需要的
+ }
+ // getUser 自动走父类的 Unimplemented
+}
+```
+
+Go 版本的优势在于**嵌入比继承更灵活**——你可以同时嵌入多个类型、控制方法优先级(离当前结构体越近优先匹配),并且没有单继承限制。
+
+## 执行流程图解
+
+```mermaid
+flowchart TD
+ Client["Client RPC Call"] --> Lookup["Server 查找 dispatch table"]
+ Lookup --> Found{"Method 是否被覆写?"}
+ Found -->|"是"| Handler["Handler 执行业务逻辑"]
+ Found -->|"否"| Fallback["UnimplementedStub
codes.Unimplemented 错误"]
+
+ Handler --> Resp["Response to Client"]
+ Fallback --> Resp
+
+ ProtoChanged["Proto 新增 Method"] --> GenCode["Protoc 重新生成"]
+ GenCode --> CompileErr["userService 缺少新方法,编译失败 ✅"]
+
+ style Client fill:#E3F2FD,stroke:#1976D2
+ style Handler fill:#A8E6CF,stroke:#2E7D32
+ style Fallback fill:#FFB3BA,stroke:#C62828
+ style CompileErr fill:#FFF3E0,color:#000,stroke:#E65100
+```
+
+## 实际使用中的注意事项
+
+> [!warning] 不要手动实现 Unimplemented 方法
+
+如果你在自己的结构体中也定义了一个同名 method(比如不小心写了 `func (s *userService) MustEmbedUnimplementedUserServiceServer()`),它会**覆盖**嵌入类型的空方法。虽然功能上不影响(interface 检查只看是否存在),但语义混乱,应避免。
+
+> [!tip] 如何快速确认某个 Service 的所有方法都被覆盖了?
+
+可以用 IDE 的 "implement interface" 功能列出未实现的方法,或者写一个简单的 compile test:
+
+```go
+var _ pb.UserServiceServer = (*userService)(nil) // 编译期检查
+```
+
+加上这一行后,任何遗漏的方法都会在这句编译报错,适合作为 CI 的一部分。
+
+## 常见陷阱
+
+> [!danger] 嵌入顺序决定方法优先级
+>
+> Go 的结构体嵌入具有**层级可达性**——当多个嵌入类型都有同名方法时,编译器选择距离当前结构体最近的那个。如果不小心嵌入了错误的类型,你的实现可能永远不会被调用:
+>
+> ```go
+> type userService struct {
+> pb.UnimplementedUserServiceServer
+> someOtherEmbeddedStruct // ← 如果有同名方法,这里优先!
+> }
+> ```
+>
+> 养成**将 Unimplemented 嵌入放在最后**的习惯,避免意外覆盖:
+>
+> ```go
+> type userService struct {
+> *UserStore // 业务依赖先放前面
+> someHelper // 工具类
+> pb.UnimplementedUserServiceServer // 永远放最后
+> }
+> ```
+
+> [!warning] `embed` vs `pointer embed` 的选择
+>
+> gRPC 生成的 `UnimplementedXxxServer` **值类型和指针类型都可以嵌入**,但效果不同:
+>
+> | 嵌入方式 | `*userService` 是否满足接口 | 说明 |
+> |---------|---------------------------|------|
+> | `pb.UnimplementedXxxServer`(值) | `*userService` ✅ | 推荐,指针接收者可调用值方法 |
+> | `*pb.UnimplementedXxxServer`(指针) | `*userService` ✅ 且 `userService` ❌ | 仅指针满足接口 |
+>
+> **最佳实践**: 大多数 gRPC server 方法签名是 `func (s *Service) Method(...)`,所以用指针 receiver。嵌入值类型的 `Unimplemented` 即可同时支持两种调用方式。
+
+## Server 集成示例
+
+下面是一个完整的、可直接运行的 Server 搭建片段,展示 UnimplementedStub 在整个链路中的位置:
+
+```go
+package server
+
+import (
+ "context"
+ "log"
+
+ "google.golang.org/grpc"
+ pb "your/proto/package"
+)
+
+// UserServiceImpl — 只需实现实际需要的方法
+type UserServiceImpl struct {
+ store *UserStore // 业务依赖
+ pb.UnimplementedUserServiceServer // 放最后
+}
+
+func (s *UserServiceImpl) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
+ user := &model.User{Name: req.GetName(), Email: req.GetEmail()}
+ if err := s.store.Create(ctx, user); err != nil {
+ return nil, status.Errorf(codes.Internal, "create failed: %v", err)
+ }
+ return &pb.CreateUserResponse{Id: fmt.Sprintf("%d", user.ID)}, nil
+}
+
+// GetUser 不写 — 自动返回 codes.Unimplemented
+// DeleteUser 不写 — 同上
+
+func NewUserServiceImpl(store *UserStore) *UserServiceImpl {
+ return &UserServiceImpl{store: store}
+}
+
+// Register 注册所有服务到 grpc.Server
+func Register(srv *grpc.Server, store *UserStore) {
+ pb.RegisterUserServiceServer(srv, NewUserServiceImpl(store))
+ log.Println("UserService registered")
+}
+```
+
+关键要点:
+- `RegisterUserServiceServer()` 内部会校验你传入的类型是否实现了 `UserServiceServer` interface
+- 由于 `UnimplementedUserServiceServer` 提供了所有方法的默认实现,你只需编写实际需要的 method
+- Proto 新增 method → protoc 重新生成 → 编译失败 → **有意识的决策点**
+
+## 关联笔记
+
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册]]
+- [[hhs/gRPC/3. 服务端实现/09-Streaming Handler]]
diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server.md
new file mode 100644
index 0000000..c1b002d
--- /dev/null
+++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server.md
@@ -0,0 +1,214 @@
+---
+tags: [gRPC, Go, Server, Beginner]
+create time: 2026-05-13 23:15
+---
+
+# Hello World — 最小可运行 Server
+
+## 概述
+
+这是你搭建 gRPC 服务器的第一行代码。从零开始,用最少的步骤跑通一个能接收请求的 gRPC 服务——不需要 TLS、不需要优雅关闭、不需要拦截器。先把"能跑起来"搞定,再一步步往生产环境逼近。
+
+> [!tip] 学习路径建议
+> 本文是「Server 搭建」系列的**第一课**。后续我们会在这个最小示例上逐步叠加配置,直到达到生产可用。不要跳过——每一篇解决一个实际问题。
+
+## 三段式模板
+
+不管多复杂的 gRPC 服务器,本质上都是三步:
+
+| 步骤 | 做了什么 | 类比 |
+|------|---------|------|
+| 1. 监听端口 | 告诉操作系统"我准备在这里接客了" | 餐厅挂出"营业中"牌子 |
+| 2. 注册服务 | 告诉 server "谁来接待客人" | 安排服务员站好位置 |
+| 3. 启动 Serve | 正式开始接客 | 开门迎客 |
+
+### 生命周期时序
+
+```mermaid
+sequenceDiagram
+ participant M as main()
+ participant G as grpc.Server
+ participant O as OS
+ participant C as Client
+
+ M->>O: net.Listen("tcp", ":50051")
+ Note over O: "操作系统开始监听端口"
+ M->>G: grpc.NewServer()
+ M->>G: RegisterUserServiceServer()
+ G-->>M: "服务已注册"
+ M->>O: Serve(lis)
+ Note over M,G: "main 阻塞在此"
+ loop 持续监听
+ C->>O: "gRPC 连接请求"
+ O->>G: "转发请求"
+ G->>G: "路由到对应 RPC 方法"
+ G-->>C: "返回响应"
+ end
+```
+
+**一句话理解**:`main()` 是总指挥,先让操作系统"就位"(Listen),再给 server "派活"(Register),最后自己站在门口等活上门(Serve)。
+
+### 完整代码
+
+```go
+// cmd/server/main.go
+package main
+
+import (
+ "context"
+ "log"
+ "net"
+
+ "go.uber.org/zap" // 日志库
+ "google.golang.org/grpc" // gRPC 框架
+ "google.golang.org/grpc/reflection"
+
+ pb "your/proto/gen/go" // protoc 生成的代码
+)
+
+func main() {
+ // ====== 初始化日志 ======
+ logged, _ := zap.NewProduction()
+ zap.ReplaceGlobals(logged)
+ defer logged.Sync()
+
+ // ====== 第一步:监听端口 ======
+ lis, err := net.Listen("tcp", ":50051")
+ if err != nil {
+ log.Fatalf("监听失败: %v", err)
+ }
+
+ // ====== 第二步:创建 server + 注册服务 ======
+ s := grpc.NewServer() // 创建空 server
+ pb.RegisterUserServiceServer(s, &userService{}) // 注册 UserService
+
+ // Reflection 方便开发时调试(生产环境关掉)
+ reflection.Register(s)
+
+ zap.L().Sugar().Infow("server 启动", "addr", lis.Addr())
+
+ // ====== 第三步:开始接客 ======
+ if err := s.Serve(lis); err != nil {
+ log.Fatalf("serve 出错: %v", err)
+ }
+}
+
+// userService 实现 pb.UserServiceServer interface
+type userService struct {
+ // UnimplementedUserServiceServer 提供了所有方法的默认行为(返回 Unimplemented)
+ // 新增 proto 方法时,编译器会因缺少实现而报错 → 提前发现问题
+ pb.UnimplementedUserServiceServer
+}
+
+// 你只需要实现实际需要提供的方法
+func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
+ // ... 业务逻辑
+ return &pb.CreateUserResponse{Id: "123"}, nil
+}
+```
+
+逐段解释:
+
+### 第 1 段:监听端口
+
+```go
+lis, err := net.Listen("tcp", ":50051")
+```
+
+`net.Listen` 让操作系统在 `50051` 端口等待连接。**注意这里还没有启动任何 gRPC 逻辑**——它只是一个普通的 TCP 监听器。gRPC 框架只是在 TCP 之上加了一层协议而已。
+
+> [!warning] 常见坑:端口冲突
+> 如果端口已被占用,`Listen` 会立即返回错误。开发时可以选 `90051`、`150051` 这种高端口避开系统服务;生产环境则通过配置管理端口号。
+
+> [!tip] 进阶:网络地址格式
+> `":50051"` 等价于 `"0.0.0.0:50051"`(监听所有网卡)。如果只暴露给本机调试,可以改为 `"127.0.0.1:50051"`;如果需要 IPv6,用 `"[::]:50051"`。
+
+这个值后续会抽取到配置文件里,不再硬编码。
+
+### 第 2 段:创建 server 并注册
+
+```go
+s := grpc.NewServer()
+pb.RegisterUserServiceServer(s, &userService{})
+```
+
+- `grpc.NewServer()` 创建一个**空的** gRPC server——它什么都不做,只是容器。它此时没有 TLS、没有拦截器、没有任何中间件配置。
+- `RegisterUserServiceServer` 把你的实现塞进去,告诉 server 哪些 RPC 方法可以响应。
+
+> [!question] 一个 server 能注册多个 service 吗?
+> 当然可以。proto 文件里的每个 `service` 都会生成一个对应的 `RegisterXxxServer` 函数,全部挂到同一个 server 上即可。这正是微服务中"多 proto 共享端口"的基础。
+
+后续会通过 `grpc.NewServer(opts ...grpc.ServerOption)` 传入各种选项来增强这个 server(TLS、流控、keepalive……),目前先用零配置的默认版本。
+
+### 第 3 段:启动 Serve
+
+```go
+s.Serve(lis)
+```
+
+这一步之后,main 函数会**阻塞在这里**,持续监听并处理请求——直到进程被杀死或遇到错误。
+
+这意味着:**Serve 之后的代码永远不会执行**。需要在这之前完成所有初始化工作(日志、数据库连接、协程启动等)。这也是为什么大多数项目最终会把 `main()` 封装成一个更结构化的启动函数——纯裸跑只适合入门示例。
+
+## 为什么需要 UnimplementedXxxServer?
+
+很多新手会疑惑:为什么要嵌入一个 `UnimplementedUserServiceServer`?
+
+原因很简单:**Proto 新增方法时,编译期会自动提醒你。**
+
+假设你的 proto 文件新增了一个 `DeleteUser` 方法:
+
+```proto
+service UserService {
+ rpc CreateUser(...) returns (...);
+ rpc GetUser(...) returns (...);
+ rpc DeleteUser(...) returns (...); // ← 新加的
+}
+```
+
+重新 `protoc` 后,如果你没有嵌入 `UnimplementedUserServiceServer`,编译器会报错:
+
+```
+cannot use &userService{} as UserServiceServer:
+ missing method DeleteUser in receiver type userService
+```
+
+从"线上出 Bug 才发现问题"变成了"提交代码前就发现"——把风险挡在编译期。
+
+> [!question] 那如果我不嵌入会怎样?
+> 你可以手动实现所有方法,但每次 proto 变动都要手动检查一遍有没有遗漏。人总会忘,编译器不会。
+
+详细的零-Stub 模式机制参见 [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式]]。
+
+## 运行验证
+
+启动 server 后,用 `grpcurl` 快速验证是否工作:
+
+```bash
+# 列出可用的服务和方法
+grpcurl -plaintext localhost:50051 list
+
+# 查看某个 service 的详细接口定义
+grpcurl -plaintext localhost:50051 describe UserService
+```
+
+如果看到你的 `UserService` 和相关方法,说明一切正常。
+
+> [!note] 这里的 `-plaintext` 表示不使用 TLS。生产环境必须加上 TLS,那是后面的话题。
+
+## 下一步
+
+现在你有了一个能跑的 server,但离生产还有距离:
+
+- 没有限制消息大小 → 大请求可能撑爆内存 —→ [[hhs/gRPC/3. 服务端实现/02-ServerOption 生产级配置速查]]
+- 没有 TLS → 数据明文传输 —→ [[hhs/gRPC/3. 服务端实现/02-ServerOption 生产级配置速查]]
+- 没有 keepalive → 中间的负载均衡器可能切断空闲连接 —→ [[hhs/gRPC/3. 服务端实现/02-ServerOption 生产级配置速查]]
+- 没有优雅关闭 → 直接 kill 会导致正在处理的请求丢失 —→ [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/06-Graceful Shutdown 优雅关闭]]
+
+接下来的文章会逐个解决这些问题。
+
+## 关联笔记
+
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式]]
+- [[hhs/gRPC/3. 服务端实现/02-ServerOption 生产级配置速查]]
+- [[hhs/gRPC/2. gRPC 核心篇/02-gRPC 核心术语]]
diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/02-ServerOption 生产级配置速查.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/02-ServerOption 生产级配置速查.md
new file mode 100644
index 0000000..b3b9306
--- /dev/null
+++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/02-ServerOption 生产级配置速查.md
@@ -0,0 +1,360 @@
+---
+tags: [gRPC, Go, Server, Configuration]
+create time: 2026-05-13 23:20
+---
+
+# ServerOption — 生产级配置速查
+
+## 概述
+
+`grpc.NewServer(...)` 接受任意数量的 `ServerOption`,每个 Option 都是一个函数,用来改变 server 的行为。它们使用**函数式选项模式**——看起来像参数列表,其实是链式修改内部状态。
+
+本文不罗列所有选项(官方文档有),只讲你在生产环境中最常遇到、最需要认真考虑的这几个——从 TLS 到传输层优化,共九项。
+
+> [!question] 为什么不全部开启?
+> 有些选项只在特定场景有用,开了反而增加复杂度或带来副作用。学会"知道什么时候不开"比"知道开什么"更重要。
+
+## 核心选项速查
+
+### 1. TLS 认证
+
+```go
+grpc.Creds(credentials.NewTLS(tlsConfig))
+```
+
+| 项目 | 说明 |
+|------|------|
+| **默认值** | 无加密,明文传输 |
+| **必须配吗** | **生产环境必须** |
+| **为什么** | gRPC 通常跑在内网,但不代表数据可以不加密。中间人攻击、旁路监听都可能读到你的请求体 |
+
+> [!tip] 内网也要 TLS
+> "我们部署在内网,不需要 TLS"——这个想法在过去可能成立,但现在零信任架构下,内网到内网之间也应该加密。即使只做 mTLS(双向认证)也好过什么都没有。
+
+### 2. 消息大小限制
+
+```go
+grpc.MaxRecvMsgSize(16 * 1024 * 1024), // 接收上限 16MB
+grpc.MaxSendMsgSize(16 * 1024 * 1024), // 发送上限 16MB
+```
+
+| 项目 | 说明 |
+|------|------|
+| **默认值** | 各 4MB |
+| **调大原因** | 文件上传、大批量导出等场景需要更大的消息 |
+| **注意事项** | 每次收到一个超过上限的消息,连接会被直接关闭,client 收到 `resource exhausted` 错误 |
+
+> [!warning] 对称设置
+> `MaxRecvMsgSize` 和 `MaxSendMsgSize` 应该对称设置。如果你限制了接收为 16MB,但发送还是默认的 4MB,那当你的服务要返回大数据时也会报错。
+
+### 3. 最大并发流数量
+
+```go
+grpc.MaxConcurrentStreams(1024)
+```
+
+| 项目 | 说明 |
+|------|------|
+| **默认值** | 100 |
+| **含义** | HTTP/2 单个连接上最多同时存在 100 个未响应的 stream |
+| **为什么要改** | 如果 client 大量使用 streaming RPC,100 可能不够用;放宽到 1024+ 是常见做法 |
+
+> [!note] DoS 防护考量
+> 这个值的本质是防恶意客户端开太多并发流耗尽服务器资源。正常业务不会触发上限,所以调到 1024 基本没有风险。
+
+### 4. Keepalive 心跳参数 ⭐ 高频踩坑
+
+```go
+grpc.KeepaliveParams(keepalive.ServerParameters{
+ MaxConnectionIdle: 15 * time.Minute, // 空闲多久断开
+ KeepaliveTime: 20 * time.Second, // 每多久发一次心跳
+ KeepaliveTimeout: 5 * time.Second, // 心跳超时时间
+ MinTimeBetweenPings: 10 * time.Second,
+ PingWithoutCallsAllowed: true, // 没请求时也允许发 ping
+})
+```
+
+这是最容易忽略也最容易出问题的配置。
+
+#### 默认值的问题
+
+gRPC 默认的 keepalive 策略是:idle 超过 **2 小时**才断开连接。这意味着:
+
+- 如果你的服务后面有 **负载均衡器**(如 Envoy、Nginx、AWS ALB),LB 的超时时间通常只有几分钟
+- LB 会在 idle 几分钟后切断连接,但 gRPC server 不知道,还在往已经断掉的 socket 写数据
+- Client 侧看到的现象就是随机的 **"connection reset by peer"** 错误
+
+#### 解决方案
+
+把 `MaxConnectionIdle` 设短到 **15~20 分钟**——让 server 在 LB 之前主动重建连接。这样两端对连接生命周期有一致认知,LB 只是被动转发,不会被要求管理长连接。
+
+| 场景 | `MaxConnectionIdle` | 建议 |
+|------|---------------------|------|
+| **直连无 LB** | 保持默认 (2h) | 减少不必要的连接重建开销 |
+| **有 LB (Nginx/Envoy)** | 15~20 分钟 | 与 LB 超时时间错开即可 |
+| **K8s + Ingress** | 5~10 分钟 | K8s Service 的 timeout 更短 |
+
+```mermaid
+flowchart LR
+ subgraph Client["Client"]
+ C_conn["HTTP/2 Connection"]
+ end
+
+ subgraph LB["Load Balancer
timeout: 5min"]
+ LB_state["连接状态: active → idle → close"]
+ end
+
+ subgraph Server["Server
默认 idle: 2h"]
+ S_conn["HTTP/2 Connection"]
+ end
+
+ C_conn -->|连接| LB_state
+ LB_state -->|连接| S_conn
+
+ style C_conn fill:#E3F2FD
+ style S_conn fill:#A8E6CF
+ style LB_state fill:#FFEAA7,color:#000
+```
+
+直连场景:
+
+```mermaid
+flowchart LR
+ Client["Client"] -->|直连| Server["Server"]
+ style Client fill:#E3F2FD
+ style Server fill:#A8E6CF
+```
+
+### 5. 拦截器链
+
+```go
+grpc.ChainUnaryInterceptor(logging.Unary(), auth.Unary())
+```
+
+拦截器是 gRPC 里最灵活的扩展点,用于在方法执行前后插入横切逻辑。
+
+| 层级 | 做什么 | 例子 |
+|------|--------|------|
+| **外层** | 横切关注点 | 日志、链路追踪、指标采集 |
+| **中层** | 业务安全校验 | 鉴权、限流 |
+| **内层** | 兜底逻辑 | panic recover、超时控制 |
+
+执行顺序像剥洋葱:
+
+```mermaid
+flowchart LR
+ Client["请求到达"] --> Logging["日志拦截器
(最外)"]
+ Logging --> Auth["鉴权拦截器
(中层)"]
+ Auth --> Recovery["恢复拦截器
(最内)"]
+ Recovery --> Handler["实际业务方法"]
+
+ style Client fill:#E3F2FD
+ style Handler fill:#FFF3E0
+```
+
+拦截器的详细用法参见 [[hhs/gRPC/5. 中间件与拦截器/14-Unary 与 Stream 拦截器]]。
+
+### 6. StatsHandler(链路追踪 / 指标)
+
+```go
+// grpc.StatsHandler(otelgrpc.NewServerHandler()), // OpenTelemetry
+```
+
+StatsHandler 是一种比拦截器更底层的方式注入可观测性数据。典型用途是接入 OpenTelemetry 做分布式链路追踪。
+
+> [!note] 拦截器 vs StatsHandler
+> - 拦截器:Go-only 方案,编写灵活,可以直接访问 request/response
+> - StatsHandler:多语言统一方案,OpenTelemetry SDK 原生支持,跨语言的 metrics/tracing 一致性好
+
+#### 用 StatsHandler 采集 gRPC 核心指标
+
+一个典型的 server-side StatsHandler 需要关注三个事件:
+
+```go
+type MetricsHandler struct{}
+
+func (*MetricsHandler) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context {
+ return ctx // 连接级别标签(可选)
+}
+
+func (*MetricsHandler) HandleConn(ctx context.Context, cs stats.ConnStats) {
+ if cs.Done() {
+ zap.L().Sugar().Infow("连接关闭",
+ "remote", cs.RemoteAddr(),
+ "duration", cs.Duration().Milliseconds(),
+ )
+ }
+}
+
+func (*MetricsHandler) TagRPC(context.Context, *stats.RPCTagInfo) context.Context { return context.Background() }
+
+func (*MetricsHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) {
+ switch rs.(type) {
+ case *stats.Begin:
+ // RPC 开始
+ case *stats.OutPayload:
+ // 记录请求大小
+ case *stats.InPayload:
+ // 记录响应大小 + 耗时
+ case *stats.End:
+ // 汇总:状态码、总耗时 → Prometheus counter/histogram
+ }
+}
+```
+
+生产环境通常不需要自己写——直接使用 `prometheus-grpc` 或 `otelgrpc` 库即可。理解原理是为了排查"为什么某个指标的粒度不够"时知道该去哪里改。
+
+---
+
+## 更多实用选项
+
+### 7. 流式安全模式
+
+gRPC **没有提供**一个直接的全局选项来限制 stream 累计收到的消息数量。这是有意为之的设计选择——有些场景(如大文件分块传输)天然需要大量小消息。
+
+因此你需要在应用层做防护:
+
+```go
+// Stream 服务端实现中手动计数
+func (s *server) UploadFile(stream pb.UserService_UploadFileServer) error {
+ var msgCount int64
+ const maxMessages = 100000
+
+ for {
+ req, err := stream.Recv()
+ if err != nil {
+ return status.Convert(err).Err()
+ }
+ msgCount++
+ if msgCount > maxMessages {
+ return status.Errorf(codes.ResourceExhausted,
+ "max messages exceeded: %d", msgCount)
+ }
+ // 处理数据...
+ }
+}
+```
+
+> [!tip] 结合上下文取消
+> gRPC streaming RPC 天然会收到 `context.Canceled` / `context.DeadlineExceeded`。只要你正确设置了 client deadline,大多数异常情况都会被自动终止,不需要额外兜底。
+
+### 8. 请求超时控制的完整理解 ⭐
+
+> [!question] gRPC 有默认的客户端超时吗?
+> 没有。gRPC 协议本身不设定任何超时——如果 client 不指定 deadline,server 会一直等下去。这意味着一个慢查询可以永远占用一个 goroutine。
+
+这是 gRPC 最容易让人困惑的地方:**超时控制本质上是 client 的责任**,但 server 也应该有自己的兜底策略。
+
+| 层 | 谁设 | 怎么设 | 作用 |
+|----|------|--------|------|
+| **client 端 deadline** | Client | `context.WithTimeout(ctx, 5*time.Second)` | 请求从发起起最多活 5 秒,超时时自动取消 |
+| **server 端 recover** | Server interceptor | panic recover / context 检查 | 防止业务逻辑泄漏 goroutine |
+
+```go
+// Client 侧:设置 5 秒超时
+ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+defer cancel()
+resp, err := client.GetUser(ctx, &pb.GetRequest{Id: "123"})
+```
+
+```go
+// Server 侧:拦截器兜底(放在拦截器链最内层)
+func TimeoutUnaryInterceptor(maxTimeout time.Duration) grpc.UnaryServerInterceptor {
+ return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
+ if _, ok := ctx.Deadline(); !ok {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, maxTimeout)
+ defer cancel()
+ }
+ return handler(ctx, req)
+ }
+}
+```
+
+> [!tip] 最佳实践
+> - **Client 必须设 deadline**——这是第一道防线
+> - **Server 拦截器设兜底 timeout**——防止漏网的请求挂住 goroutine
+> - 兜底 timeout 应该 ≥ 大部分正常请求的 P99 耗时,否则会把正常请求误杀
+
+### 9. 传输层性能优化 ⭐
+
+这些选项直接操作 TCP 层缓冲行为,在高吞吐场景下效果明显。
+
+```go
+grpc.WriteBufferSize(512 * 1024), // 写缓冲区 512KB(默认 32KB)
+```
+
+| 选项 | 默认值 | 适用场景 | 注意事项 |
+|------|--------|---------|---------|
+| `WriteBufferSize` | 32KB | 大消息频繁发送的场景 | 增大可减少系统调用次数,但会引入更多写入延迟 |
+| `ReadBufferSize` | 32KB | 接收大消息的场景 | 同写入侧,增大读缓冲减少 read() 系统调用 |
+
+> [!note] gRPC-Go 的公开边界
+> gRPC-Go 把大部分传输层调优参数放在内部类型中(如 `InitialWindowSize`、`InitialConnWindowSize`),对普通用户不开放。**大多数情况下你只需要关注 `WriteBufferSize`**。如果连它都不需要调——说明你的瓶颈不在网络 I/O。
+
+```mermaid
+flowchart LR
+ CA["Client App"] --> CB["Write Buffer
客户端 32KB"]
+ CB --> CT["TCP Send
Stack"]
+ CT -->|TCP Packets| ST["TCP Recv
Stack"]
+ ST --> SB["Read Buffer
服务端 32KB"]
+ SB --> SA["Server App"]
+
+ style CB fill:#FFEAA7,color:#000
+ style SB fill:#FFEAA7,color:#000
+ style CT fill:#E3F2FD
+ style ST fill:#A8E6CF
+```
+
+> [!tip] 什么时候需要改?
+> 如果你的 QPS 不高、消息体 ≤ 1MB,默认值完全够用。**先测后调**——用压测工具对比调整前后的 CPU 和 throughput,有提升再上线。不要凭感觉改参数。
+
+---
+
+## 完整配置示例
+
+把上面讲到的所有选项组合在一起。按需裁剪,**不要照抄**。
+
+```go
+s := grpc.NewServer(
+ // TLS — 生产必配
+ grpc.Creds(credentials.NewTLS(tlsConfig)),
+
+ // 消息大小 — 根据业务需要调整
+ grpc.MaxRecvMsgSize(16*1024*1024),
+ grpc.MaxSendMsgSize(16*1024*1024),
+
+ // 并发流限制 — 放宽到 1024
+ grpc.MaxConcurrentStreams(1024),
+
+ // Keepalive — 适配 LB 环境
+ grpc.KeepaliveParams(keepalive.ServerParameters{
+ MaxConnectionIdle: 15 * time.Minute,
+ KeepaliveTime: 20 * time.Second,
+ KeepaliveTimeout: 5 * time.Second,
+ PingWithoutCallsAllowed: true,
+ }),
+
+ // 传输层优化 — 高吞吐场景才需要调整
+ grpc.WriteBufferSize(512 * 1024),
+
+ // 拦截器链
+ grpc.ChainUnaryInterceptor(
+ logging.Unary(), // 外层:日志
+ auth.Unary(), // 中层:鉴权
+ recovery.Unary(), // 内层:panic recover
+ ),
+
+ // OpenTelemetry 链路追踪(替代或叠加拦截器)
+ // grpc.StatsHandler(otelgrpc.NewServerHandler()),
+)
+```
+
+记住:**不要照抄上面的配置**。每一个参数都应该根据你的具体场景(有没有 LB、消息有多大、是否需要 TLS)来做决策。
+
+## 关联笔记
+
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server]]
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/06-Graceful Shutdown 优雅关闭]]
+- [[hhs/gRPC/5. 中间件与拦截器/14-Unary 与 Stream 拦截器]]
+- [[hhs/gRPC/5. 中间件与拦截器/16-日志与链路追踪]]
diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/03-Service Registration 服务注册详解.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/03-Service Registration 服务注册详解.md
new file mode 100644
index 0000000..7f17112
--- /dev/null
+++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/03-Service Registration 服务注册详解.md
@@ -0,0 +1,278 @@
+---
+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
"/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
service Xxx { ... }"] --> B["protoc 生成代码
_grpc.pb.go"]
+ B --> C["实现 XxxServer interface
嵌入 Unimplemented"]
+ C --> D["构造器注入依赖
NewXxxService(...)"]
+ D --> E["RegisterXxxServer(s, impl)"]
+ E --> F["s.Serve(lis)
阻塞等待请求"]
+ 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 定义与代码生成]]
diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/04-Reflection 开发调试利器.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/04-Reflection 开发调试利器.md
new file mode 100644
index 0000000..3bad77e
--- /dev/null
+++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/04-Reflection 开发调试利器.md
@@ -0,0 +1,196 @@
+---
+tags: [gRPC, Go, Server, Debugging, Reflection]
+create time: 2026-05-13 23:30
+---
+
+# Reflection — 开发调试利器
+
+## 概述
+
+Reflection(反射机制)允许外部客户端在**运行时**通过 gRPC 协议查询 server 上已注册的所有 service、method 和 message 定义——完全不需要提前拿到 `.proto` 文件。
+
+> [!tip] 核心场景
+> Reflection 的定位是**开发期的调试工具**。想象一下你刚启动一个服务,手头没有 proto 文件,不知道暴露了哪些接口——reflection 让你直接问 server "你有什么?"即可得到答案。
+
+它的价值在于让 `grpcurl` 等工具能够动态枚举 API、构造请求并交互式调用,大幅降低开发和联调门槛。但正因为它暴露了完整的内部 API schema,**生产环境必须禁用**。
+
+## 核心原理
+
+> [!question] 没有 .proto 文件,客户端怎么知道服务有哪些接口?
+>
+> gRPC 协议本身要求两端共享 .proto 文件来解析消息。Reflection 巧妙地在 server 上内置了一个特殊的 service(`grpc.reflection.v1.ServerReflection`),server 在启动时将所有已注册服务的 descriptor(描述符)存入内存。客户端通过调用这个内置 service,以请求-响应的方式"问"出所有元数据,然后动态构建 proto、生成 stub。
+
+```mermaid
+sequenceDiagram
+ participant Dev as "开发者 (grpcurl)"
+ participant Ref as "Server Reflection RPC"
+ participant Store as "Registered Descriptors"
+ participant Server as "gRPC Server"
+
+ Dev->>Ref: ServerReflectionInfo() -> stream open
+ Ref->>Store: ListAllServices()
+ Store-->>Ref: ["user.v1.UserService", ...]
+ Ref-->>Dev: response with service names
+ Dev->>Ref: FileContainingSymbol("UserService.GetUser")
+ Ref->>Store: resolve descriptors
+ Store-->>Ref: serialized FileDescriptorProto
+ Ref-->>Dev: bytes returned
+ Note over Dev: client dynamically generates stub
+ Dev->>Server: actual RPC call with typed message
+```
+
+**关键点**:Reflection 并不发送 `.proto` 源码字符串,而是返回编译后的 `FileDescriptorProto` 字节流——客户端用这个就能原地重建完整的类型信息,无需任何本地 proto 文件。
+
+## 开启方式
+
+在 Go 中开启 reflection 只需要一行代码:
+
+```go
+import (
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/reflection"
+)
+
+func main() {
+ s := grpc.NewServer()
+
+ // 注册业务服务...
+ mypb.RegisterUserServiceServer(s, &userService{})
+
+ // 开启反射 —— 加在 NewServer 之后、Serve 之前即可
+ reflection.Register(s)
+
+ s.ListenAndServe()
+}
+```
+
+> [!note] init() 自动注册模式
+> 另一种写法是直接 blank import,利用包的 init() 函数自动完成注册:
+>
+> ```go
+> import _ "google.golang.org/grpc/reflection"
+> ```
+>
+> 这种方式最简洁但可读性较差——读代码的人可能不理解为什么没有显式调用 `reflection.Register()`。推荐显式注册的写法,明确表达意图。
+
+两种方式的本质区别:
+
+| 方式 | 触发时机 | 可读性 | 推荐度 |
+|------|---------|--------|--------|
+| `reflection.Register(s)` | 运行时调用 | 清晰,一眼可知 | ✅ 推荐 |
+| `import _ "..."` | 包初始化阶段 | 隐蔽,需要深入理解 | ⚠️ 仅适合快速原型 |
+
+## 开发阶段有什么用?
+
+### 1. 列出可用 API
+
+不需要 `.proto` 文件就能查看当前 server 提供了哪些接口:
+
+```bash
+grpcurl -plaintext localhost:50051 list
+
+# 输出示例:
+# my.service.v1
+# my.service.v1.UserService
+# my.service.v1.UserService.CreateUser
+# my.service.v1.UserService.GetUser
+```
+
+### 2. 查看接口详细定义
+
+```bash
+grpcurl -plaintext localhost:50051 describe my.service.v1.UserService.CreateUser
+
+# 输出示例:
+# message CreateUserRequest {
+# string name = 1;
+# string email = 2;
+# }
+# message CreateUserResponse {
+# string id = 1;
+# }
+```
+
+### 3. 快速测试接口
+
+```bash
+# 构造 JSON 直接发请求
+grpcurl -plaintext -d '{"name":"Alice","email":"alice@example.com"}' \
+ localhost:50051 my.service.v1.UserService/CreateUser
+```
+
+### 4. IDE 自动补全
+
+配合 IDE 插件(如 VS Code 的 gRPC extension),可以直接在编辑器中通过 reflection 自动发现可用的 method,获得类似原生的方法名和参数结构补全体验。
+
+## 常见调试命令速查
+
+以下是日常开发中最常用的 `grpcurl` + reflection 组合命令:
+
+| 目的 | 命令 |
+|------|------|
+| 列出所有 service | `grpcurl -plaintext host:port list` |
+| 列出某 service 的子元素 | `grpcurl -plaintext host:port list my.service.FooService` |
+| 查看 service 定义 | `grpcurl -plaintext host:port describe my.service.FooService` |
+| 查看 message 定义 | `grpcurl -plaintext host:port describe my.service.CreateReq` |
+| 调用 RPC(交互式) | `grpcurl -plaintext -d '{}' host:port my.service.FooService/Bar` |
+| 格式化 JSON 输出 | `grpcurl -plaintext -pretty -d '{}' host:port ...` |
+
+> [!tip] `-pretty` 标志
+> gRPC 返回的二进制数据默认以紧凑格式显示。加上 `-pretty` 后会自动 format JSON,阅读友好很多——调试时建议始终带上这个 flag。
+
+## 为什么生产环境必须关闭?
+
+> [!warning] 安全红线
+> Reflection 本质上是一个**无需认证的内省接口**。任何能连接到 gRPC port 的客户端都可以调用它,不需要 token、不需要 API key。
+
+| 风险 | 严重程度 | 说明 |
+|------|---------|------|
+| **信息泄露** | 🔴 高 | 暴露完整的 API schema——方法名、消息结构、字段类型。攻击者可以用来规划攻击路径 |
+| **内部方法探测** | 🟡 中 | 即使某些方法未对外公开文档,也可以通过 reflection 发现 |
+| **内存占用** | 🟢 低 | 需要在内存中保存所有 descriptor protobuf,开销不大但确实存在 |
+
+### 条件开启方案
+
+通过环境变量控制,确保生产环境不会误开:
+
+```go
+func main() {
+ s := grpc.NewServer()
+
+ // 注册业务服务...
+
+ // 仅在非生产环境下开启 reflection
+ if os.Getenv("ENABLE_GRPC_REFLECTION") == "true" {
+ reflection.Register(s)
+ }
+
+ s.ListenAndServe()
+}
+```
+
+> [!note] 不同环境的推荐配置
+>
+> | 环境 | Reflection | 控制方式 |
+> |------|-----------|---------|
+> | Local Dev | On | 默认开启,无需额外配置 |
+> | Staging / Pre-release | Optional | 按需手动设置 `ENABLE_GRPC_REFLECTION=true` |
+> | Production | **Off** | 不设置变量,代码路径被跳过 |
+
+这种基于环境变量的控制方式兼顾了灵活性——测试和预发可以随时决定是否开启,而生产环境即使忘记注释代码也不会出问题。
+
+## 生产环境:更安全的替代方案
+
+如果你的需求是在生产环境查询接口定义或文档,reflection 不是正确选择。以下是更安全的方式:
+
+| 方案 | 适用场景 | 优势 |
+|------|---------|------|
+| **OpenAPI Gateway** | RESTful API 展示层 | Swagger UI + auth 可控 |
+| **自建文档站** | 团队内部知识库 | 基于 `.proto` 文件自动生成 |
+| **配置中心** (Apollo / Consul) | 集中管理服务元数据 | 权限管理、版本追溯 |
+
+## 关联笔记
+
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server]]
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/02-ServerOption 生产级配置速查]]
+- [[hhs/gRPC/3. 服务端实现/10-健康检查与反射]]
diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/05-Multi-Port 多端口暴露.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/05-Multi-Port 多端口暴露.md
new file mode 100644
index 0000000..e2b5a52
--- /dev/null
+++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/05-Multi-Port 多端口暴露.md
@@ -0,0 +1,154 @@
+---
+tags: [gRPC, Go, Server, Network]
+create time: 2026-05-13 23:35
+---
+
+# Multi-Port — 多端口暴露的正确姿势
+
+## 概述
+
+有时候你希望同一个服务同时监听多个端口——比如内网和外网分开、HTTP 和 gRPC 分开、IPv4 和 IPv6 双栈。这看起来很自然,但有一个 **经典坑** 值得单独一篇文章来讲。
+
+## ❌ 常见误区:一个 Server 多个 Listener
+
+新手最容易想到的写法:
+
+```go
+srv := grpc.NewServer(opts...)
+pb.RegisterUserServiceServer(srv, svc)
+
+lis1, _ := net.Listen("tcp", ":50051") // 内网
+lis2, _ := net.Listen("tcp", ":50052") // 外网
+
+go func() { _ = srv.Serve(lis1) }()
+go func() { _ = srv.Serve(lis2) }() // ← 这段有问题
+```
+
+看起来完美对吧?两个端口都监听了,请求都能进来。**但这是错误的。**
+
+### 为什么错?
+
+`grpc.Server` 内部的连接状态、stream 管理器都是**非线程安全**的。同一个 `Serve()` 被两个 goroutine 并发调用会导致 race condition——可能在某次调用中出现偶现 panic。
+
+底层原因是 `Serve()` 内部会修改 server 的连接表(添加/删除连接),而这个操作没有锁保护。
+
+实际运行中,轻则出现偶现的 `panic: concurrent goroutine`,重则导致服务直接崩溃重启——而且在开发阶段往往不触发,到了高并发生产环境才暴露出来,非常难排查。
+
+> [!summary] 误区 vs 正确做法对比
+> | 维度 | ❌ 一个 Server 多个 Serve() | ✅ 每个端口独立 Server |
+> |------|---------------------------|------------------------|
+> | **线程安全** | 未定义行为,race condition | 完全隔离,无竞争 |
+> | **调试难度** | 偶现 panic,极难复现 | 确定性好,稳定 |
+> | **差异化暴露** | 只能暴露相同的服务集 | 可按端口自定义 |
+> | **优雅关闭** | 需手动协调两个 Serve() | 各自 GracefulStop |
+
+## ✅ 正确做法:每个端口独立 Server 实例
+
+```go
+// 内网 server:只暴露 UserService
+srv1 := grpc.NewServer(opts...)
+pb.RegisterUserServiceServer(srv1, userSvc)
+
+// 外网 server:暴露 UserService + OrderService
+srv2 := grpc.NewServer(opts...)
+pb.RegisterUserServiceServer(srv2, userSvc)
+pb.RegisterOrderServiceServer(srv2, orderSvc)
+
+// 分别监听
+lis1, _ := net.Listen("tcp", ":50051")
+lis2, _ := net.Listen("tcp", ":50052")
+
+go func() {
+ if err := srv1.Serve(lis1); err != nil {
+ log.Printf("srv1 error: %v", err)
+ }
+}()
+
+go func() {
+ if err := srv2.Serve(lis2); err != nil {
+ log.Printf("srv2 error: %v", err)
+ }
+}()
+
+// GracefulStop 各管各的
+<-ctx.Done()
+srv1.GracefulStop()
+srv2.GracefulStop()
+```
+
+关键点:
+- **每个端口一个独立的 `grpc.Server`**——彼此隔离,没有状态竞争
+- **可以实现复用**——同一个 `userSvc` 实例注册到两个 server 上,节省内存
+- **差异化暴露**——内网 server 只挂核心 service,外网 server 额外挂更多 service
+
+## 架构模式:内外网分离
+
+这种模式在微服务架构中很常见:
+
+```mermaid
+flowchart LR
+ subgraph Internet["互联网"]
+ Client["External Client"]
+ end
+
+ subgraph Gateway["API Gateway"]
+ GW["网关层
聚合 + 鉴权"]
+ end
+
+ subgraph Services["业务服务"]
+ extSrv["Server :50052
(外网)
UserService + OrderService"]
+ intSrv["Server :50051
(内网)
UserService"]
+ end
+
+ Client -->|HTTPS| GW
+ GW -->|gRPC| extSrv
+ GW -.->|直连| intSrv
+
+ style Internet fill:#FFF3E0
+ style Gateway fill:#E3F2FD
+ style Services fill:#A8E6CF
+```
+
+内网端口供同 VPC 内的其他服务直连调用(低延迟、无需认证),外网端口由 gateway 统一接入(做鉴权、限流、日志)。两者物理隔离,安全边界清晰。
+
+## 关键实践:Service 复用
+
+虽然 `grpc.Server` 不能共享,但 **Service 实现可以安全复用**。同一个服务实例注册到多个 server 上没有任何问题——因为每个 server 的调用都在各自 goroutine 中串行分发,真正并发的只是业务逻辑层本身。
+
+如果需要按需懒加载 server(启动时不占用过多文件描述符),可以用 `sync.Once` 控制初始化时机:
+
+```go
+var (
+ userSrv *grpc.Server
+ userSrvOnce sync.Once
+)
+
+func getInternalUserServer() *grpc.Server {
+ userSrvOnce.Do(func() {
+ userSrv = newServer()
+ pb.RegisterUserServiceServer(userSrv, userSvc)
+ })
+ return userSrv
+}
+```
+
+这样可以在不同端口上按需组合 server,适合配置驱动的微服务架构。
+
+## 注意事项
+
+> [!warning] 不要共享 ListenConfig
+> 每个端口还是应该各自 `net.Listen()`,不要尝试共用同一个 listener。Listener 本身也不是并发安全的(多个 `Serve()` 争用一个 lis 会产生不确定行为)。
+
+> [!tip] 相同 options 封装一下
+> 如果多个 server 用相同的选项(TLS、keepalive 等),建议封装一个 helper 函数,避免重复写配置:
+
+```go
+func newServer(opts ...grpc.ServerOption) *grpc.Server {
+ return grpc.NewServer(append(defaultOpts, opts...)...)
+}
+```
+
+## 关联笔记
+
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/06-Graceful Shutdown 优雅关闭]]
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/03-Service Registration 服务注册详解]]
diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/06-Graceful Shutdown 优雅关闭.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/06-Graceful Shutdown 优雅关闭.md
new file mode 100644
index 0000000..222c8ae
--- /dev/null
+++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/06-Graceful Shutdown 优雅关闭.md
@@ -0,0 +1,251 @@
+---
+tags: [gRPC, Go, Server, Production, Shutdown]
+create time: 2026-05-13 23:40
+---
+
+# Graceful Shutdown — 优雅关闭机制
+
+## 概述
+
+gRPC 基于 HTTP/2 的长连接设计——一个连接上同时跑着多个 RPC 流。如果直接 kill 进程,所有正在处理的请求会突然断掉,client 收到的是 TCP RST(连接重置),而不是正常的结束信号。这在订单系统、支付场景中可能导致重复扣款、状态不一致。
+
+> [!question] 为什么不能等 client 发现连接断了再重试?
+>
+> 能。但从业务角度看就晚了——用户可能看到"重复扣款"的账单投诉到客服。**优雅关闭是服务端对下游负责的最后一步。**
+
+优雅关闭的目标:**拒绝新流量 → 排空已有请求 → 干净退出。**
+
+## Stop vs GracefulStop
+
+gRPC server 提供两种停止方式:
+
+| 方法 | 行为 | 未完成请求 |
+|------|------|-----------|
+| `s.Stop()` | 立即断开所有连接,不再接受新请求 | 直接报错断开 |
+| `s.GracefulStop()` | 拒绝新连接,等待活跃流完成 | 继续处理直到结束 |
+
+```go
+s.Stop() // 粗暴退出,适合紧急场景(panic recovery)
+s.GracefulStop() // 优雅退出,生产环境首选
+```
+
+> [!tip] 记忆口诀
+>
+> - **紧急情况** → `Stop()` ——保命要紧
+> - **正常关停** → `GracefulStop()` ——对下游负责
+> - **兜底保护** → 超时 + `Stop()` ——永远不要完全信任业务代码
+
+## 为什么需要超时兜底?
+
+`GracefulStop` **没有内置超时**。这意味着如果某个 handler 因为缺少 `ctx.Done()` 检测而永远不返回,整个关闭流程就会被卡死——进程永远退不出。
+
+> [!danger] 一个忘记 ctx 的 handler 就能让 GracefulStop 形同虚设
+>
+> `GracefulStop` 的设计哲学是"等所有请求完成再退出"。但如果你的业务代码里没有响应取消信号,它就真的会无限等下去。生产环境中见过多次因单个慢查询导致关停卡住 20 分钟以上的案例。
+
+所以需要一层超时保护:
+
+```go
+shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+defer cancel()
+
+done := make(chan struct{})
+go func() {
+ srv.GracefulStop()
+ close(done)
+}()
+
+select {
+case <-done:
+ log.Println("优雅退出成功")
+case <-shutdownCtx.Done():
+ log.Println("30s 超时,强制关闭")
+ srv.Stop() // 超时无情斩断
+}
+```
+
+30 秒够大部分正常请求完成了。如果一个 handler 超过 30 秒还没返回,那大概率是有问题的 handler 逻辑——继续等下去只会拖慢整个关闭流程。
+
+## Signal Handling 完整流程
+
+生产环境的标准写法将上面的片段串起来,核心思路是**用两个 select 分别处理运行期错误和关停信号**:
+
+```go
+func main() {
+ // 1. 监听 OS 信号 (Ctrl+C, docker stop, kubectl delete pod)
+ ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+ defer stop()
+
+ // 2. 创建 server
+ srv, err := NewGRPCServer()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // 3. 启动监听(阻塞)
+ lis, _ := net.Listen("tcp", ":50051")
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- srv.Serve(lis)
+ }()
+
+ // 4. 等待信号或错误
+ select {
+ case err := <-errCh:
+ log.Printf("server error: %v", err)
+ return
+ case <-ctx.Done():
+ // 收到 SIGTERM/SIGINT → 开始优雅关闭
+ }
+
+ // 5. 优雅关闭 + 超时兜底
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ done := make(chan struct{})
+ go func() {
+ srv.GracefulStop()
+ close(done)
+ }()
+
+ select {
+ case <-done:
+ log.Println("server stopped gracefully")
+ case <-shutdownCtx.Done():
+ log.Println("shutdown timeout, forcing stop")
+ srv.Stop()
+ }
+}
+```
+
+> [!note] 关键设计细节
+>
+> | 设计点 | 原因 |
+> |--------|------|
+> | `signal.NotifyContext` | Go 1.21+ 推荐用法,比手动 `signal.Ch` 更简洁安全 |
+> | `errCh` 缓冲大小 1 | 防止 goroutine 在 channel 未准备好时 panic |
+> | 两个独立的 `select` | 运行期错误直接退出,不需要走优雅关闭流程 |
+> | `srv.Serve` 放 goroutine | 它本身是阻塞的,不放goroutine会卡住 `select` |
+
+## 执行流程图
+
+```mermaid
+flowchart TD
+ SIGTERM["收到 SIGTERM
信号"] --> StopNew["停止接收新连接
新请求返回 UNAVAILABLE"]
+ StopNew --> Drain["排空活跃 Stream
等待 handler 处理完成"]
+ Drain --> Check{"全部完成?"}
+ Check -->|"是"| CleanExit["干净退出
所有回调执行完毕"]
+ Check -->|"否, 等待中..."--> Wait["等待中..."]
+ Wait --> Check
+ Check -->|"超时 30s"| Force["超时触发
srv.Stop() 强制断开"]
+ Force --> ForcedExit["强制退出
可能丢失未完成请求"]
+
+ style CleanExit fill:#00D866,color:#fff
+ style ForcedExit fill:#EE5A24,color:#fff
+ style Wait fill:#FFEAA7,color:#000
+```
+
+## 最大的坑:Handler 里必须检测 ctx.Done()
+
+`GracefulStop` 能工作的关键前提是:**你的 handler 必须响应 context 取消信号。**
+
+如果 handler 里完全不检 `ctx.Done()`,那 `GracefulStop` 基本等于白写——进程要等到最后一个 handler 跑完才退出。
+
+### 正确的 handler 写法
+
+```go
+func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
+ // 随时检查 ctx,收到取消信号立即返回
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err() // context canceled / deadline exceeded
+ default:
+ // 继续执行业务逻辑
+ }
+
+ // ... 业务逻辑
+
+ // 再次检查(长时间操作的中间点)
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ return &pb.CreateUserResponse{Id: generatedId}, nil
+}
+```
+
+> [!tip] 什么时候该检 ctx?
+>
+> - **调用外部服务时**:gRPC/DHTTP client 默认会传递 parent context,你不需要额外处理
+> - **长时间循环/睡眠中**:在循环体和 sleep 前加 select,避免白白等待
+> - **数据库查询前**:长查询最好在查询前检测一次,查询中也能靠 db timeout 兜底
+
+### 不检测会怎样?
+
+```go
+// ❌ 危险写法
+func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
+ s.db.Exec("INSERT INTO users ...") // 不依赖 ctx,永远不会提前返回
+ time.Sleep(60 * time.Second) // 60 秒后才返回
+ return &pb.CreateUserResponse{Id: "1"}, nil
+}
+```
+
+即使 server 调用了 `GracefulStop()`,这个 handler 也会继续跑完 60 秒才返回。如果多个 handler 都这样,关停时间会线性叠加。
+
+### Streaming Handler 更要小心
+
+Streaming 场景下这个问题更严重——一个 stream 可能是无限循环的 `for { recv() }`,没有自然的退出点:
+
+```go
+// ❌ 永远不退出的 streaming handler
+func (s *orderService) StreamOrders(stream pb.OrderService_StreamOrdersServer) error {
+ for {
+ req, err := stream.Recv() // 永远在等,不会停
+ if err != nil { return err }
+ // 处理...
+ }
+}
+```
+
+```go
+// ✅ 正确写法:结合 ctx 和 stream.Recv
+func (s *orderService) StreamOrders(stream pb.OrderService_StreamOrdersServer) error {
+ for {
+ select {
+ case <-stream.Context().Done():
+ return nil // stream 断开或 ctx 取消 → 安全退出
+ default:
+ }
+
+ req, err := stream.Recv()
+ if err != nil {
+ return err
+ }
+ // 处理...
+ }
+}
+```
+
+> [!caution] Streaming handler 不要只用 Recv 的错误返回值判断退出
+>
+> `Recv()` 返回非 io.EOF 错误时,不代表 client 断开了连接——可能是权限校验失败、消息格式错误等各种原因。只有在收到 cancel 信号(如 `context.Canceled`)时才应该 clean up 资源并退出。而 **正常请求被中断** 正是 GracefulStop 期间最常见的场景。
+
+详见 [[hhs/gRPC/3. 服务端实现/09-Streaming Handler]]。
+
+## 小结
+
+| 要点 | 一句话 |
+|------|--------|
+| 用 `GracefulStop` 而非 `Stop` | 优先优雅退出 |
+| 始终加 30s 超时兜底 | 防止 handler 永远不返回 |
+| handler 里必须检测 `ctx.Done()` | 否则 GracefulStop 形同虚设 |
+| Streaming handler 更要小心 | `for { Recv() }` 模式需要主动 break |
+
+## 关联笔记
+
+- [[hhs/gRPC/3. 服务端实现/09-Streaming Handler]]
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/05-Multi-Port 多端口暴露]]
+- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/03-Service Registration 服务注册详解]]