grpc通信模式

第一部分: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 编译器

  1. 安装 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 中)

  2. 创建项目目录

    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;

// 1. 一元调用:简单的请求和响应
message HelloRequest { string name = 1; }
message HelloResponse { string message = 1; }

// 2. 服务端流:请求简单,响应是流
message CountRequest { int32 start = 1; }
message CountResponse { int32 current = 1; }

// 3. 客户端流:请求是流,响应简单
message SumRequest { int32 num = 1; }
message SumResponse { int32 total = 1; }

// 4. 双向流:请求和响应都是流
message ChatMessage { string text = 1; }

service DemoService {
// 一元:普通调用
rpc SayHello (HelloRequest) returns (HelloResponse);

// 服务端流:stream 关键字在 returns 后面
rpc CountDown (CountRequest) returns (stream CountResponse);

// 客户端流:stream 关键字在参数前面
rpc SumStream (stream SumRequest) returns (SumResponse);

// 双向流:两边都有 stream
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.godemo_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
}

// 实现 SayHello 方法
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() {
// 1. 连接
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()

// 2. 获取客户端桩
client := proto.NewDemoServiceClient(conn)

// 3. 调用
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)
// 关键 API:发送流数据
stream.Send(&proto.CountResponse{Current: i})
// 模拟耗时
// time.Sleep(1 * time.Second)
}
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 {
// 关键 API:接收流数据
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 {
// 关键 API:接收客户端发来的流
req, err := stream.Recv()
if err != nil {
break
}
total += req.Num
log.Printf("累加数字:%d, 当前总和:%d", req.Num, total)
}
// 关键 API:发送最终结果并关闭流
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})
}

// 关键 API:关闭发送并获取最终响应
resp, _ := stream.CloseAndRecv()
log.Printf("服务端计算的总和:%d", resp.Total)
}

Demo 4:双向流 (Bidirectional Streaming)

场景:聊天室。客户端发一句,服务端回一句,互不干扰。
注意:为了简单演示,这里使用单线程顺序收发。真实聊天通常需要 goroutine 分离收发。

1. 服务端 (server_bidirectional.go)
重点:在一个循环里既 RecvSend

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})
// time.Sleep(1 * time.Second)
}

// 防止主程序立即退出,实际项目中通常有信号监听
// time.Sleep(2 * time.Second)
}

第五部分:核心代码对比总结

为了让你一目了然,我把四种模式中 最核心的 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()

给小白的学习建议

  1. 从一元开始:90% 的业务场景(如登录、下单、查询)都只需要 **一元调用 (Unary)**。先把它彻底搞懂。
  2. 理解 Stream 对象:流模式的核心在于那个 Stream 对象。它就像一个 管道,你可以往里面塞东西 (Send),也可以从里面拿东西 (Recv)。
  3. 错误处理:在流模式中,Recv() 返回 io.EOF 表示流正常结束,其他 error 表示出错了。
  4. 调试工具:推荐安装 PostmanBloomRPC,它们可以像测试 HTTP 接口一样测试 gRPC 接口,不用每次都写客户端代码。
[up主专用,视频内嵌代码贴在这]