一个恋爱小故事告诉你什么是gRPC?!

RPC#

对 RPC 不了解的人,或许会纠结其与 TCP、HTTP 等的关系。后者是网络传输中的协议,而 RPC 是一种设计、实现框架,通讯协议只是其中一部分,RPC 不仅要解决协议通讯的问题,还有序列化与反序列化,以及消息通知。

一个完整的 RPC 架构里面包含了四个核心的组件,分别是 Client ,Server,ClientOptions 以及 ServerOptions,这个 Options 就是 RPC 需要设计实现的东西。

  • 客户端(Client):服务的调用方。

  • 服务端(Server):真正的服务提供方。

  • 客户端存根(ClientOption):socket 管理,网络收发包的序列化。

  • 服务端存根(ServerOption):socket 管理,提醒 server 层 rpc 方法调用,以及网络收发包的序列化。

RPC 的逻辑示意图如下

\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsHlMmU3-1612838576573)(./image-20210208113707741.png)\]

什么是 gRPC#

gRPC 是 RPC 的一种,它使用 Protocol Buffer (简称 Protobuf) 作为序列化格式,Protocol Buffer 是来自 google 的序列化框架,比 Json 更加轻便高效,同时基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特性。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。用 protoc 就能使用 proto 文件帮助我们生成上面的 option 层代码。

在 gRPC 中,客户端应用程序可以直接在另一台计算机上的服务器应用程序上调用方法,就好像它是本地对象一样,从而使您更轻松地创建分布式应用程序和服务。

gRPC 的调用模型如下

\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XGtHTgq9-1612838576577)(./grpc_concept_diagram_00.png)\]

适用场景#

  • 分布式场景 :gRPC 设计为低延迟和高吞吐量通信,非常适用于效率至关重要的轻型微服务。
  • 点对点实时通信: gRPC 对双向流媒体提供出色的支持,可以实时推送消息而无需轮询。
  • 多语言混合开发 :支持主流的开发语言,使 gRPC 成为多语言开发环境的理想选择。
  • 网络受限环境 : 使用 Protobuf(一种轻量级消息格式)序列化 gRPC 消息。gRPC 消息始终小于等效的 JSON 消息。

四种调用方式#

学习 gRPC 使用之前,先介绍一下 RPC 中的客户端与服务端。在 RPC 中,服务端会开启服务供客户端调用,每一句 RPC 调用都是一次客户端发请求到服务器获得相应的过程,中间过程被封装了,看起来像本地的一次调用一样,一次 RPC 调用也就是一次通讯过程。

RPC 调用通常根据双端是否流式交互,分为了单项 RPC、服务端流式 RPC、客户端流式 RPC、双向流 PRC 四种方式。为了便于大家理解四种 grpc 调用的应用场景,这里举一个例子,假设你是小超,有一个女朋友叫婷婷,婷婷的每种情绪代表一个微服务,你们之间的每一次对话可以理解为一次 PRC 调用,为了便于画流程图,RPC 请求被封装成 client.SayHello,请求包为 HelloRequest,响应为 HelloReply。

1. 单项 RPC#

当你在等婷婷回去吃饭,婷婷在加班时,你们之间的 rpc 调用可能是这样的:

小超:回来吃饭吗

婷婷:还在加班

这就是单项 RPC,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。

\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2VIawkZ-1612838576579)(./image-20210208113746442.png)\]

  • client 层调用 SayHello 接口,把 HelloRequest 包进行序列化
  • client option 将序列化的数据发送到 server 端
  • server option 接收到 rpc 请求
  • 将 rpc 请求返回给 server 端,server 端进行处理,将结果给 server option
  • server option 将 HelloReply 进行序列化并发给 client
  • client option 做反序列化处理,并返回给 client 层
2. 服务端流式 RPC#

当你比赛输了给婷婷发消息时:

小超:今天比赛输了

婷婷:没事,一次比赛而已

婷婷:晚上带你去吃好吃的

这就是服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。

\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-notxon7k-1612838576581)(./image-20210208114053248.png)\]

  • client 层调用 SayHello 接口,把 HelloRequest 包进行序列化
  • client option 将序列化的数据发送到 server 端
  • server option 接收到 rpc 请求
  • 将 rpc 请求返回给 server 端,server 端进行处理,将将数据流给 server option
  • server option 将 HelloReply 进行序列化并发给 client
  • client option 做反序列化处理,并返回给 client 层
3. 客户端流式 RPC#

当你惹婷婷生气的时候:

小超:怎么了,宝贝

小超:别生气了,带你吃好吃的

婷婷:滚

客户端流式 RPC,即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答,

\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6EP2uFmD-1612838576583)(./image-20210208114110968.png)\]

  • client 层调用 SayHello 接口,把 HelloRequest 包进行序列化
  • client option 将序列化的数据流发送到 server 端
  • server option 接收到 rpc 请求
  • 将 rpc 请求返回给 server 端,server 端进行处理,将结果给 server option
  • server option 将 HelloReply 进行序列化并发给 client
  • client option 做反序列化处理,并返回给 client 层
4. 双向流 RPC#

当你哄好婷婷时:

小超:今天看了一个超好看的视频

婷婷:什么视频

小超:发给你看看

婷婷:这也叫好看?

双向流 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持

\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iyp22RQm-1612838576585)(./image-20210208114138744.png)\]

这幅图就不做流程介绍了,读者可以自己试着看图能不能理解过程,相信理解了客户端流 RPC 和服务端流 RPC 俩种方式,这里一定可以理解的。

gPRC 代码实现#

gRPC 使用 Protobuf 作为序列化格式,Protobuf 比 Json 更加轻便高效。与 Json 一样,它与开发语言和平台无关,具有良好的可扩展性。关于 Protobuf 使用请参考官网地址 developers.google.com/protocol-buf...

下面我们实现 Go 语言版的四种 gRPC 调用方式。

1. 单向 RPC 实现#
编写 proto#
//proto3标准
syntax = "proto3";

//包名
package helloworld;

//定义rpc接口
service Greets {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

//HelloReply协议内容
message HelloReply {
  string name = 1;
  string message = 2;
}

//HelloRequest协议
message HelloRequest {
  string name = 1;
  string message = 2;
}
  1. Greet 为定义 rpc 服务的类名,rpc SayHello (HelloRequest) returns (HelloReply) {} 表示定义 rpc 方法 SayHello,传入 HelloRequest,返回 HelloReply
  2. 进入 proto 文件夹,运行命令 protoc -I . –go_out=plugins=grpc:. ./helloworld.proto 在。目录中生成 helloworld.pb.go 文件
编写 server#
type Server struct {
}

//实现SayHello接口
func (s *Server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Println(in.Name, in.Message)
    return &pb.HelloReply{Name: "婷婷", Message: "不回来了"}, nil
}

func main() {
    //协议类型以及ip,port
    lis, err := net.Listen("tcp", ":8002")
    if err != nil {
        fmt.Println(err)
        return
    }

    //定义一个rpc的server
    server := grpc.NewServer()
    //注册服务,相当与注册SayHello接口
    pb.RegisterGreetsServer(server, &Server{})
    //进行映射绑定
    reflection.Register(server)

    //启动服务
    err = server.Serve(lis)
    if err != nil {
        fmt.Println(err)
        return
    }
}
  1. pb 为 proto 文件生成的文件别名

  2. 定义 server 结构体作为 rpc 调用的结构体,这个结构体必须实现 SayHello 这个接口

  3. listen -> grpc.NewServer() -> pb.RegisterGreetsServer(server, &Server{}) -> s.Serve(lis)

编写 client#
func main() {
    //创建一个grpc连接
    conn, err := grpc.Dial("localhost:8002", grpc.WithInsecure())
    if err != nil {
        fmt.Println(err)
        return
    }
    defer conn.Close()

    //创建RPC客户端
    client := pb.NewGreetsClient(conn)
    //设置超时时间
    _, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    // 调用方法
    reply, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "小超", Message: "回来吃饭吗"})
    if err != nil {
        log.Fatalf("couldn not greet: %v", err)
    }
    log.Println(reply.Name, reply.Message)
}
  1. grpc.Dial (“localhost:8002”, grpc.WithInsecure ()) 连接到服务器,grpc.WithInsecure () 取消明文检测
  2. context.WithTimeout (context.Background (), time.Second) 设置超时时间
  3. c := pb.NewGreetsClient (conn) 创建 rpc 调用的客户端
  4. c.SayHello (context.Background (), &pb.HelloRequest {Name: name}) 进行 rpc 调用
抽象接口#

其实也就是要实现这个接口,因为俩边都是单项调用,所以调用和实现的接口都是这个

type GreetsClient interface {
   SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
2. 服务端流 RPC#
编写 proto#
//proto3标准
syntax = "proto3";

//包名
package helloworld;

//定义rpc接口

service Greet{
  rpc SayHello (HelloRequest) returns(stream HelloReply) {}
}

//HelloReply协议内容
message HelloReply {
  string name = 1;
  string message = 2;
}

//HelloRequest协议
message HelloRequest {
  string name = 1;
  string message = 2;
}

相比于单项 RPC 调用,因为是客户端流,所以在 HelloRequest 多了一个 stream

编写 server#
type Server struct {
}

//实现rpc接口
func (*Server) SayHello(request *pb.HelloRequest, server pb.Greet_SayHelloServer) error {
    fmt.Println(request)
    var err error
    for i := 0; i < 2; i++ {
        if i == 0 {
            err = server.Send(&pb.HelloReply{Name: "小超", Message: "没事,一次比赛而已"})
        } else {
            err = server.Send(&pb.HelloReply{Name: "小超", Message: "晚上带你去吃好吃的"})
        }
        if err != nil {
            fmt.Println(err)
            return err
        }
    }
    return nil
}

func main() {
    //协议类型以及ip,port
    listen, err := net.Listen("tcp", ":8002")
    if err != nil {
        fmt.Println(err)
        return
    }

    //定义一个rpc的server
    s := grpc.NewServer()
    //注册服务,相当与注册SayHello接口
    pb.RegisterGreetServer(s, &Server{})
    //进行映射绑定
    reflection.Register(s)

    //启动服务
    err = s.Serve(listen)
    if err != nil {
        fmt.Println(err)
    }
}
编写 client#

client 发送的是一个流,与单项 RPC 方式不同,他通过 rpc 调用获得的是一个流传输对象 greetClient,可以用流传输对象不停的往对端发送数据

func main() {
    //创建一个grpc的连接
    grpcConn, err := grpc.Dial("127.0.0.1"+":8002", grpc.WithInsecure())
    if err != nil {
        fmt.Println(err)
        return
    }

    //创建grpc的client
    client := pb.NewGreetClient(grpcConn)
    //设置超时时间
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    //调用rpc方法,获得流接口
    res, err := client.SayHello(ctx, &pb.HelloRequest{Name: "小超", Message: "今天比赛输了"})
    if err != nil {
        fmt.Println(err)
        return
    }

    //循环接收数据
    for {
        recv, err := res.Recv()
        if err != nil {
            fmt.Println(err)
            break
        }
        fmt.Println(recv)
    }
}
抽象接口#

服务器要实现的接口

// GreetsServer is the server API for Greets service.
type GreetsServer interface {
   SayHello(Greets_SayHelloServer) error
}

客户端调用的接口

type GreetsClient interface {
   SayHello(ctx context.Context, opts ...grpc.CallOption) (Greets_SayHelloClient, error)
}
3. 服务端流 RPC#
编写 proto#
//proto3标准
syntax = "proto3";

//包名
package helloworld;

//定义rpc接口
service Greets{
  rpc SayHello (stream HelloRequest) returns (HelloReply) {}
}

//HelloReply协议内容
message HelloReply {
  string name = 1;
  string message = 2;
}

//HelloRequest协议
message HelloRequest {
  string name = 1;
  string message = 2;
}
编写服务器#
type Server struct{}

//实现rpc方法,直到对端调用CloseAndRecv就会读到EOF
func (*Server) SayHello(in pb.Greets_SayHelloServer) error {
    for {
        recv, err := in.Recv()
        //接收完数据之后发送响应
        if err == io.EOF {
            err := in.SendAndClose(&pb.HelloReply{Name: "婷婷", Message: "滚"})
            if err != nil {
                return err
            }
            return nil
        } else if err != nil {
            return err
        }
        fmt.Println(recv)
    }
}

func main() {
    //绑定协议,ip以及端口
    lis, err := net.Listen("tcp", ":8002")
    if err != nil {
        fmt.Println("failed to listen: %v", err)
        return
    }

    //创建一个grpc服务对象
    server := grpc.NewServer()
    //注册rpc服务
    pb.RegisterGreetsServer(server, &Server{})
    //注册服务端反射
    reflection.Register(server)

    //启动服务器
    err = server.Serve(lis)
    if err != nil {
        fmt.Println(err)
        return
    }
}
编写客户端#
func main() {
    //创建一个grpc的连接
    grpcConn, err := grpc.Dial("127.0.0.1"+":8002", grpc.WithInsecure())
    if err != nil {
        fmt.Println(err)
        return
    }

    //创建grpc的client
    client := pb.NewGreetsClient(grpcConn)

    //设置超时时间
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    //调用rpc方法,得到一个客户端用于循环发送数据
    greetClient, err := client.SayHello(ctx)

    if err != nil {
        fmt.Println("sayHello error")
        fmt.Println(err)
        return
    }

    maxCount := 2
    curCount := 0

    //循环发送
    //调了CloseAndRecv()服务端就会读到EOF,server端可根据是否读到EOF来判断客户端是否将数据发完
    for {
        if curCount == 0 {
            err = greetClient.Send(&pb.HelloRequest{Name: "小超", Message: "怎么了,宝贝"})
        } else {
            err = greetClient.Send(&pb.HelloRequest{Name: "小超", Message: "别生气了,带你吃好吃的"})
        }

        if err != nil {
            fmt.Println("send error")
            fmt.Println(err)
            return
        }
        curCount += 1
        if curCount >= maxCount {
            res, err := greetClient.CloseAndRecv()
            if err != nil {
                fmt.Println(err)
                break
            }
            fmt.Println(res)
            break
        }
    }
}
抽象接口#

客户端接口

type GreetsClient interface {
   SayHello(ctx context.Context, opts ...grpc.CallOption) (Greets_SayHelloClient, error)
}

服务器接口

// GreetsServer is the server API for Greets service.
type GreetsServer interface {
   SayHello(Greets_SayHelloServer) error
}

双向流 RPC#

双向流 RPC 就交给读者自己练习吧,相信理解了单项 RPC,客户端流 RPC,服务端流 RPC 三种传输方式,写出双向流 RPC 应该没任何问题。

实现总结#

其实弄懂了单项 RPC、服务端流式 RPC、客户端流式 RPC、双向流 PRC 四种 grpc 应用场景,实现起来非常容易

  1. 根据应用场景选择好哪种 gRPC 服务
  2. 写好 proto 文件,用 protoc 生成.pb.go 文件
  3. 服务端实现接口 ->listen -> grpc.NewServer () -> pb.RegisterGreetsServer (server, &Server {}) -> s.Serve (lis)
  4. 客户端 grpc.Dial->pb.NewGreetsClient->context.WithTimeout->client.SayHello (调用接口)-> 如果是流传输则循环读取数据

欢迎关注我的公众号,查看超超后续内容更新!
在这里插入图片描述

本作品采用《CC 协议》,转载必须注明作者和本文链接