GO 语言项目开发实战 – API风格(下):RPC API介绍

GO 语言项目开发实战专栏目录总览

这一讲,我们继续看下如何设计应用的API风格。

上一讲,我有 REST API 风格,讲我来介绍下需要另外介绍一种常用的 API,RPC。调用,这时候就可以考虑使用RPC API接口了。RPC在Go项目开发中用得也非常多,需要我们掌握。

RPC 介绍

根据即百科的定义,RPC(RemoProcedure Call),另一个过程调用,是一个计算机协议协议。该协议允许远程运行于一台计算机的程序,调用计算机的子程序,而编写另一个程序为这个计算器。

通俗,服务端实现函数,客户端使用这个 RPC 调用函数的一样接口,像调用本地函数调用函数,获取返回值。的,可以将更多的时间和精力投入到具体业务逻辑本身的实现上,从而提高开发效率。

RPC的调用过程如下图所示:

GO 语言项目开发实战 – API风格(下):RPC API介绍

RPC调用具体流程如下:

Client通过本地调用,调用Client Stub。

Client Stub 将参数打包(也叫 Marshalling)成一个消息,然后发送这个消息。

客户端所在的操作系统将发送消息给服务器。

服务器端接收消息后,将消息传递给Server Stub。

Server Stub 将消息解包(也叫 Unmarshalling)得到参数。

Server Stub调用服务端的子程序(函数),处理完毕后,将最终结果按照相反的步骤返回给Client。

这里需要注意,Stub负责调用参数和返回值的流式化)、参数化的打包和解包,以及网络层的通信。Client端一般叫Stub,Server端一般叫Skeleton。

例如,Dutan 的同类有很多优秀的 RPC 协议,阿里的微博、微博的、Facebook 的 Thrift、RPCX,。但使用最多的还是gRPC,这也是本专栏所的 RPC 框架,所以我会重点介绍 gRPC 框架。

gRPC 介绍

RPC 是由 Google 开发的。gRPC 开发的语言、开源、跨工具编程的通用 RPC 框架,基于 HTTP 2.0 协议,默认采用 Protocol Buffers 数据序列协议,具有如下特性:

支持多种语言,例如 Go、Java、C、C++、C#、Node.js、PHP、Python、Ruby 等。

IDL(Interface Definition Language)文件定义服务,通过proto3工具生成指定语言的数据结构、服务端接口以及客户端Stub。通过这种方式,也可以将服务端和客户端解耦,使客户端和服务端可以并行开发。

基于协议协议的2个设计标准,支持HTTP流通信、消息头压缩、单路复用、端路由等特性。

Protobuf 和 JSON 序列化数据格式是一种支持语言通信的协议。Protobuf 是一种支持网络通信的传输序列化框架,传输,提高化效率。

这里要注意的是,gRPC 的全称不是 golang Remote Procedure Call,而是 google Remote Procedure Call。

gRPC 的调用如下图所示:

GO 语言项目开发实战 – API风格(下):RPC API介绍

在 gRPC 中,可以直接调用部署在不同机器上的 gRPC 服务所提供的方法,调用非常远的 gRPC 方法就像调用本地的方法一样,简单方便,通过 gRPC 调用,我们可以轻松地制造出一个应用程序。

像很多 RPC 服务端和 RPC 服务端的接口一样,预先定义我们的接口(接口的名字、其他参数)。存根提供了与服务端相同的方法。

gRPC 支持多种语言,比如我们可以使用 Go 语言实现 gRPC 服务,并通过语言客户端调用 gRPC 服务所提供的 Java。通过多语言支持,我们编写的 gRPC 服务能够满足客户端多语言的需求。

gRPC API接口通常使用的数据传输是Protocol Buffers。格式,我们就一起了解下Protocol Buffers。

协议缓冲区介绍

Protocol Buffers(ProtocolBuffer/protobuf)是Google开发的对数据结构进行序列化的方法,可使用(数据)通信、数据协议格式等,也是一种灵活、高效的数据格式,与XML、 JSON 类似。它的传输性能非常好,所以通常被用在一些对数据传输性能要求高的系统中,作为数据传输格式。Protocol Buffers 的主要特性有下面这几个。

采用的数据传输传输时,这个数据的传输顺序加快了 JSON 格式的数据传输格式,比方说:可以使用和大量的 IO 操作,从而提高数据传输。

跨平台多语言:protobuf自带的编译protoc可以基于多工具protobuf定义文件,编译出不同语言的客户端或者服务端,供程序直接调用,从而可以满足语言需求的场景。

具有非常好的和普及的,可以更新扩展的数据结构,而不是良好的破坏和影响现有的程序。

基于IDL文件定义服务,通过proto3生成指定语言的数据结构、服务端和客户端接口。

在 gRPC 的框架中,Protocol Buffers 主要有三个作用。

首先,可以使用定义数据结构。举个例子,下面的代码定义了一个 SecretInfo 数据结构:


// SecretInfo 包含秘密细节。

消息秘密信息 {

字符串名称 = 1 ;

字符串secret_id = 2 ;

字符串用户名 = 3 ;

字符串secret_key = 4 ;

int64过期 = 5 ;

字符串描述 = 6 ;

字符串created_at = 7 ;

字符串updated_at = 8 ;

}

其次,可以定义服务接口。下面的代码定义了一个缓存服务,服务包含了 ListSecrets 和 ListPolicies 两个 API 接口。


// Cache实现了一个缓存rpc 服务。

服务缓存{

rpc ListSecrets(ListSecretsRequest)返回(ListSecretsResponse) {}

rpc ListPolicies(ListPoliciesRequest)返回(ListPoliciesResponse) {}

}

第三,可以通过protobuf序列化和反序列化,提升效率。

gRPC 示例

我们已经对 gRPC 通用 RPC 框架有什么了解,但是你可能还快速给大家通过一个 gRPC API 接口。接下来,我是 gRPC 官方的示例来写展示。运行本示例需要在 Linux 服务器上安装 Go 编译器、Protocol buffer 编译器(protoc,v3)和 protoc 的 Go 插件,在02我们已经安装过,不再讲具体的安装方法。

这个例子分为以下几个步骤:

定义了 gRPC 服务。

生成客户端和服务器代码。

实现 gRPC 服务。

实现 gRPC 客户端。

示例代码存放在gopractice-demo/apistyle/greeter目录下。


$树

├── 客户

│ └── 主要。去

├── helloworld

│ ├── helloworld.pb。去

│ └── helloworld.proto

└── 服务器

└──主要。去

客户端存放目录代码 客户端存放目录的代码,helloworld存放服务器端的目录,服务器的IDL存放目录存放服务器端的代码。

下面我具体介绍下这个示例的步骤。

定义了 gRPC 服务。

首先,需要定义我们的服务。进入helloworld目录,新建文件helloworld.proto:


$ cd helloworld

$ vi helloworld.proto

内容如下:


语法 = "proto3" ;

 

选项 go_package = "github.com/marmotedu/gopractice-demo/apistyle/greeter/helloworld" ;

 

包你好世界;

 

// 问候服务定义。

服务迎宾{

// 发送问候语

rpc SayHello (HelloRequest)返回(HelloReply) {}

}

 

// 包含用户名的请求消息。

消息 HelloRequest {

字符串名称 = 1 ;

}

 

// 包含问候语的响应消息

留言 HelloReply {

字符串消息 = 1 ;

}

在 helloworld.proto 定义文件中,选项关键字使用对 .proto 文件进行一些设置,其中 go_package 是必须的设置,并且 go_package 的值必须是包导入的路径。package 关键字指定生成的.pb.go 文件我们通过服务关键字定义服务,然后再指定该服务拥有的 RPC 方法,并定义方法的请求和返回的结构类型:


服务迎宾{

// 发送问候语

rpc SayHello (HelloRequest)返回(HelloReply) {}

}

gRPC支持定义4种类型的服务方法,分别是简单的、服务端数据流模式、客户端数据流模式和数据流模式。

简单模式(Simple):是最简单的gRPC模式。客户端发起一次请求,服务响应一个数据。定义格式为rpc SayHello(HelloRequest)返回(HelloReply){}。

服务端数据流模式(Server-side streaming RPC):客户端发送一个请求,服务器返回数据流响应,客户端从流中读取数据直到为空。定义格式为 rpc SayHello(HelloRequest)返回(流 HelloReply) {}。

客户端数据流模式(Client-side streaming RPC):客户端将以流的方式发送消息给服务器,服务器全部完成处理后返回一次响应。定义格式为rpc SayHello(流HelloRequest)返回(HelloReply){}。

双向数据流模式:客户端和服务端RPC(可以向对方数据流发送双方的数据可以同时发送,也可以同时发送,也可以实时发送数据流模式RPC框架原理。定义为rpc Saystream) HelloRequest) 返回(流 HelloReply){}。

本示例使用了简单模式。.proto 消息文件也包含了 Protocol Buffers 消息的定义,包括请求和返回消息。例如请求消息:


// 包含用户名的请求消息。

消息 HelloRequest {

字符串名称 = 1 ;

}

生成客户端和服务器代码。

我们需要根据.proto服务定义生成gRPC客户端和服务器接口。我们可以使用protoc编译工具,并指定使用其Go语言插件来生成:


$协议-I。--go_out=plugins=grpc:$GOPATH/src helloworld.proto

$ ls

helloworld.pb.go helloworld.proto

你可以看到,新增了一个 helloworld.pb.go 文件。

实现 gRPC 服务。

打开,我们就可以实现 gRPC 服务了。进入服务器目录,新建 main.go 文件:


$ cd ../服务器

$ vi main.go

main.go 内容如下:


// Package main 为 Greeter 服务实现了一个服务器。

包主

 

进口(

“语境”

“日志”

“网”

 

pb “github.com/marmotedu/gopractice-demo/apistyle/greeter/helloworld”

“google.golang.org/grpc”

)

 

常量(

端口= “:50051”

)

 

// 服务器用于实现 helloworld.GreeterServer。

类型服务器结构{

pb.UnimplementedGreeterServer

}

 

// SayHello 实现 helloworld.GreeterServer

func (s *server) SayHello (ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {

log.Printf( "收到:%v" , in.GetName())

return &pb.HelloReply{Message: "Hello" + in.GetName()}, nil

}

 

功能主要() {

lis, err := net.Listen( "tcp" , 端口)

如果错误!= nil {

log.Fatalf( "监听失败:%v" , err)

}

s := grpc.NewServer()

pb.RegisterGreeterServer(s, &server{})

if err := s.Serve(lis); 错误!=无{

log.Fatalf( "服务失败:%v" , err)

}

}

根据上面的代码实现了我们上一步服务定义生成的 Go 接口。

我们先定义了一个 Go 结构体服务器,并为服务器结构体添加SHello(context.Context, pb.Hello) (pb.Helloly, error)方法,请求服务器是 Greeter 接口(位于 world.pb.go 文件中)的一个实现。

实现了gRPC服务所定义的方法,之后就可以通过我们的net.Listen指定监听请求的请求(… (s, &server{})服务服务注册到gRPC中;最后,通过s.Serve(lis)启动gRPC。

创建完main.go文件后,在当前执行go run main.go,启动gRPC目录服务。

实现 gRPC 客户端。

打开一个新的Linux终端,进入客户端目录,新建main.go文件:


$ cd ../客户端

$ vi main.go

main.go 内容如下:


// Package main 实现了 Greeter 服务的客户端。

包主

 

进口(

“语境”

“日志”

“操作系统”

“时间”

 

pb “github.com/marmotedu/gopractice-demo/apistyle/greeter/helloworld”

“google.golang.org/grpc”

)

 

常量(

地址= “本地主机:50051”

默认名称 = “世界”

)

 

功能主要() {

// 建立与服务器的连接。

conn, err := grpc.Dial(地址, grpc.WithInsecure(), grpc.WithBlock())

如果错误!= nil {

log.Fatalf( "没有连接: %v" , err)

}

延迟conn.Close()

c := pb.NewGreeterClient(conn)

 

// 联系服务器并打印出它的响应。

名称 := 默认名称

如果len (os.Args) > 1 {

名称 = os.Args[ 1 ]

}

ctx, 取消 := context.WithTimeout(context.Background(), time.Second)

推迟取消()

r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})

如果错误!= nil {

log.Fatalf( "无法打招呼:%v" , err)

}

log.Printf( "问候语:%s" , r.Message)

}

在上面的代码中,我们通过下面的代码创建了一个 gRPC 连接,使用与服务端进行通信:


// 建立与服务器的连接。

conn, err := grpc.Dial(地址, grpc.WithInsecure(), grpc.WithBlock())

如果错误!= nil {

log.Fatalf( "没有连接: %v" , err)

}

延迟conn.Close()

例如在创建连接时,我们可以指定不同的选项,使用控制创建连接的方式,grpc.WithInsecure()、grpc.WithBlock() 等参考。gRPC 支持很多选项,更多的选项可以 grpc 仓库下的dialoptions。以大观的功能去文件中。

例如连接建立起来之后,我们需要创建一个客户端存根,使用执行 RPC 请求c := pb.NewGreeterClient(conn)。创建完成,我们就可以像调用本地函数一样,调用远程的方法了。,下面一段代码通过c.SayHello这种本地式调用了远端的SayHello接口方式:


r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})

如果错误!= nil {

log .Fatalf( "无法打招呼:%v" , err)

}

log .Printf( "问候语:%s" , r.Message)

RPC调用有以下两个特点。

调用方便的保护网络的细节,介绍了调用本地调用方法的相同方法,调用方法跟大家所用的RPC调用的类的方法一致:类名。ClassFucparams 。

不需要的操作和分组的调用和返回的结构体的结构,都需要解入参包进行组操作,不需要对返回参数进行参数包,组织了调用步骤。

最后,创建完成 main.go 文件后,在当前目录下,执行 go run main.go 调用 RPC 调用:


$去运行主要。去

2020 / 10 / 17 07 : 55 : 00问候语:Hello World

至此,我们用四个步骤,并调用了一个 gRPC 创建服务。接下来我再给大家讲解一个在具体场景中的事项。

在示例服务中,决定遇到:定义一个期望接口通过判断是否会经常出现一个参数,我们会提供一个用户接口,接口行为会提供一个用户接口,让用户在接口想开发接口的用户名根据用户名查询用户的信息,如果没有参数时用户名,则根据用户ID查询用户信息。

这时候需要判断客户端,不能有用户名的值我们的参数。我们根据参数是否为空来判断,因为我们是传是空的客户端还是传用户名。如果特性决定类型的类型会调用用户名,Go 参数默认为类型的零值,而字符串的零值就是空字符串。

那怎么判断有没有用户名参数来判断,如果我们最好的方法说明是不是通过如下方式,没有说明,具体来说,具体是什么?

编写protobuf定义的文件。

新建 user.proto 文件,内容如下:


语法 = "proto3" ;

 

包原型;

选项 go_package = "github.com/marmotedu/gopractice-demo/protobuf/user" ;

 

//go: 生成协议 -I. --experimental_allow_proto3_optional --go_out=plugins=grpc:。

 

服务用户{

rpc GetUser (GetUserRequest)返回(GetUserResponse) {}

}

 

消息获取用户请求 {

字符串类= 1;

可选字符串username = 2 ;

可选字符串user_id = 3 ;

}

 

消息获取用户响应 {

字符串类= 1;

字符串user_id = 2 ;

字符串用户名 = 3 ;

字符串地址 = 4 ;

字符串性别 = 5 ;

字符串电话 = 6 ;

}

你需要注意,这里我们需要设置为可选前面添加的可选标识。

使用 protoc 工具编译 protobuf 文件。

在选项命令时,需要执行参数-允许使用proto_proto3_optional命令-experimental ,编译如下:


$ protoc --experimental_allow_proto3_optional --go_out=plugins=grpc:。用户.proto

其中上面的命令会生成 user.pb.go 文件,的 GetUserRequest 结构体定义如下:


类型 GetUserRequest结构{

state protoimpl.MessageState

sizeCache protoimpl.SizeCache

unknownFields protoimpl.UnknownFields

 

类 字符串 `protobuf: "bytes,1,opt,name=class,proto3" json: "class,omitempty" `

用户名 *字符串`protobuf: "bytes,2,opt,name=username,proto3,oneof" json: "username,omitempty" `

UserId * string `protobuf: "bytes,3,opt,name=user_id,json=userId,proto3,oneof" json: "user_id,omitempty" `

}

通过optional + –experimental_allow_proto3_optional组合,我们可以将一个字段组合为指针类型。

编写 gRPC 接口实现。

新建一个user.go文件,内容如下:


包用户

 

进口(

“语境”

 

pb “github.com/marmotedu/api/proto/apiserver/v1”

 

“github.com/marmotedu/iam/internal/apiserver/store”

)

 

类型用户结构{

}

 

func (c *User) GetUser (ctx context.Context, r *pb.GetUserRequest) (*pb.GetUserResponse, error) {

如果r.Username != nil {

return store.Client().Users().GetUserByName(r.Class, r.Username)

}

 

返回store.Client().Users().GetUserByID(r.Class, r.UserId)

}

中,我们可以通过判断r.是否为nil,来判断客户端是否有用户名参数。

RESTful VS gRPC

到这里,今天我们已经用完了 gRPC API。在下面的表格中,你可以把它作为对照组,根据自己的需求在实际应用时进行选择。

GO 语言项目开发实战 – API风格(下):RPC API介绍

更多的时候,RESTful API 和 gRPC API 是一种合作关系,对内部业务使用 gRPC API,然后业务使用 RESTful API,如下图:

GO 语言项目开发实战 – API风格(下):RPC API介绍

总结

去项目中,因为我们选择使用 RESTful API 风格和开发其中的 API 风格,这都需要很多,RESTful API 风格规范、易于理解、使用,所以可以适合用在需要提供 API 接口的服务器上因为RPC API 性能比较高、方便,更适合用在内部业务中。

RESTful API 使用的是 HTTP 协议,而 RPC API 使用的是 RPC 协议。有很多 RPC 可供你轻松选择,而我推荐你使用 RPC,同时我推荐你使用它的量,同时它的性能、很方便一个的 RPC 框架。所以目前最优秀的 gRPC 协议是用最多的还是 gRPC 的,腾讯、阿里等大厂内部有很多核心的在线服务用的。

除了使用 gRPC 协议,在进行 Go 项目之前,你也可以了解开发其他一些优秀的 Go RPC 框架,比如腾讯的 tars-go、阿里的 dubbo-go、Facebook 的 thrift、rpcx 等,你可以在项目中事先调查,根据实际情况进行选择。