第一部分:gRPC 的四种通信模式(通俗版)
在写代码之前,我们必须先理解 gRPC 支持的四种“对话方式”。你可以把它们想象成人与人之间的交流:
| 模式 |
英文名 |
通俗比喻 |
数据流向 |
适用场景 |
| 1. 一元调用 |
Unary RPC |
问答题 客户端问一个问题,服务端答一个结果。 |
1 请求 → 1 响应 |
最常用。如:用户登录、查询余额。 |
| 2. 服务端流 |
Server Streaming RPC |
看直播 客户端点个播放,服务端源源不断发数据。 |
1 请求 → N 响应 |
如:股票行情推送、日志实时流。 |
| 3. 客户端流 |
Client Streaming RPC |
传文件 客户端分块发数据,发完后服务端给个结果。 |
N 请求 → 1 响应 |
如:大文件上传、批量数据导入。 |
| 4. 双向流 |
Bidirectional Streaming RPC |
打电话 双方随时可以说话,互不阻塞。 |
N 请求 ↔ N 响应 |
如:在线聊天室、游戏实时同步。 |
第二部分:环境准备(只需一次)
在开始之前,请确保你的电脑安装了 Go 语言 和 Protobuf 编译器。
安装 Go 插件(用于生成 Go 代码):
1 2
| go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
(确保你的 $GOPATH/bin 在环境变量 PATH 中)
创建项目目录:
1 2 3 4
| mkdir grpc-demo cd grpc-demo go mod init grpc-demo mkdir proto
|
第三部分:定义协议(Proto 文件)
gRPC 强依赖 .proto 文件。为了演示所有模式,我们定义一个包含 4 种方法的服务。
文件路径: proto/demo.proto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| syntax = "proto3";
package demo;
message HelloRequest { string name = 1; } message HelloResponse { string message = 1; }
message CountRequest { int32 start = 1; } message CountResponse { int32 current = 1; }
message SumRequest { int32 num = 1; } message SumResponse { int32 total = 1; }
message ChatMessage { string text = 1; }
service DemoService { rpc SayHello (HelloRequest) returns (HelloResponse); rpc CountDown (CountRequest) returns (stream CountResponse); rpc SumStream (stream SumRequest) returns (SumResponse); rpc Chat (stream ChatMessage) returns (stream ChatMessage); }
|
生成 Go 代码:
在 grpc-demo 根目录下运行:
1 2 3
| protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ proto/demo.proto
|
运行后,proto 目录下会多出 demo.pb.go 和 demo_grpc.pb.go,这就是我们要用的代码。
第四部分:实战 Demo
Demo 1:一元调用 (Unary RPC)
场景:客户端说 “Hello”,服务端回 “Hi”。这是最基础的模式。
1. 服务端 (server_unary.go)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package main
import ( "context" "log" "net" "grpc-demo/proto" "google.golang.org/grpc" )
type Server struct { proto.UnimplementedDemoServiceServer }
func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) { log.Printf("收到请求:%s", req.Name) return &proto.HelloResponse{Message: "Hello, " + req.Name + "!"}, nil }
func main() { lis, _ := net.Listen("tcp", ":50051") s := grpc.NewServer() proto.RegisterDemoServiceServer(s, &Server{}) log.Println("服务端启动在 :50051") s.Serve(lis) }
|
2. 客户端 (client_unary.go)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import ( "context" "log" "grpc-demo/proto" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" )
func main() { conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) defer conn.Close()
client := proto.NewDemoServiceClient(conn)
resp, _ := client.SayHello(context.Background(), &proto.HelloRequest{Name: "小白"}) log.Printf("服务端回复:%s", resp.Message) }
|
运行步骤: 先运行 server,再运行 client。你会看到日志交互。
Demo 2:服务端流 (Server Streaming)
场景:客户端说“从 3 开始倒数”,服务端连续发 3, 2, 1。
1. 服务端 (server_stream.go)
重点:使用 stream.Send() 循环发送。
1 2 3 4 5 6 7 8 9 10
| func (s *Server) CountDown(req *proto.CountRequest, stream proto.DemoService_CountDownServer) error { for i := req.Start; i > 0; i-- { log.Printf("发送倒数:%d", i) stream.Send(&proto.CountResponse{Current: i}) } return nil }
|
2. 客户端 (client_stream.go)
重点:使用 stream.Recv() 循环接收,直到报错(流结束)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| func main() { conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) client := proto.NewDemoServiceClient(conn)
stream, _ := client.CountDown(context.Background(), &proto.CountRequest{Start: 3})
for { resp, err := stream.Recv() if err != nil { break } log.Printf("收到倒数:%d", resp.Current) } }
|
Demo 3:客户端流 (Client Streaming)
场景:客户端发送 1, 2, 3,服务端算出总和 6 返回。
1. 服务端 (server_client_stream.go)
重点:使用 stream.Recv() 循环接收,最后 SendAndClose。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| func (s *Server) SumStream(stream proto.DemoService_SumStreamServer) error { total := 0 for { req, err := stream.Recv() if err != nil { break } total += req.Num log.Printf("累加数字:%d, 当前总和:%d", req.Num, total) } return stream.SendAndClose(&proto.SumResponse{Total: total}) }
|
2. 客户端 (client_client_stream.go)
重点:使用 stream.Send() 发送,最后 CloseAndRecv。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| func main() { conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) client := proto.NewDemoServiceClient(conn)
stream, _ := client.SumStream(context.Background())
nums := []int32{1, 2, 3, 4, 5} for _, n := range nums { stream.Send(&proto.SumRequest{Num: n}) }
resp, _ := stream.CloseAndRecv() log.Printf("服务端计算的总和:%d", resp.Total) }
|
Demo 4:双向流 (Bidirectional Streaming)
场景:聊天室。客户端发一句,服务端回一句,互不干扰。
注意:为了简单演示,这里使用单线程顺序收发。真实聊天通常需要 goroutine 分离收发。
1. 服务端 (server_bidirectional.go)
重点:在一个循环里既 Recv 又 Send。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func (s *Server) Chat(stream proto.DemoService_ChatServer) error { for { req, err := stream.Recv() if err != nil { break } log.Printf("收到消息:%s", req.Text) reply := "Echo: " + req.Text stream.Send(&proto.ChatMessage{Text: reply}) } return nil }
|
2. 客户端 (client_bidirectional.go)
重点:同样需要循环收发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| func main() { conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) client := proto.NewDemoServiceClient(conn)
stream, _ := client.Chat(context.Background())
go func() { for { resp, err := stream.Recv() if err != nil { return } log.Printf("收到回复:%s", resp.Text) } }()
msgs := []string{"你好", "在吗", "再见"} for _, m := range msgs { stream.Send(&proto.ChatMessage{Text: m}) } }
|
第五部分:核心代码对比总结
为了让你一目了然,我把四种模式中 最核心的 API 差异 总结在下面:
| 模式 |
服务端核心方法签名 |
服务端发送/结束 |
客户端核心方法签名 |
客户端发送/结束 |
| 一元 (Unary) |
func(ctx, req) (resp, err) |
直接 return resp |
func(ctx, req) (resp, err) |
直接调用获取 resp |
| 服务端流 |
func(req, stream) error |
stream.Send() return nil |
func(ctx, req) (Stream, err) |
stream.Recv() 直到 io.EOF |
| 客户端流 |
func(stream) error |
stream.Recv() stream.SendAndClose() |
func(ctx) (Stream, err) |
stream.Send() stream.CloseAndRecv() |
| 双向流 |
func(stream) error |
stream.Recv() & stream.Send() return nil |
func(ctx) (Stream, err) |
stream.Send() & stream.Recv() |
给小白的学习建议
- 从一元开始:90% 的业务场景(如登录、下单、查询)都只需要 **一元调用 (Unary)**。先把它彻底搞懂。
- 理解 Stream 对象:流模式的核心在于那个
Stream 对象。它就像一个 管道,你可以往里面塞东西 (Send),也可以从里面拿东西 (Recv)。
- 错误处理:在流模式中,
Recv() 返回 io.EOF 表示流正常结束,其他 error 表示出错了。
- 调试工具:推荐安装 Postman 或 BloomRPC,它们可以像测试 HTTP 接口一样测试 gRPC 接口,不用每次都写客户端代码。
[up主专用,视频内嵌代码贴在这]