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

215 lines
7.8 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, 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 核心术语]]