深入解析grpc源码1-grpc介绍及使用

1grpc 介绍

在grpc 官网上,grpc 是这样介绍的:

开源高性能远程 调用(RPC)框架,可以在任何环境中运行。它支持可插拔的负载均衡、链路跟踪、健康检查和身份验证等等商业和安全功能

grpc 是一项进程间通信技术,可以用来连接、调用、操作和调试构建分布式程序,调用过程如同调用函数一样,整个过程操作起来很简单,就像调用本地方法一样。与许多rpc 系统一样,grpc 是定义服务的思想,服务器需要实现此接口并运行grpc 来处理客户端调用。

gRPC客户端和服务器可以在各种环境中运行并相互通信,并且可以用gRPC支持的任何语言编写。因此,可以轻松地用Java创建一个gRPC服务器,客户端使用Go、Python或Ruby。

使用grpc 构建应用,定义服务接口,需要定义远程调用的服务及方法,以及这些服务调用的参数及消息格式,所使用的语言叫作接口定义语言 (interface defìnition language , IDL).

grpc 默认使用 Protocol Buffers 作为 IDL 定义服务接口。 Protocol Buffers 是中立的、与平台无关、实现结构化数据序列化的可扩展机制,虽然也可以使用json,但是 Protocol Buffers 是二进制编解码,所以编解码性能比Json好。服务接口定义在 proto 文件中指定,也就是在扩展名为 ". proto" 的普通文本文件中。我们要按照普通的 Protocol Buffers 范式来定义grpc服务,并将方法参数和返回类型指定为 Protocol buffers 的格式,也因为服务定义是Protocol Buffers 规范的扩展,所以可以借助特殊的 gRPC 插件来根据 proto 文件生成代码。

1.1grpc 的来源

  • grpc 来源其内部RPC 框架Stubby,并于2015开源,后来加入了CNCF。
  • 在1.1版本grpc, 'g'代表“good",1.2 版本代表”green"绿色

1.2 grpc 优势

  • 高效的进程间通信
    没有使用类似json和xml 的文本语言,而是采用二进制的Protocol Buffers
  • 双工流
    尽管也是类似RESTful 的请求响应模式,但是却提供了steam 流式数据通信
  • 成熟,稳定,并且内置商业化特性,经过了大量大型开源项目的验证,如docker 、etcd 等等

1.3grpc 缺点

  • grpc 生态相对于RESTful 还是比较小,因为浏览器和移动端对grpc支持 依然在初级阶段
  • grpc 不太直接适合面向外部通信,强类型来说有更多约束,向外提供接口的解决方案是配合网关使用

2grpc 使用

2.1 使用grpc 实现远程hello world

1、安装protoc 工具

//安装proto-gen-go
go install github.com/golang/protobuf/protoc-gen-go@latest //go 1.17 后只能这样安装了,带上@version
//安装protoc,去官方下载适合自己平台的并安装,下载完成后将bin目录的protoc 复制到$GOPATH/bin/
https://github.com/protocolbuffers/protobuf/releases

下载后解压,我这里下载windows版本,二进制在bin 里面,同时注意include 这个目录,这个目录在讲protobuf 的时候会提到它的作用,先不要删

观察$GOPATH是否有两个工具,protoc和proto-gen-go

2、创建项目,结构为

  • client -客户端
  • protos -proto 协议定义目录
  • server-服务端代码

3 、定义proto协议

syntax = "proto3";
option go_package = "protos/pbs";
service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}
​
message HelloRequest {
  string greeting = 1;
}
​
message HelloResponse {
  string reply = 1;
}
​

4、编译

D:\Output\grpc\demo\1helloworld>protoc --go_out=. protos/*.proto

可见protos 下面生成了pbs/hello.pb.go

这样是生成了proto 文件,但是还不能进行grpc 通信,因为没有生成grpc 服务,所以我们添加grpc插件重新编译下

D:\Output\grpc\demo\1helloworld>protoc --go_out=plugins=grpc:. protos/*.proto

5、写一个服务器代码

package main
​
import (
    "context"
    "fmt"
    "google.golang.org/grpc"
    "grpcdemo/1helloworld/protos/pbs"
    "log"
    "net"
)
​
type HelloServer struct {
​
}
​
func (h *HelloServer)SayHello(ctx context.Context, req*pbs.HelloRequest) 
(res*pbs.HelloResponse,err error)  {
    //接收到客户到来的消息
    fmt.Println(req.Greeting)
    //响应消息
    res =&pbs.HelloResponse{
        Reply: "you too!",
    }
    return
}
func main()  {
    lis, err := net.Listen("tcp", ":9528") //新建一个listen 
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer() //创建一个grpc 服务
    pbs.RegisterHelloServiceServer(s, &HelloServer{}) //注册该服务
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil { //启动该服务
        log.Fatalf("failed to serve: %v", err)
    }
}
​

6、写一个客户端代码

package main
​
import (
    "context"
    "google.golang.org/grpc"
    "grpcdemo/1helloworld/protos/pbs"
    "log"
​
    "time"
)
​
func main() {
    //与服务器建立连接
    conn, err := grpc.Dial("127.0.0.1:9528", grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pbs.NewHelloServiceClient(conn)
    //设置超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    //向服务器发送消息
    r, err := c.SayHello(ctx, &pbs.HelloRequest{Greeting:"you are son of bitch!"})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.GetReply())
}
​

7、验证hello world

服务器启动:

D:\Output\grpc\demo\1helloworld\server>go run server.go

客户端启动后:

2021/11/15 23:27:27 Greeting: you too!

服务器收到客户端发的消息,并且客户端也收到了服务器响应的消息

D:\Output\grpc\demo\1helloworld\server>go run server.go
2021/11/15 23:26:16 server listening at [::]:9528
you are son of bitch!

至此一个简单的grpc 客户端和服务器实现了,但是对于许多新手来说,可能会有很多疑惑?

  • protoc 命令怎么使用?
  • protobuf 怎么定义?
  • grpc 服务器和客户端怎么使用?

这个先不要急,后面将会一一进行详细解答,下面来看看grpc 其他几种通信方式

2.2 grpc常用几种通信模式

前面介绍grpc 不同于RESTful 通信方式,除了支持简单的一问一答,还支持steam 模式通信,可以理解为tcp 的数据流,只不过更加人性化,可读化。下面来看看几种通信模式。

2.2.1一元rpc 模式

前面介绍的hello world 属于简单一元rpc模式,类似http 协议一问一答,这里就不再举例了

2.2.2服务端数据流模式

这种流模式可以理解为,服务器向客户端源源不断的发送数据流,应用场景很多,比如游戏中定时任务或者其他事件造成玩家数据变化需要将数据推送给客户端。

一元rpc模式下,grpc服务器端和 grpc 客户端在通信时始终只有 一个请求和 一个响应。在服务器端流rpc 模式下,服务端接收到一个请求后发送多个响应组成的序列,在服务器发送所有响应消息完毕后,发送trailer 元数据给客户端,标识流结束。

下面来看一个例子:游戏发送多条资源变化信息给玩家

prop_update.proto

syntax = "proto3";
option go_package = "protos/pbs";
service PropService {
  rpc UserProp (UserPropRequest) returns (stream UserPropResponse);
}
​
//使用道具
message UserPropRequest {
  string Id = 1; //道具配置id
  int64 Count  = 2; //道具数量
​
}
//使用道具响应
message UserPropResponse {
​
}
//道具变化推送
message PropChangePush{
  string PropId = 1; //道具id
  int64 Count  = 2; //道具总数量
}
//资源变化推送
message ResourcesPush{
  string ResId = 1; //道具id
  int64 Count  = 2; //道具总数量
}

server.go

package main
​
import (
   "fmt"
   "google.golang.org/grpc"
   "grpcdemo/2server_stream/protos/pbs"
   "log"
   "net"
)
​
type PropServer struct {
​
}
​
func (h *PropServer)UserProp(req*pbs.UserPropRequest, stream pbs.PropService_UserPropServer) error{
   if req.Count<=0{
      req.Count=1 //错误处理,防止作弊
   }
   fmt.Println(req)
   err:=stream.Send(&pbs.UserPropResponse{})
   if err!=nil{
      panic(err)
   }
   //假设道具减少,资源增加了
   err=stream.SendMsg(&pbs.PropChangePush{
      PropId: req.Id,
      Count: 0,
   })
   if err!=nil{
      panic(err)
   }
   err=stream.SendMsg(&pbs.ResourcesPush{
      ResId: "res_1",
      Count: 100,
   })
   if err!=nil{
      panic(err)
   }
   return nil
}
func main()  {
   lis, err := net.Listen("tcp", ":9528")
   if err != nil {
      log.Fatalf("failed to listen: %v", err)
   }
   s := grpc.NewServer()
   pbs.RegisterPropServiceServer(s, &PropServer{})
   log.Printf("server listening at %v", lis.Addr())
   if err := s.Serve(lis); err != nil {
      log.Fatalf("failed to serve: %v", err)
   }
}

client.go

package main
​
import (
    "context"
    "google.golang.org/grpc"
    "grpcdemo/2server_stream/protos/pbs"
    "io"
    "log"
​
    "time"
)
​
func main() {
​
    conn, err := grpc.Dial("127.0.0.1:9528", grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pbs.NewPropServiceClient(conn)
​
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    stream, err := c.UserProp(ctx, &pbs.UserPropRequest{
        Id: "prop_1",
        Count: 1,
    })
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    for {
        msg,err:=stream.Recv()
        if err==io.EOF{
            break
        }
        log.Printf("msg: %s", msg)
    }
​
}
​

客户端收到结果

2021/11/21 22:26:02 msg: 
2021/11/21 22:26:02 msg: 1:"prop_1"
2021/11/21 22:26:02 msg: 1:"res_1"  2:100
  • 客户端接收流数据需要循环接收,直到出现io.EOF,代表服务器发送流数据已经完毕,后面会写grpc实现这个功能的原理,慢慢的脱下grpc 神秘的衣服,各位看官不要急(* ̄︶ ̄)。

2.2.3客户端数据流模式

客户端可以将数据源源不断发送给服务器,跟服务端流相反,客户端会发送多条响应,服务器发送一条响应,但是服务器不必等到发送完所有消息才响应。可以发送一条或几条消息就开始响应。

下面来看一个例子:物联网硬件将本地的缓存信息上传到服务器

data.proto

syntax = "proto3";
option go_package = "protos/pbs";
service DataService {
  rpc DataUpload (stream DataUploadRequest) returns (DataUploadResponse);
}
enum DeviceStatus {
    DeviceStatusStop=0;    //停止
    DeviceStatusRunning=1; //运行
    DeviceStatusIdle=2;    //怠机
​
}
message DataUploadRequest {
  int64 Id=1;//终端id
  int64 Temperature =2;//温度
  int64 Humidity =3;//湿度
  DeviceStatus status=4;//设备状态
  int64 Time =5 ; //数据产生时间
​
}
​
message DataUploadResponse {
​
}
​

server.go

package main
​
import (
    "fmt"
    "google.golang.org/grpc"
    "grpcdemo/3client_stream/protos/pbs"
    "io"
    "log"
    "net"
)
​
type DataServer struct {
​
}
​
func (h *DataServer)DataUpload(stream pbs.DataService_DataUploadServer )error  {
    for {
        data,err:=stream.Recv()
        if err==io.EOF{ //已经接收完毕
            return stream.SendAndClose(&pbs.DataUploadResponse{})
        }
​
        h.doSave(data)
    }
}
//将数据落到时序数据库
func (h *DataServer)doSave(data *pbs.DataUploadRequest){
    fmt.Println(data)
}
​
func main()  {
    lis, err := net.Listen("tcp", ":9528")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pbs.RegisterDataServiceServer(s, &DataServer{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
​
  • 服务器需要以流的方式去接收数据,当客户端关闭流的时候会返回io.EOF,这时候我们可以做响应。

client.go

package main
​
import (
    "context"
    "fmt"
    "google.golang.org/grpc"
    "grpcdemo/3client_stream/protos/pbs"
    "io"
    "log"
​
    "time"
)
​
func main() {
​
    conn, err := grpc.Dial("127.0.0.1:9528", grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pbs.NewDataServiceClient(conn)
​
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    stream,err:=c.DataUpload(ctx)
    if err!=nil{
        panic(err)
    }
    for _,v:=range GetData(){
        err =stream.Send(v)
        if err!=nil{
            panic(err)
        }
    }
    response,err:=stream.CloseAndRecv()
    if err!=nil&&err!=io.EOF{
        panic(err)
    }
    fmt.Println(response)
}
//模拟物联网设备传数据
func GetData()(res []*pbs.DataUploadRequest){
    res= append(res, &pbs.DataUploadRequest{
        Id: 1,
        Temperature: 45,
        Humidity: 20,
        Time: 1637502637,
    })
    res= append(res, &pbs.DataUploadRequest{
        Id: 2,
        Temperature: 46,
        Humidity: 20,
        Time: 1637502638,
    })
    res= append(res, &pbs.DataUploadRequest{
        Id: 3,
        Temperature: 47,
        Humidity: 21,
        Time: 1637502640,
    })
    res= append(res, &pbs.DataUploadRequest{
        Id: 4,
        Temperature: 48,
        Humidity: 22,
        Time: 1637502659,
    })
    return res
}
​
  • 客户端需要通过定义rpc方法c.DataUpload(ctx)打开流,然后通过send 发送请求,发送完后调用CloseAndRecv关闭流等待消息响应,并处理错误,这里为了demo 演示 ,err就直接panic,实际情况可能更加复杂,对错误处理也很多种方式。

客户端请求后,服务器打印了如下结果

2021/11/21 22:01:58 server listening at [::]:9528
Id:1 Temperature:45 Humidity:20 Time:1637502637
Id:2 Temperature:46 Humidity:20 Time:1637502638
Id:3 Temperature:47 Humidity:21 Time:1637502640
Id:4 Temperature:48 Humidity:22 Time:1637502659

2.2.4双向数据流模式

双方都可以将数据源源不断发给对方。简单来说就是上面客户端流和服务器流的一个整合。

下面来看一个例子:玩家连续进行了多次战斗请求,服务器将操作结果响应给玩家

battle.proto

syntax = "proto3";
option go_package = "protos/pbs";
​
service BattleService {
  rpc Battle (stream BattleRequest) returns (stream BattleResponse);
}
​
​
message HeroInfo{
  string Id=1; //英雄id
  int64 Life=2;//英雄生命
}
//请求战斗
message BattleRequest {
  string HeroId=1; //英雄id
  string SkillId= 2; //技能id
}
message BattleResponse {
  repeated HeroInfo hero=1;
  repeated SkillInfo skill=2;
}
message SkillInfo {
  string SkillId=1; //技能id
  int64 CoolDown=2;//技能冷却时间
}

server.go

package main
​
import (
   "fmt"
   "google.golang.org/grpc"
   "grpcdemo/4stream/protos/pbs"
   "io"
   "log"
   "net"
)
​
type BattleServer struct {
​
}
​
func (h *BattleServer)Battle(steam pbs.BattleService_BattleServer) error{
   for {
      req,err:=steam.Recv()
      fmt.Println(req)
      if err==io.EOF{ //发送最后一次结果给前端
         err= steam.Send(&pbs.BattleResponse{})
         if err!=nil{
            log.Println(err)
         }
         return  nil
      }
      err=steam.Send(&pbs.BattleResponse{
         Hero:[]*pbs.HeroInfo{
            {Id: "hero_1",Life: 999},
         } ,
         Skill: []*pbs.SkillInfo{
            {SkillId: "skill_1",CoolDown:1637507349 },
         },
      })
      if err!=nil{
         log.Println(err)
      }
   }
}
func main()  {
   lis, err := net.Listen("tcp", ":9528")
   if err != nil {
      log.Fatalf("failed to listen: %v", err)
   }
   s := grpc.NewServer()
   pbs.RegisterBattleServiceServer(s, &BattleServer{})
   log.Printf("server listening at %v", lis.Addr())
   if err := s.Serve(lis); err != nil {
      log.Fatalf("failed to serve: %v", err)
   }
}
  • 服务器读到客户端流关闭时返回nil,标记服务器流结束。
  • 与之前客户端流模式不一样,客户端流模式是直接closeSend()。下面这样读到一半数据返回nil,也标识服务器流数据结束,只是可能会丢数据
func (h *BattleServer)Battle(steam pbs.BattleService_BattleServer) error{
   for {
      req,err:=steam.Recv()
      fmt.Println(req)
      if err==io.EOF{ //发送最后一次结果给前端
         err= steam.Send(&pbs.BattleResponse{})
         if err!=nil{
            log.Println(err)
         }
         return  nil
      }
............
      return nil
   }
}

client.go

package main
​
import (
   "context"
   "fmt"
   "google.golang.org/grpc"
   "grpcdemo/4stream/protos/pbs"
   "io"
   "log"
​
   "time"
)
​
func main() {
​
   conn, err := grpc.Dial("127.0.0.1:9528", grpc.WithInsecure(), grpc.WithBlock())
   if err != nil {
      log.Fatalf("did not connect: %v", err)
   }
   defer conn.Close()
   c := pbs.NewBattleServiceClient(conn)
​
   ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   defer cancel()
   stream, err := c.Battle(ctx)
   if err != nil {
      log.Fatalf("could not battle: %v", err)
   }
   err =stream.SendMsg(&pbs.BattleRequest{
      HeroId: "hero_1",
      SkillId: "Skill_1",
   })
   if err != nil {
      log.Fatalf("could not battle: %v", err)
   }
   err =stream.SendMsg(&pbs.BattleRequest{
      HeroId: "hero_2",
      SkillId: "Skill_2",
   })
   if err != nil {
      log.Fatalf("could not battle: %v", err)
   }
   ch:=make(chan struct{})
   go asyncDoBattle(stream,ch)
   err =stream.CloseSend()
   if err != nil {
      log.Fatalf("could not battle: %v", err)
   }
   <-ch
}
func asyncDoBattle(stream pbs.BattleService_BattleClient,c chan struct{} )  {
   for {
      rsp,err:=stream.Recv()
      if err==io.EOF{
         break
      }
      fmt.Println(rsp)
   }
   c<- struct{}{}
}
  • 启动一个协程异步接收数据,官方有说明,一个goroutine 读,一个goroutine 写是不会有并发问题的。
  • stream.CloseSend()代表关闭客户端流,标记客户端流已经结束

客户端请求后:

hero:{Id:"hero_1" Life:999} skill:{SkillId:"skill_1" CoolDown:1637507349}
hero:{Id:"hero_1" Life:999} skill:{SkillId:"skill_1" CoolDown:1637507349}

服务器:

HeroId:"hero_1"  SkillId:"Skill_1"
HeroId:"hero_2"  SkillId:"Skill_2"
<nil>
HeroId:"hero_1"  SkillId:"Skill_1"
HeroId:"hero_2"  SkillId:"Skill_2"
<nil>
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值