Files
cs-note/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server.md
T

215 lines
7.8 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
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 核心术语]]