Files
cs-note/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server.md
T
2026-05-24 11:42:38 +08:00

7.8 KiB
Raw Blame History

tags, create time
tags create time
gRPC
Go
Server
Beginner
2026-05-13 23:15

Hello World — 最小可运行 Server

概述

这是你搭建 gRPC 服务器的第一行代码。从零开始,用最少的步骤跑通一个能接收请求的 gRPC 服务——不需要 TLS、不需要优雅关闭、不需要拦截器。先把"能跑起来"搞定,再一步步往生产环境逼近。

[!tip] 学习路径建议 本文是「Server 搭建」系列的第一课。后续我们会在这个最小示例上逐步叠加配置,直到达到生产可用。不要跳过——每一篇解决一个实际问题。

三段式模板

不管多复杂的 gRPC 服务器,本质上都是三步:

步骤 做了什么 类比
1. 监听端口 告诉操作系统"我准备在这里接客了" 餐厅挂出"营业中"牌子
2. 注册服务 告诉 server "谁来接待客人" 安排服务员站好位置
3. 启动 Serve 正式开始接客 开门迎客

生命周期时序

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)。

完整代码

// 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 段:监听端口

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 并注册

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

s.Serve(lis)

这一步之后,main 函数会阻塞在这里,持续监听并处理请求——直到进程被杀死或遇到错误。

这意味着:Serve 之后的代码永远不会执行。需要在这之前完成所有初始化工作(日志、数据库连接、协程启动等)。这也是为什么大多数项目最终会把 main() 封装成一个更结构化的启动函数——纯裸跑只适合入门示例。

为什么需要 UnimplementedXxxServer?

很多新手会疑惑:为什么要嵌入一个 UnimplementedUserServiceServer?

原因很简单:Proto 新增方法时,编译期会自动提醒你。

假设你的 proto 文件新增了一个 DeleteUser 方法:

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 快速验证是否工作:

# 列出可用的服务和方法
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 核心术语