热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

gRPC实操指南(golang)

1RPC(RemoteProcedureCallProtocol)1.1什么是RPCRPC即远程调用协议,简单来说就是调用远程的函数。正常单机开发的情况下&#x

1 RPC(Remote Procedure Call Protocol)


1.1 什么是RPC

RPC即远程调用协议,简单来说就是调用远程的函数。

正常单机开发的情况下,我们通过函数的方式实现部分功能的解耦

func sum(num1,num2 int) int {return num1 + num2
}

如上是一个最简单的求和函数,我们只需要调用函数就可以实现求和的功能。

但大部分时候函数不会这么简单,尤其对于非单机的分布式系统,远程调用就尤为重要。

1.2 RPC业务场景

RPC的应用场景很广泛:

•所有的分布式机都需要进行登陆的验证,对于所有的主机都实现相同的登陆验证逻辑维护极差,同时也失去部分分布式意义,所以从解耦的角度考虑,我们需要定义一个统一的登陆验证业务来做。•C/S架构的传输业务,如股票软件,每天需要用户登陆的时候去服务器拉取最新的数据,或者较简单的文件传输业务,登陆验证业务,证书业务都可以使用rpc的方式•跨语言开发的项目,比如web业务使用golang进行开发,底层使用cpp或c,部分脚本使用py,跨语言通信可以通过RPC提供的不同语言的开发机制进行实现。

因而实际上,RPC就是一个远程的函数,只不过RPC协议做的就是把整个过程透明化,以使得从开发角度来看,和本地函数调用没有区别。

1.3 主流RPC框架

目前主流的RPC,有ali的Dubbo,还有google的gRPC(本文主题)等

一般RPC框架如下所示:

•客户端:客户端作为整个RPC业务的发起者,如上所说的股票软件,需要客户端主动发起请求去拉取最新的股票数据。•服务端:服务端接受客户端的请求,并做出相应的回应。简单来说,函数实体在服务端,数据处理在服务端。

服务端和客户端是每个RPC框架,开发者可见度最高的部分,实现RPC业务的重点就在于对C/S的设计和理解。首先,客户端一定是率先发起请求的部分,服务端一定是具体处理请求的部分。比如之前我们说的求和函数,函数主体一定是在服务端,客户端有两个数字num1,num2,向服务端发起RPC远程调用,并最后拿到求和结果。

分清C/S很重要!!!!!

•客户端stub,服务端stub,可以变相的理解为应用层。主要是对客户端的rpc调用和服务端的返回进行序列化和反序列化,并进行传输,即把rpc业务抽象成tcp socket的send和receive。(gRPC使用的就是tcp,http2.0协议,建立在传输层)

2 gRPC


2.1 什么是gRPC

gRPC是google的开源RPC框架,引用官网的一句话

A high-performance, open-source universal RPC framework

如图,展示了gRPC跨语言开发的结构图,本文将描述golang使用grpc的过程。

严格来说,grpc通过tcp进行通信,使用http2.0协议,同时使用protobuf定义接口,因而相对于传统的restful api来说,速度更快,数据更小,接口要求更严谨。(protobuf此处不做详细介绍,Google Protobuf[1])

2.2 四种gRPC服务类型

准确来说不应称为四种,实际上是因为rpc入参和出参都可实现流式或非流式,进而排列组合形成四种常用的gRPC模式。

•简单RPC

即客户端发起一次请求,服务端进行响应(类似restful api)。这种模式下,rpc调用和本地函数基本相同,常常用于登陆验证,握手协议,简单业务等。

    •客户端流RPC

即客户端流式发送请求,有序发送很多req包(如文件流上传),server接收到所有的req包后会检测到EOF,回发一个res并关闭连接。比如云计算应用,客户端传输众多基础数据,等待服务端计算完成并返回结果。

    •服务端流RPC

即客户端发起一次请求,服务端会发很多res包(如文件流下载),server发送完成后关闭连接。常用于数据的拉取,如请求大量数据,无法及时进行反馈,进而通过流式进行反馈。

    •双端流RPC

即双方对话,可以实现一问一答,一问多答,多问一答等,常用于聊天室等及时通讯业务。


3 gRPC实操


3.1 环境配置


3.1.1 首先使用go get获取grpc的官方软件包

go get google.golang.org/grpc

3.1.2 下载protobuf编译器

protobuf代码生成工具[2],通过proto文件生成对应的代码。

(此处需要加入环境变量,各个系统操作不同,不赘述,protoc命令能够正常使用即可)

3.1.3 安装golang编译插件

我们需要.proto最终生成可用的golang代码,因而需要独立安装golang grpc的插件

go get -u github.com/golang/protobuf/protoc-gen-go

3.2 编写proto文件

protobuf的详细语法见官方文档,此处主要介绍rpc相关的内容

proto中rpc业务实际上就是一个函数,由服务端重写(overwrite)的函数,一般网上的文章会把gRPC分为四种:简单RPC,服务端流RPC,客户端流RPC,双端流RPC。实际上区别就在于rpc函数的入参和出参,接下来详细介绍一下四种情况,和一般的应用场景。

3.2.1 简单RPC

//指定使用proto3(proto2,3有很多不同,不可混写)
syntax = "proto3";
//指定生成的go_package,简单来说就是生成的go代码使用什么包,即package proto
option go_package = ".;proto";//定义rpc服务
//此处rpc服务的定义,一定要从服务端的角度考虑,即接受请求,处理请求并返回响应的一端
//请求接受一个LoginReq(username+password)
//响应回发一条msg("true" or "false")
service Login{rpc Login(LoginReq)returns(LoginRes){}
}message LoginReq {string username = 1;string password = 2;
}message LoginRes {string msg = 1;
}

以上就是一个简单的RPC业务,功能是进行登陆验证。

但实际上业务不会这么简单,比如请求或者响应体特别大,肯定不能封装到一个protobuf包进行传输,因而需要使用流式传输,如请求视频资源,或者上传文件等,此时就引出了两种单向流类型,即客户端流和服务端流。

3.2.2 客户端流RPC

简单来说,就是客户端请求是个流,其他和简单RPC类似。

syntax = "proto3";
option go_package = ".;proto";//下载服务
//请求接受一个UploadReq(username+password)
//响应回发多条数据("true" or "false")
service Upload{rpc Upload(stream UploadReq)returns(UploadRes){}
}message UploadReq {string path = 1;int64 offset = 2;int64 size = 3;bytes data = 4;
}message UploadRes {string msg = 1;
}

这里展示的应用场景为上传文件,即客户端指定文件路径,数据偏移量和大小,以及传输的二进制数据,打包通过protobuf发送给服务端,服务端不停接受req并写文件,最终写完之后给客户端一个反馈res。

RPC的流指的是客户端流式发送数据,本质上是分块写的思想。即每个数据包指定路径,偏移和写入大小,同时包含数据内容,每次写一个固定大小的块(如2M),流式指的是流式发送很多个块,如1G为512个2M的块。

3.2.3 服务端流RPC

同上~

syntax = "proto3";
option go_package = ".;proto";//下载服务
//请求接受一个DownloadReq(username+password)
//响应回发多条数据("true" or "false")
service Download{rpc Download(DownloadReq)returns(stream DownloadRes){}
}message DownloadReq {string path = 1;int64 offset = 2;int64 size = 3;
}message DownloadRes {int64 offset = 1;int64 size = 2;bytes data = 3;
}

理解了客户端流,服务端流也一样的道理,客户端发送一个请求,服务端不停的发送响应,直到全部发送完成。

上述代码的场景即为下载文件,发送一次请求,请求读取某个路径下的文件,比如读取6M大小,从2M的位置开始读,响应即分为三个块,分别包含2-4,4-6,6-8的数据(块大小可以定制,仅以2M举例)。

3.2.4 双端流RPC

双端流RPC就是入参,出参皆为流。一般的应用场景,如聊天室,聊天室需要维持一个长链接,连接过程中双方进行通信,都是流式的信息,类似应用场景使用双端流式的RPC。

综上,其实分类的四种RPC本质上只是RPC函数在入参和出参上有一些不同,本质上没有太大区别。但go中具体每个rpc业务的复写,针对流式和非流式处理不同,下面会详细描述,golang中如何实现除双端流之外的三种RPC(双端流同理)。

3.3 生成go rpc代码

编写完proto文件就可以通过proto去生成对应的go语言代码了~

protoc --go_out=plugins=grpc:. *.proto

protoc为编译器的命令,指定使用插件为grpc,输出目录为.(grpc:.)当前目录,待编译文件为*.proto。此处可以指定某个文件编译,也可以指定输出目录,这条命令会编译当前目录下的所有proto文件并生成到当前目录。

以login为例子,生成的pb.go,rpc的核心就在Client和Server的两个interface中

Client interface

// LoginClient is the client API for Login service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type LoginClient interface {Login(ctx context.Context, in *LoginReq, opts ...grpc.CallOption) (*LoginRes, error)
}

Server interface

// LoginServer is the server API for Login service.
type LoginServer interface {Login(context.Context, *LoginReq) (*LoginRes, error)
}

客户端调用Client interface的方法,服务端重写Server interface的方法

一定要理解上述这句话!!!!!

例如这个列出服务器目录的rpc方法,客户端只需要创建客户端实例对象,然后调用这个方法就可以,传入req,接受res。因而我们说,对于客户端来说,此次调用和本地函数没有区别,但实际上是gRPC实现的远程调用,对于客户端开发是不可见的。

再说服务端,服务端需要重写Server中的方法,即服务端需要实现Server接口,对req进行处理,并生成res,同时提供ctx上下文用作并发处理。

综上!!!!客户端是这个函数的调用者,需要调用这个函数,服务端是这个函数的定义者,需要重写这个函数

3.4 服务端

下述代码皆可从我的github库中获得源码grpc-example[3]

3.4.1 重写Server interface


3.4.1.1 简单RPC

package mainimport ("context""grpcExample/simple_rpc/proto"
)type LoginServer struct {}//判断用户名,密码是否为root,123456,验证正确即返回
func (*LoginServer)Login(ctx context.Context, req *proto.LoginReq) (*proto.LoginRes, error) {//为降低复杂度,此处不对ctx进行处理if req.Username == "root" && req.Password == "123456" {return &proto.LoginRes{Msg: "true"},nil} else {return &proto.LoginRes{Msg: "false"},nil}
}

此处的login函数即为server端重写的server interface的login函数,目的是处理req,生成res并返回。整个rpc业务的核心就在于服务端重写的方法,此处验证用户名和密码并返回提示信息。(仅用于grpc演示,忽略网络安全相关内容)

3.4.1.2 客户端流RPC

package mainimport ("grpcExample/client_stream_rpc/proto""io""log"
)type UploadServer struct{}func (*UploadServer) Upload(uploadServer proto.Upload_UploadServer) error {for {//循环接受客户端传的流数据recv, err := uploadServer.Recv()//检测到EOF(客户端调用close)if err == io.EOF {//发送reserr := uploadServer.SendAndClose(&proto.UploadRes{Msg: "finish"})if err != nil {return err}return nil} else if err != nil{return err}log.Printf("get a upload data package~ offset:%v, size:%v\n",recv.Offset,recv.Size)}
}

客户端流式的rpc的入参是一个server对象,可以通过这个server对象调用Recv函数获取客户端发送的每一个流。此处如果客户端关闭连接,服务端会收到一个io.EOF的error,因而此处需要对err进行判断处理,如果客户端方传输完成关闭连接等待响应,服务端检测到EOF,应调用SendAndClose发送res响应信息并关闭连接,进而完成客户端流的传输。

3.4.1.3 服务端流RPC

package mainimport ("grpcExample/server_stream_rpc/proto""log"
)type DownloadServer struct{}func (*DownloadServer) Download(req *proto.DownloadReq, downloadServer proto.Download_DownloadServer) error {offset := req.Offset//循环发送数据for {err := downloadServer.Send(&proto.DownloadRes{Offset: offset,Size: 4 * 1024,Data: nil,})if err != nil {return err}offset += 4 * 1024if offset >= req.Offset + req.Size {break}}return nil
}

3.4.2 注册服务

func main() {lis, err := net.Listen("tcp", ":6012")if err != nil {log.Fatalf("failed to listen: %v", err)}//构建一个新的服务端对象s := grpc.NewServer()//向这个服务端对象注册服务proto.RegisterDownloadServer(s,&DownloadServer{})//注册服务端反射服务reflection.Register(s)//启动服务s.Serve(lis)//可配合ctx实现服务端的动态终止//s.Stop()
}

实际使用中,可以将这部分独立为一个模块,通过ctx控制server的启动和停止,进而灵活的控制grpc服务。

3.5 客户端


3.5.1 调用Client func


3.5.1.1 简单RPC

package mainimport ("context""google.golang.org/grpc""grpcExample/simple_rpc/proto""log""time"
)func main() {//创立grpc连接grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())if err != nil {log.Fatalln(err)}//通过grpc连接创建一个客户端实例对象client := proto.NewLoginClient(grpcConn)//设置ctx超时(根据情况设定)ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()//通过client客户端对象,调用Login函数res, err := client.Login(ctx, &proto.LoginReq{Username: "root",Password: "123456",})if err != nil {log.Fatalln(err)}//输出登陆结果log.Println("the login answer is", res.Msg)
}

所以,客户端只需要维持一个实例化的client对象,通过client调用方法就可以使用RPC服务,注意和服务端不同的是,每个服务都需要一个客户端,即服务端是在一个对象上注册很多个服务,而客户端调用每个RPC业务都需要一个对应函数的Client对象。

3.5.1.2 客户端流RPC

package mainimport ("context""google.golang.org/grpc""grpcExample/client_stream_rpc/proto""log""time"
)func main(){//创立grpc连接grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())if err != nil {log.Fatalln(err)}//通过grpc连接创建一个客户端实例对象client := proto.NewUploadClient(grpcConn)//设置ctx超时(根据情况设定)ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()//和简单rpc不同,此时获得的不是res,而是一个client的对象,通过这个连接对象去发送数据uploadClient,err := client.Upload(ctx)if err != nil {log.Fatalln(err)}var offset int64var size int64size = 4 * 1024//循环处理数据,当大于64kb退出for {err := uploadClient.Send(&proto.UploadReq{Path: "../test.txt",Offset: offset,Size: size,Data: nil,})if err != nil {log.Fatalln(err)}offset += size//发送超过64KB,调用CloseAndRecv方法接收responseif offset >= 64 * 1024 {res, err := uploadClient.CloseAndRecv()if err != nil {log.Fatalln(err)}log.Println("upload over~, response is ",res.Msg)break}}
}

客户端流在调用函数的时候获得的不是单纯的res对象,而是一个client对象,通过这个对象控制流的发送,并且在发送完成后主动调用CloseAndRecv去关闭连接并接受服务端的返回res。

3.5.1.3 服务端流RPC

package mainimport ("context""google.golang.org/grpc""grpcExample/server_stream_rpc/proto""log""time"
)func main(){//创立grpc连接grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())if err != nil {log.Fatalln(err)}//通过grpc连接创建一个客户端实例对象client := proto.NewDownloadClient(grpcConn)//设置ctx超时(根据情况设定)ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()//和简单rpc不同,此时获得的不是res,而是一个client的对象,通过这个连接对象去读取数据downloadClient,err := client.Download(ctx,&proto.DownloadReq{Path: "../test.txt",Offset: 0,Size: 64 * 1024,})if err != nil {log.Fatalln(err)}//循环处理数据,当监测到读取完成后退出for {res, err := downloadClient.Recv()if err != nil {log.Fatalln(err)}log.Printf("get a date package~ offset:%v, size:%v\n",res.Offset,res.Size)if res.Size + res.Offset >= 64 * 1024 {break}}log.Println("download over~")
}

此处获取的也是一个读取数据需要的对象,即客户端发送请求后得到该对象,通过该对象调用Recv来读取服务端流式发送的数据。

4 写在最后

建议先理解grpc的C/S架构

建议阅读:

•Go gRPC教程[4]•gRPC-go example[5]

github(vx):cjq99419 欢迎提问和批评指正!

References

[1] Google Protobuf: https://developers.google.com/protocol-buffers
[2] protobuf代码生成工具: https://github.com/protocolbuffers/protobuf/releases
[3] grpc-example: https://github.com/cjq99419/grpc-example
[4] Go gRPC教程: https://studygolang.com/articles/28205
[5] gRPC-go example: https://github.com/grpc/grpc-go/tree/master/examples


推荐阅读
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 本文讨论了在使用Git进行版本控制时,如何提供类似CVS中自动增加版本号的功能。作者介绍了Git中的其他版本表示方式,如git describe命令,并提供了使用这些表示方式来确定文件更新情况的示例。此外,文章还介绍了启用$Id:$功能的方法,并讨论了一些开发者在使用Git时的需求和使用场景。 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • Sleuth+zipkin链路追踪SpringCloud微服务的解决方案
    在庞大的微服务群中,随着业务扩展,微服务个数增多,系统调用链路复杂化。Sleuth+zipkin是解决SpringCloud微服务定位和追踪的方案。通过TraceId将不同服务调用的日志串联起来,实现请求链路跟踪。通过Feign调用和Request传递TraceId,将整个调用链路的服务日志归组合并,提供定位和追踪的功能。 ... [详细]
  • Kubernetes(k8s)基础简介
    Kubernetes(k8s)基础简介目录一、Kubernetes概述(一)、Kubernetes是什么(二& ... [详细]
  • SAP接口编程PyRFC 调用 BAPI_FIXEDASSET_CREATE1创建固定资产
    本篇演示通过PyRFC调用BAPI_FIXEDASSET_CREATE1在SAP系统中创建固定资产,再一次体验一下Python与其它语言相比的简洁性。首先简单说明B ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • Centos下安装memcached+memcached教程
    本文介绍了在Centos下安装memcached和使用memcached的教程,详细解释了memcached的工作原理,包括缓存数据和对象、减少数据库读取次数、提高网站速度等。同时,还对memcached的快速和高效率进行了解释,与传统的文件型数据库相比,memcached作为一个内存型数据库,具有更高的读取速度。 ... [详细]
  • 解决Sharepoint 2013运行状况分析出现的“一个或多个服务器未响应”问题的方法
    本文介绍了解决Sharepoint 2013运行状况分析中出现的“一个或多个服务器未响应”问题的方法。对于有高要求的客户来说,系统检测问题的存在是不可接受的。文章详细描述了解决该问题的步骤,包括删除服务器、处理分布式缓存留下的记录以及使用代码等方法。同时还提供了相关关键词和错误提示信息,以帮助读者更好地理解和解决该问题。 ... [详细]
  • 本文总结了初学者在使用dubbo设计架构过程中遇到的问题,并提供了相应的解决方法。问题包括传输字节流限制、分布式事务、序列化、多点部署、zk端口冲突、服务失败请求3次机制以及启动时检查。通过解决这些问题,初学者能够更好地理解和应用dubbo设计架构。 ... [详细]
  • 云原生应用最佳开发实践之十二原则(12factor)
    目录简介一、基准代码二、依赖三、配置四、后端配置五、构建、发布、运行六、进程七、端口绑定八、并发九、易处理十、开发与线上环境等价十一、日志十二、进程管理当 ... [详细]
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • 像跟踪分布式服务调用那样跟踪Go函数调用链 | Gopher Daily (2020.12.07) ʕ◔ϖ◔ʔ
    每日一谚:“Acacheisjustamemoryleakyouhaven’tmetyet.”—Mr.RogersGo技术专栏“改善Go语⾔编程质量的50个有效实践” ... [详细]
author-avatar
空心悟心
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有