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