头图

写给go开发者的gRPC教程-通信模式

本篇为【写给go开发者的gRPC教程系列】第二篇

第一篇:protobuf基础

第二篇:通信模式 👈

第三篇:拦截器

第四篇:错误处理


上一篇介绍了如何编写 protobuf 的 idl,并使用 idl 生成了 gRPC 的代码,现在来看看如何编写客户端和服务端的代码

Simple RPC (Unary RPC)

syntax = "proto3";

package ecommerce;

import "google/protobuf/wrappers.proto";

option go_package = "ecommerce/";

message Order {
  string id = 1;
  repeated string items = 2;
  string description = 3;
  float price = 4;
  string destination = 5;
}

service OrderManagement {
  rpc getOrder(google.protobuf.StringValue) returns (Order);
}

定义如上的 idl,需要关注几个事项

  • 使用protobuf最新版本syntax = "proto3";
  • protoc-gen-go要求 pb 文件必须指定 go 包的路径。即option go_package = "ecommerce/";
  • 定义的method仅能有一个入参和出参数。如果需要传递多个参数需要定义成message
  • 使用import引用另外一个文件的 pb。google/protobuf/wrappers.proto是 google 内置的类型

生成 go 和 grpc 的代码

$ protoc -I ./pb \
  --go_out ./ecommerce --go_opt paths=source_relative \
  --go-grpc_out ./ecommerce --go-grpc_opt paths=source_relative \
  ./pb/product.proto
ecommerce
├── product.pb.go
└── product_grpc.pb.go
pb
└── product.proto

server 实现

1、由 pb 文件生成的 gRPC 代码中包含了 service 的接口定义,它和我们定义的 idl 是吻合的

service OrderManagement {
  rpc getOrder(google.protobuf.StringValue) returns (Order);
}
type OrderManagementServer interface {
    GetOrder(context.Context, *wrapperspb.StringValue) (*Order, error)
    mustEmbedUnimplementedOrderManagementServer()
}

2、我们的业务逻辑就是实现这个接口

package main

import (
    "context"
    "log"

    pb "github.com/liangwt/note/grpc/unary_rpc_example/ecommerce"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "google.golang.org/protobuf/types/known/wrapperspb"
)

var _ pb.OrderManagementServer = &OrderManagementImpl{}

var orders = make(map[string]pb.Order)

type OrderManagementImpl struct {
    pb.UnimplementedOrderManagementServer
}

// Simple RPC
func (s *OrderManagementImpl) GetOrder(ctx context.Context, orderId *wrapperspb.StringValue) (*pb.Order, error) {
    ord, exists := orders[orderId.Value]
    if exists {
        return &ord, status.New(codes.OK, "").Err()
    }

    return nil, status.Errorf(codes.NotFound, "Order does not exist. : ", orderId)
}

3、在实现完业务逻辑之后,我们可以创建并启动服务

package main

import (
    "net"

    pb "github.com/liangwt/note/grpc/unary_rpc_example/ecommerce"
    "google.golang.org/grpc"
)

func main() {
    s := grpc.NewServer()

    pb.RegisterOrderManagementServer(s, &OrderManagementImpl{})

    lis, err := net.Listen("tcp", ":8009")
    if err != nil {
        panic(err)
    }

    if err := s.Serve(lis); err != nil {
        panic(err)
    }
}

服务端代码实现的流程如下

client 实现

1、由 pb 文件生成的 gRPC 代码中包含了 client 的实现,它和我们定义的 idl 也是吻合的

service OrderManagement {
  rpc getOrder(google.protobuf.StringValue) returns (Order);
}
type orderManagementClient struct {
    cc grpc.ClientConnInterface
}

func NewOrderManagementClient(cc grpc.ClientConnInterface) OrderManagementClient {
    return &orderManagementClient{cc}
}

func (c *orderManagementClient) GetOrder(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*Order, error) {
    out := new(Order)
    err := c.cc.Invoke(ctx, "/ecommerce.OrderManagement/getOrder", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

2、直接使用 client 来进行 rpc 调用

package main

import (
    "context"
    "log"
    "time"

    pb "github.com/liangwt/note/grpc/unary_rpc_example/ecommerce"
    "google.golang.org/grpc"
    "google.golang.org/protobuf/types/known/wrapperspb"
)

func main() {
    conn, err := grpc.Dial("127.0.0.1:8009", grpc.WithInsecure())
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    client := pb.NewOrderManagementClient(conn)

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    // Get Order
    retrievedOrder, err := client.GetOrder(ctx, &wrapperspb.StringValue{Value: "101"})
    if err != nil {
        panic(err)
    }

    log.Print("GetOrder Response -> : ", retrievedOrder)
}

客户端代码实现的流程如下

小总结

✨ 前文提到过protobuf协议是平台无关的。演示的客户端和服务端都是 golang 的,即使客户端和服务端不同语言也是类似的可以通信的

✨ 对于上面介绍的的这种类似于http1.x的模式:客户端发送请求,服务端响应请求,一问一答的模式在 gRPC 里叫做Simple RPC (也称Unary RPC)。gRPC 同时也支持其他类型的交互方式。

Simple RPC (Unary RPC)

Server-Streaming RPC 服务器端流式 RPC

服务器端流式 RPC,显然是单向流,并代指 Server 为 Stream 而 Client 为普通 RPC 请求

简单来讲就是客户端发起一次普通的 RPC 请求,服务端通过流式响应多次发送数据集,客户端 Recv 接收数据集。大致如图:

Server-Streaming RPC

pb 定义

syntax = "proto3";

package ecommerce;

option go_package = "ecommerce/";

import "google/protobuf/wrappers.proto";

message Order {
  string id = 1;
  repeated string items = 2;
  string description = 3;
  float price = 4;
  string destination = 5;
}

service OrderManagement {
  rpc searchOrders(google.protobuf.StringValue) returns (stream Order);
}

server 实现

✨ 注意与Simple RPC的区别:因为我们的服务端是流式响应的,因此对于服务端来说函数入参了一个stream OrderManagement_SearchOrdersServer参数用来写入多个响应,可以把它看作是客户端的对象

✨ 可以通过调用这个流对象的Send(...),来往客户端写入数据

✨ 通过返回nil或者error来表示全部数据写完了

func (s *server) SearchOrders(query *wrapperspb.StringValue,
                              stream pb.OrderManagement_SearchOrdersServer) error {
    for _, order := range orders {
        for _, str := range order.Items {
            if strings.Contains(str, query.Value) {
                err := stream.Send(&order)
                if err != nil {
                    return fmt.Errorf("error send: %v", err)
                }
            }
        }
    }

    return nil
}

client 实现

✨ 注意与Simple RPC的区别:因为我们的服务端是流式响应的,因此 RPC 函数返回值stream是一个流,可以把它看作是服务端的对象

✨ 使用streamRecv函数来不断从服务端接收数据

✨ 当Recv返回io.EOF代表流已经结束

c := pb.NewOrderManagementClient(conn)
ctx, cancelFn := context.WithCancel(context.Background())
defer cancelFn()

stream, err := c.SearchOrders(ctx, &wrapperspb.StringValue{Value: "Google"})
if err != nil{
  panic(err)
}

for{
  order, err := stream.Recv()
  if err == io.EOF{
    break
  }

  log.Println("Search Result: ", order)
}

小总结

通信模式-服务端流RPC

Client-Streaming RPC 客户端流式 RPC

客户端流式 RPC,显然也是单向流,客户端通过流式发起多次 RPC 请求给服务端,服务端发起一次响应给客户端,大致如图:

服务端没有必要等到客户端发送完所有请求再响应,可以在收到部分请求之后就响应

Client-Streaming RPC

pb 定义

syntax = "proto3";

package ecommerce;

option go_package = "ecommerce/";

import "google/protobuf/wrappers.proto";

message Order {
  string id = 1;
  repeated string items = 2;
  string description = 3;
  float price = 4;
  string destination = 5;
}

service OrderManagement {
  rpc updateOrders(stream Order) returns (google.protobuf.StringValue);
}

server 实现

✨ 注意与Simple RPC的区别:因为我们的客户端是流式请求的,因此请求参数stream OrderManagement_UpdateOrdersServer就是流对象

✨ 可以从stream OrderManagement_UpdateOrdersServerRecv函数读取消息

✨ 当Recv返回io.EOF代表流已经结束

✨ 使用stream OrderManagement_UpdateOrdersServerSendAndClose函数关闭并发送响应

// 在这段程序中,我们对每一个 Recv 都进行了处理
// 当发现 io.EOF (流关闭) 后,需要将最终的响应结果发送给客户端,同时关闭正在另外一侧等待的 Recv
func (s *server) UpdateOrders(stream pb.OrderManagement_UpdateOrdersServer) error {
    ordersStr := "Updated Order IDs : "
    for {
        order, err := stream.Recv()
        if err == io.EOF {
            // Finished reading the order stream.
            return stream.SendAndClose(
                &wrapperspb.StringValue{Value: "Orders processed " + ordersStr})
        }
        // Update order
        orders[order.Id] = *order

        log.Println("Order ID ", order.Id, ": Updated")
        ordersStr += order.Id + ", "
    }
}

Client 实现

✨ 注意与Simple RPC的区别:因为我们的客户端是流式响应的,因此 RPC 函数返回值stream是一个流

✨ 可以通过调用这个流对象的Send(...),来往这个对象写入数据

✨ 使用streamCloseAndRecv函数关闭并发送响应

c := pb.NewOrderManagementClient(conn)
ctx, cancelFn := context.WithCancel(context.Background())
defer cancelFn()

stream, err := c.UpdateOrders(ctx)
if err != nil {
  panic(err)
}

if err := stream.Send(&pb.Order{
  Id:          "00",
  Items:       []string{"A", "B"},
  Description: "A with B",
  Price:       0.11,
  Destination: "ABC",
}); err != nil {
  panic(err)
}

if err := stream.Send(&pb.Order{
  Id:          "01",
  Items:       []string{"C", "D"},
  Description: "C with D",
  Price:       1.11,
  Destination: "ABCDEFG",
}); err != nil {
  panic(err)
}

res, err := stream.CloseAndRecv()
if err != nil {
  panic(err)
}

log.Printf("Update Orders Res : %s", res)

小总结

通信模式-客户端流RPC

Bidirectional-Streaming RPC 双向流式 RPC

双向流式 RPC,顾名思义是双向流。由客户端以流式的方式发起请求,服务端同样以流式的方式响应请求

首个请求一定是 Client 发起,但具体交互方式(谁先谁后、一次发多少、响应多少、什么时候关闭)根据程序编写的方式来确定(可以结合协程)

假设该双向流是按顺序发送的话,大致如图:

Bidirectional-Streaming RPC

pb 定义

syntax = "proto3";

package ecommerce;

option go_package = "ecommerce/";

import "google/protobuf/wrappers.proto";

message Order {
  string id = 1;
  repeated string items = 2;
  string description = 3;
  float price = 4;
  string destination = 5;
}

message CombinedShipment {
  string id = 1;
  string status = 2;
  repeated Order orderList = 3;
}

service OrderManagement {
  rpc processOrders(stream google.protobuf.StringValue)
      returns (stream CombinedShipment);
}

server 实现

✨ 函数入参OrderManagement_ProcessOrdersServer是用来写入多个响应和读取多个消息的对象引用

✨ 可以通过调用这个流对象的Send(...),来往这个对象写入响应

✨ 可以通过调用这个流对象的Recv(...)函数读取消息,当Recv返回io.EOF代表流已经结束

✨ 通过返回nil或者error表示全部数据写完了

func (s *server) ProcessOrders(stream pb.OrderManagement_ProcessOrdersServer) error {

    batchMarker := 1
    var combinedShipmentMap = make(map[string]pb.CombinedShipment)
    for {
        orderId, err := stream.Recv()
        log.Printf("Reading Proc order : %s", orderId)
        if err == io.EOF {
            log.Printf("EOF : %s", orderId)
            for _, shipment := range combinedShipmentMap {
                if err := stream.Send(&shipment); err != nil {
                    return err
                }
            }
            return nil
        }
        if err != nil {
            log.Println(err)
            return err
        }

        destination := orders[orderId.GetValue()].Destination
        shipment, found := combinedShipmentMap[destination]

        if found {
            ord := orders[orderId.GetValue()]
            shipment.OrderList = append(shipment.OrderList, &ord)
            combinedShipmentMap[destination] = shipment
        } else {
            comShip := pb.CombinedShipment{Id: "cmb - " + (orders[orderId.GetValue()].Destination), Status: "Processed!"}
            ord := orders[orderId.GetValue()]
            comShip.OrderList = append(shipment.OrderList, &ord)
            combinedShipmentMap[destination] = comShip
            log.Print(len(comShip.OrderList), comShip.GetId())
        }

        if batchMarker == orderBatchSize {
            for _, comb := range combinedShipmentMap {
                log.Printf("Shipping : %v -> %v", comb.Id, len(comb.OrderList))
                if err := stream.Send(&comb); err != nil {
                    return err
                }
            }
            batchMarker = 0
            combinedShipmentMap = make(map[string]pb.CombinedShipment)
        } else {
            batchMarker++
        }
    }
}

Client 实现

✨ 函数返回值OrderManagement_ProcessOrdersClient是用来获取多个响应和写入多个消息的对象引用

✨ 可以通过调用这个流对象的Send(...),来往这个对象写入响应

✨ 可以通过调用这个流对象的Recv(...)函数读取消息,当Recv返回io.EOF代表流已经结束

c := pb.NewOrderManagementClient(conn)
ctx, cancelFn := context.WithCancel(context.Background())
defer cancelFn()

stream, err := c.ProcessOrders(ctx)
if err != nil {
  panic(err)
}

go func() {
  if err := stream.Send(&wrapperspb.StringValue{Value: "101"}); err != nil {
    panic(err)
  }

  if err := stream.Send(&wrapperspb.StringValue{Value: "102"}); err != nil {
    panic(err)
  }

  if err := stream.CloseSend(); err != nil {
    panic(err)
  }
}()

for {
  combinedShipment, err := stream.Recv()
  if err == io.EOF {
    break
  }
  log.Println("Combined shipment : ", combinedShipment.OrderList)
}

小总结

双向流相对还是比较复杂的,大部分场景都是使用事件机制进行异步交互,需要精心的设计

通信模式-双向流RPC

示例代码

https://github.com/liangwt/gr...

image.png


写给go开发者的gRPC教程
写给go开发者的gRPC教程
12 声望
4 粉丝
0 条评论
推荐阅读
写给go开发者的gRPC教程-metadata
本篇为【写给go开发者的gRPC教程系列】第五篇第一篇:protobuf基础第二篇:通信模式第三篇:拦截器第四篇:错误处理第五篇:metadata 👈 本系列将持续更新,欢迎关注👏获取实时通知导语和在普通HTTP请求中一样,gRP...

liangwt阅读 249

封面图
前端如何入门 Go 语言
类比法是一种学习方法,它是通过将新知识与已知知识进行比较,从而加深对新知识的理解。在学习 Go 语言的过程中,我发现,通过类比已有的前端知识,可以更好地理解 Go 语言的特性。

robin22阅读 3.2k评论 4

封面图
Golang 中 []byte 与 string 转换
string 类型和 []byte 类型是我们编程时最常使用到的数据结构。本文将探讨两者之间的转换方式,通过分析它们之间的内在联系来拨开迷雾。

机器铃砍菜刀24阅读 57.8k评论 2

年度最佳【golang】map详解
这篇文章主要讲 map 的赋值、删除、查询、扩容的具体执行过程,仍然是从底层的角度展开。结合源码,看完本文一定会彻底明白 map 底层原理。

去去100216阅读 11.5k评论 2

年度最佳【golang】GMP调度详解
Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底的. 这篇文章将通过分析...

去去100215阅读 11.9k评论 4

万字详解,吃透 MongoDB!
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常...

JavaGuide8阅读 1.6k

封面图
数据结构与算法:二分查找
一、常见数据结构简单数据结构(必须理解和掌握)有序数据结构:栈、队列、链表。有序数据结构省空间(储存空间小)无序数据结构:集合、字典、散列表,无序数据结构省时间(读取时间快)复杂数据结构树、 堆图二...

白鲸鱼9阅读 5.2k

12 声望
4 粉丝
宣传栏