通信模式简介
一元 RPC
一元 RPC 模式也被称为简单 RPC 模式。在该模式中,当客户端调用服务器端的远程方法时,客户端发送请求至服务器端并获得一个响应,与响应一起发送的还有状态细节以及 trailer 元数据。
在一元 RPC 模式中,gRPC 服务器端和 gRPC 客户端在通信时始终只有一个请求和一个响应,过程如下图所示:
一元 RPC 模式的服务定义如下形式:
rpc addProduct(Product) returns (ProductID);
rpc getProduct(ProductID) returns (Product);
服务端流 RPC
在服务器端流 RPC 模式中,服务器端在接收到客户端的请求消息后,会发回一个响应的序列,这种多个响应所组成的序列也被称为 “流” 。
服务端流式 RPC 除了在得到客户端请求信息后发送回一个应答流之外,在发送完所有应答后,服务端的状态详情(状态码和可选的状态信息)和可选的跟踪元数据被发送回客户端,从而标记流的结束,以此来完成服务端的工作,客户端在接收到所有服务端的应答后也完成了工作。
客户端发起一次普通的 RPC 请求,服务端通过流式响应多次发送数据集,客户端调用 Recv() 方法来接收数据集,过程如下图所示:
服务器端流 RPC 模式的服务定义如下形式:
// 服务端返回流式数据
rpc ServerStream(StreamRequest) returns (stream StreamResponse);
客户端流 RPC
在客户端流 RPC 模式中,客户端会发送多个请求给服务器端,而不再是单个请求,服务端通常(但并不必须)会在接收到客户端所有的请求后发送回一个应答,其中附带有它的状态详情和可选的跟踪数据。但服务器端不一定要等到从客户端接收到所有消息后才发送响应,在接收到流中的一条消息或几条消息之后就发送响应,也可以在读取完流中的所有消息之后再发送响应。
客户端通过流式发起多次 RPC 请求给服务端,服务端发起一次响应给客户端,过程如下图所示:
客户端流 RPC 模式的服务定义如下形式:
// 客户端发送流式数据
rpc ClientStream(stream StreamRequest) returns (StreamResponse);
双向流 RPC
双向流式 RPC 的调用由客户端调用方法来初始化,而服务端则接收到客户端的元数据,方法名和截止时间,服务端可以选择发送回它的初始元数据或等待客户端发送请求。
在双向流 RPC 模式中,由客户端以流式的方式发起请求,服务端同样以流式的方式响应请求。首个请求一定是 Client 发起,但具体交互方式(谁先谁后、一次发多少、响应多少、什么时候关闭)根据程序编写的方式来确定(可以结合协程),假设该双向流是按顺序发送,则过程如下图所示:
双向流 RPC 模式的服务定义如下形式:
// 双向流式数据
rpc SCStream(stream StreamRequest) returns (stream StreamResponse) {};
gRPC 程序示例
一元 RPC
(1)在任意目录下,创建 server 和 client 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 unary.proto
文件,具体的目录结构如下所示:
Unary
├── client
│ └── proto
│ └── unary.proto
└── server
└── proto
└── unary.proto
(2)在 proto 文件夹下的 unary.proto
文件中,写入如下内容:
syntax = "proto3";
package ecommerce;
option go_package="../proto";
service ProductInfo {
rpc addProduct(Product) returns (ProductID);
rpc getProduct(ProductID) returns (Product);
}
message Product {
string id = 1;
string name = 2;
string description = 3;
float price = 4;
}
message ProductID {
string value = 1;
}
(3)为服务端和客户端生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
正确生成后的目录结构如下所示:
Unary
├── client
│ ├── go.mod
│ ├── go.sum
│ └── proto
│ ├── unary_grpc.pb.go
│ ├── unary.pb.go
│ └── unary.proto
└── server
├── go.mod
├── go.sum
└── proto
├── unary_grpc.pb.go
├── unary.pb.go
└── unary.proto
(4)在 server 项目目录下初始化项目( go mod init server
),编写 Server 端程序重写定义的方法,该程序的具体代码如下:
package main
import (
"context"
"log"
"net"
"github.com/gofrs/uuid"
pb "server/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
port = ":50051"
)
// server is used to implement ecommerce/product_info.
type server struct {
productMap map[string]*pb.Product
pb.UnimplementedProductInfoServer
}
// AddProduct implements ecommerce.AddProduct
func (s *server) AddProduct(ctx context.Context,in *pb.Product) (*pb.ProductID, error) {
out, err := uuid.NewV4()
if err != nil {
return nil, status.Errorf(codes.Internal, "Error while generating Product ID", err)
}
in.Id = out.String()
if s.productMap == nil {
s.productMap = make(map[string]*pb.Product)
}
s.productMap[in.Id] = in
log.Printf("Product %v : %v - Added.", in.Id, in.Name)
return &pb.ProductID{Value: in.Id}, status.New(codes.OK, "").Err()
}
// GetProduct implements ecommerce.GetProduct
func (s *server) GetProduct(ctx context.Context, in *pb.ProductID) (*pb.Product, error) {
product, exists := s.productMap[in.Value]
if exists && product != nil {
log.Printf("Product %v : %v - Retrieved.", product.Id, product.Name)
return product, status.New(codes.OK, "").Err()
}
return nil, status.Errorf(codes.NotFound, "Product does not exist.", in.Value)
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterProductInfoServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
(4)在 client 项目目录下初始化项目( go mod init client
),编写 Client 端程序调用服务,该程序的具体代码如下:
package main
import (
"context"
"log"
"time"
pb "client/proto"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewProductInfoClient(conn)
// Contact the server and print out its response.
name := "Apple iPhone 11"
description := "Meet Apple iPhone 11. All-new dual-camera system with Ultra Wide and Night mode."
price := float32(699.00)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.AddProduct(ctx, &pb.Product{Name: name, Description: description, Price: price})
if err != nil {
log.Fatalf("Could not add product: %v", err)
}
log.Printf("Product ID: %s added successfully", r.Value)
product, err := c.GetProduct(ctx, &pb.ProductID{Value: r.Value})
if err != nil {
log.Fatalf("Could not get product: %v", err)
}
log.Printf("Product: %v", product.String())
}
(5)执行 Server 端和 Client 端的程序,分别输出如下的结果:
// Server
2023/02/14 20:09:12 Product 1cc853f1-f772-401b-af7e-37b1929f43ca : Apple iPhone 11 - Added.
2023/02/14 20:09:12 Product 1cc853f1-f772-401b-af7e-37b1929f43ca : Apple iPhone 11 - Retrieved.
// Client
2023/02/14 20:09:12 Product ID: 1cc853f1-f772-401b-af7e-37b1929f43ca added successfully
2023/02/14 20:09:12 Product: id:"1cc853f1-f772-401b-af7e-37b1929f43ca" name:"Apple iPhone 11" description:"Meet Apple iPhone 11. All-new dual-camera system with Ultra Wide and Night mode." price:699
服务端流 RPC
(1)在任意目录下,创建 server 和 client 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 stream.proto
文件,具体的目录结构如下所示:
Stream
├── client
│ └── proto
│ └── stream.proto
└── server
└── proto
└── stream.proto
(2)在 proto 文件夹下的 stream.proto
文件中,写入如下内容:
syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本
option go_package = "../proto"; // 指定生成的 Go 代码在项目中的导入路径
package pb; // 包名
// 定义服务
service StreamService {
rpc ServerStream(StreamRequest) returns (stream StreamResponse) {};
}
// 请求消息
message StreamRequest {
string name = 1;
}
// 响应消息
message StreamResponse {
string reply = 1;
}
为服务端和客户端生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
正确生成后的目录结构如下所示:
Stream
├── client
│ └── proto
│ ├── stream_grpc.pb.go
│ ├── stream.pb.go
│ └── stream.proto
└── server
└── proto
├── stream_grpc.pb.go
├── stream.pb.go
└── stream.proto
(3)在 server 目录下初始化项目( go mod init server
),编写 Server 端程序重写定义的方法,该程序的具体代码如下:
package main
import (
"fmt"
pb "stream_server/proto"
"net"
"google.golang.org/grpc"
)
// server
type server struct {
pb.UnimplementedStreamServiceServer
}
// 返回使用多种语言打招呼
func (s *server) ServerStream(in *pb.StreamRequest, stream pb.StreamService_ServerStreamServer) error {
words := []string{
"你好 ",
"hello ",
"こんにちは ",
"안녕하세요 ",
}
for _, word := range words {
data := &pb.StreamResponse{
Reply: word + in.GetName(),
}
// 使用流引用对象的 Send() 方法将其写入流并通过返回的 nil 来标记流的结束
if err := stream.Send(data); err != nil {
return err
}
}
return nil
}
func main() {
// 监听本地的 8972 端口
lis, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
s := grpc.NewServer() // 创建 gRPC 服务器
pb.RegisterStreamServiceServer(s, &server{}) // 在 gRPC 服务端注册服务
// 启动服务
err = s.Serve(lis)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
(4)在 client 目录下初始化项目( go mod init client
),编写 Client 端程序调用服务,该程序的具体代码如下:
package main
import (
"context"
"flag"
"log"
"time"
"io"
pb "stream_client/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// client
const (
defaultName = "cqupthao!"
)
var (
addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
name = flag.String("name", defaultName, "Name to greet")
)
func runServerStream(c pb.StreamServiceClient) error {
// server 端流式 RPC
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
stream, err := c.ServerStream(ctx, &pb.StreamRequest{Name: *name})
if err != nil {
return err
}
for {
// 接收服务端返回的流式数据调用 Recv() 方法从客户端流中检索消息并持续检索到流结束为止(即收到 io.EOF 或错误时退出)
res, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
log.Printf("gRPC Stream Server: %q\n", res.GetReply())
}
return nil
}
func main() {
flag.Parse()
// 连接到 server 端,此处禁用安全传输
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Do not connect: %v", err)
}
defer conn.Close()
c := pb.NewStreamServiceClient(conn)
// 执行 RPC 调用并打印收到的响应数据
_,cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err = runServerStream(c)
if err != nil{
log.Fatalf("ServerStream.err: %v", err)
}
}
(5)执行 Server 端和 Client 端的程序,输出如下的结果:
// Client
2023/02/14 20:24:54 gRPC Stream Server: "你好 cqupthao!"
2023/02/14 20:24:54 gRPC Stream Server: "hello cqupthao!"
2023/02/14 20:24:54 gRPC Stream Server: "こんにちは cqupthao!"
2023/02/14 20:24:54 gRPC Stream Server: "안녕하세요 cqupthao!"
客户端流 RPC
(1)在任意目录下,创建 server 和 client 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 stream.proto
文件,具体的目录结构如下所示:
Stream
├── client
│ └── proto
│ └── stream.proto
└── server
└── proto
└── stream.proto
(2)在 proto 文件夹下的 stream.proto
文件中,写入如下内容:
syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本
option go_package = "../proto"; // 指定生成的 Go 代码在项目中的导入路径
package pb; // 包名
// 定义服务
service StreamService {
rpc ClientStream(stream StreamRequest) returns (StreamResponse) {};
}
// 请求消息
message StreamRequest {
string name = 1;
}
// 响应消息
message StreamResponse {
string reply = 1;
}
为服务端和客户端生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
正确生成后的目录结构如下所示:
Stream
├── client
│ └── proto
│ ├── stream_grpc.pb.go
│ ├── stream.pb.go
│ └── stream.proto
└── server
└── proto
├── stream_grpc.pb.go
├── stream.pb.go
└── stream.proto
(3)在 server 目录下初始化项目( go mod init server
),编写 Server 端程序重写定义的方法,该程序的具体代码如下:
package main
import (
"io"
"fmt"
pb "stream_server/proto"
"net"
"google.golang.org/grpc"
)
// server
type server struct {
pb.UnimplementedStreamServiceServer
}
// 接收流式数据
func (s *server) ClientStream(stream pb.StreamService_ClientStreamServer) error {
reply := "(Introduction)"
for {
// 接收客户端发来的流式数据
res, err := stream.Recv()
if err == io.EOF {
// 最终统一回复
return stream.SendAndClose(&pb.StreamResponse{
Reply: reply,
})
}
if err != nil {
return err
}
reply += res.GetName()
}
}
func main() {
// 监听本地的 8972 端口
lis, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
s := grpc.NewServer() // 创建 gRPC 服务器
pb.RegisterStreamServiceServer(s, &server{}) // 在 gRPC 服务端注册服务
// 启动服务
err = s.Serve(lis)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
(4)在 client 目录下初始化项目( go mod init client
),编写 Client 端程序调用服务,该程序的具体代码如下:
package main
import (
"context"
"flag"
"log"
"time"
pb "stream_client/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// client
const (
defaultName = "cqupthao!"
)
var (
addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
name = flag.String("name", defaultName, "Name to greet")
)
func runClientStream(c pb.StreamServiceClient) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 客户端流式 RPC
stream, err := c.ClientStream(ctx)
if err != nil {
return err
}
names := []string{"name:cqupthao ", "school:CQUPT ", "country:China"}
for _, name := range names {
// 发送流式数据
err := stream.Send(&pb.StreamRequest{Name: name})
if err != nil {
log.Fatalf("c.StreamClient stream.Send(%v) failed, err: %v", name, err)
}
}
res, err := stream.CloseAndRecv()
if err != nil {
return err
}
log.Printf("gRPC Stream Client: %v", res.GetReply())
return nil
}
func main() {
flag.Parse()
// 连接到 server 端,此处禁用安全传输
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Do not connect: %v", err)
}
defer conn.Close()
c := pb.NewStreamServiceClient(conn)
// 执行 RPC 调用并打印收到的响应数据
_,cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err = runClientStream(c)
if err != nil{
log.Fatalf("ClientStream.err: %v", err)
}
}
(5)执行 Server 端和 Client 端的程序,输出如下的结果:
// Client
2023/02/14 20:31:27 gRPC Stream Client: (Introduction) name:cqupthao school:CQUPT country:China
双向流 RPC
(1)在任意目录下,创建 server 和 client 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 stream.proto
文件,具体的目录结构如下所示:
Stream
├── client
│ └── proto
│ └── stream.proto
└── server
└── proto
└── stream.proto
(2)在 proto 文件夹下的 stream.proto
文件中,写入如下内容:
syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本
option go_package = "../proto"; // 指定生成的 Go 代码在项目中的导入路径
package pb; // 包名
// 定义服务
service StreamService {
rpc SCStream(stream StreamRequest) returns (stream StreamResponse) {};
}
// 请求消息
message StreamRequest {
string name = 1;
}
// 响应消息
message StreamResponse {
string reply = 1;
}
为服务端和客户端生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
正确生成后的目录结构如下所示:
Stream
├── client
│ └── proto
│ ├── stream_grpc.pb.go
│ ├── stream.pb.go
│ └── stream.proto
└── server
└── proto
├── stream_grpc.pb.go
├── stream.pb.go
└── stream.proto
(3)在 server 目录下初始化项目( go mod init server
),编写 Server 端程序重写定义的方法,该程序的具体代码如下:
package main
import (
"io"
"strings"
"fmt"
pb "stream_server/proto"
"net"
"google.golang.org/grpc"
)
// server
type server struct {
pb.UnimplementedStreamServiceServer
}
// 双向流式打招呼,StreamService_SCStreamServer 参数是客户端和服务端之间消息流的引用对象
func (s *server) SCStream(stream pb.StreamService_SCStreamServer) error {
for {
// 接收流式请求
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
reply := magic(in.GetName()) // 对收到的数据做些处理
// 返回流式响应
if err := stream.Send(&pb.StreamResponse{Reply: reply}); err != nil {
return err
}
}
}
// magic 一段价值连城的“人工智能”代码
func magic(s string) string {
s = strings.ReplaceAll(s, "吗", "")
s = strings.ReplaceAll(s, "吧", "")
s = strings.ReplaceAll(s, "你", "我")
s = strings.ReplaceAll(s, "?", "!")
s = strings.ReplaceAll(s, "?", "!")
return s
}
func main() {
// 监听本地的 8972 端口
lis, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
s := grpc.NewServer() // 创建 gRPC 服务器
pb.RegisterStreamServiceServer(s, &server{}) // 在 gRPC 服务端注册服务
// 启动服务
err = s.Serve(lis)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
(4)在 client 目录下初始化项目( go mod init client
),编写 Client 端程序调用服务,该程序的具体代码如下:
package main
import (
"context"
"flag"
"log"
"time"
"io"
"os"
"fmt"
"strings"
"bufio"
pb "stream_client/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// client
const (
defaultName = "cqupthao!"
)
var (
addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
name = flag.String("name", defaultName, "Name to greet")
)
func runSCStream(c pb.StreamServiceClient) error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
// 双向流模式
stream, err := c.SCStream(ctx)
if err != nil {
return err
}
waitc := make(chan struct{})
go func() {
for {
// 接收服务端返回的响应
in, err := stream.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
log.Fatalf("c.SCStream stream.Recv() failed, err: %v", err)
}
fmt.Println("Server Response:", in.GetReply())
}
}()
// 从标准输入获取用户输入
reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
fmt.Println("Please input the message and end with an enter!")
for {
cmd, _ := reader.ReadString('\n') // 读到换行
cmd = strings.TrimSpace(cmd)
if len(cmd) == 0 {
continue
}
if strings.ToUpper(cmd) == "QUIT" {
break
}
// 将获取到的数据发送至服务端
if err := stream.Send(&pb.StreamRequest{Name: cmd}); err != nil {
log.Fatalf("c.SCStream stream.Send(%v) failed: %v", cmd, err)
}
}
stream.CloseSend()
<-waitc
return nil
}
func main() {
flag.Parse()
// 连接到 server 端,此处禁用安全传输
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Do not connect: %v", err)
}
defer conn.Close()
c := pb.NewStreamServiceClient(conn)
// 执行 RPC 调用并打印收到的响应数据
_,cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err = runSCStream(c)
if err != nil {
log.Fatalf("SCStream.err: %v ", err)
}
}
(5)执行 Server 端和 Client 端的程序并输入内容,输出如下的结果:
Please input the message and end with an enter!
你好吗
Server Response: 我好
在吗
Server Response: 在
走吧
Server Response: 走
go?
Server Response: go!
在 Server 端的 Send() 方法在 protoc 在生成时,根据定义生成了各式各样符合标准的接口方法,最终再统一调度内部的 SendMsg() 方法,该方法涉及以下过程:
-
消息体(对象)序列化;
-
压缩序列化后的消息体;
-
对正在传输的消息体增加 5 个字节的 header ;
-
判断压缩 + 序列化后的消息体总字节长度是否大于预设的 maxSendMessageSize(预设值为 math.MaxInt32 ),若超出则提示错误写入给流的数据集。
在 Client 端的 Recv() 方法调用 RecvMsg() 方法会从流中读取完整的 gRPC 消息体,RecvMsg() 方法是阻塞等待的,当流成功或结束时会返回 io.EOF
;当流出现任何错误时,流会被中止,错误信息会包含 RPC 错误码,而在 RecvMsg() 方法中可能出现如下错误:
-
io.EOF
-
io.ErrUnexpectedEOF
-
transport.ConnectionError
-
google.golang.org/grpc/codes
同时默认的 MaxReceiveMessageSize 值为 1024 _ 1024 _ 4 ,建议不要超出此值。