API 风格 (二):RPC API

RPC介绍

在 Go 项目开发中,如果业务对性能要求比较高,并且需要提供给多种编程语言调用,这时候就可以考虑使用 RPC API 接口。 

RPC(Remote Procedure Call 远程过程调用),是一个计算机通信协议。该协议允许运行于一台计算机的程序,像调用本地方法一样,调用另一台计算机的子程序。

服务端实现一个函数,客户端使用 RPC 框架提供的接口,像调用本地函数一样调用这个函数,并获取返回值。

RPC 屏蔽底层的网络通信细节,使得开发人员无需关注网络编程的细节,可以将更多的时间、精力放在业务逻辑本身的实现上,从而提高开发效率。

RPC 调用具体流程

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

2. Client Stub 将本地参数打包(Marshalling)成一个消息,然后发送这个消息。

3. Client 所在的 OS 将消息发送给 Server。

4. Server 端接收到消息后,将消息传递给 Server Stub。

5. Server Stub 将消息解包(Unmarshalling)得到参数。

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

 Stub 负责 调用参数和返回值的序列化(serialization)、参数的打包和解包、网络层的通信

 Client 端一般叫 Stub,Server 端一般叫 Skeleton

gRPC 介绍

 gRPC 是由 Google 基于 HTTP 2.0 开发的高性能、开源、跨多种编程语言的通用 RPC 框架,默认采用 Protocol Buffers 数据序列化协议。

注意:gRPC 全称是 google Remote Procedure Call。

gRPC 特性

1. 支持多种语言。

2. 通信协议基于标准的 HTTP 2.0 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性。

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

4. 支持 Protobuf 和 JSON 序列化数据格式。Protobuf 是一种语言无关的高性能序列化框架,可以减少网络传输流量,提高通信效率。 

gRPC 调用

gRPC 通过 IDL 语言,预先定义好服务、接口(接口的名字、传入参数、返回参数)。

在服务端,gRPC 服务实现我们所定义的接口。

在客户端,gRPC 存根提供了跟服务端相同的方法。

服务类型

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

1. 简单模式              (Simple RPC)

    客户端发起一次请求,服务端响应一个数据。定义格式为:

rpc SayHello (HelloRequest) returns (HelloReply) {}

2. 服务端数据流模式(Server-side streaming RPC)

    客户端发送一个请求,服务器返回 数据流 响应,客户端从流中读取数据直到为空。格式为:

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

3. 客户端数据流模式(Client-side streaming RPC)

    客户端以流的方式发送给服务器,服务器全部处理完之后返回一次响应。

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

4. 双向数据流模式    (Bidirectional streaming RPC)

    客户端和服务端都可以向对方发送数据流,这个时候双发的数据可以同时互相发送,也就是

    可以实现实时交互。

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

Protocol Buffers 介绍

 gRPC API 接口通常使用的数据传输格式是 Protocol Buffers。

Protocol Buffers(ProtocolBuffer / protobuf)是 Google 开发的一套对数据结构进行序列化的方法,可用于(数据)通信协议、数据存储格式等,也是一种更加灵活、高效的数据格式,与XML、JSON 类似。

特性

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

2. 跨平台、多语言

    protobuf 自带的编译工具 protoc 可以基于 protobuf 定义文件,编译出不同语言的客户端或者

    服务端,供程序直接调用。因此可以满足多语言需求的场景。

3. 更快的数据传输速度

     protobuf 在传输时,会将数据序列化为二进制数据,和 XML、JSON 的文本传输格式相比,

     可以节省大量的 IO 操作,从而提高数据传输速度。

4. 具有非常好的扩展性和兼容性,可以更新已有的数据结构,而不破坏和影响原有的程序。

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

1. 定义数据结构。例如,定义一个 SecretInfo 数据结构:

message SecretInfo {
    string name = 1;
    string secret_id = 2;
    string username = 3;
    string create_at = 4;
}

2. 定义服务接口。例如定义一个 Cache 服务,服务包含了 ListSecrets 和 ListPolicies 两个 API 接口。

service Cache{
  rpc ListSecrets(ListSecretsRequest) returns (ListSecretsResponse) {}
  rpc ListPolicies(ListPoliciesRequest) returns (ListPoliciesResponse) {}
}

3. 通过 protobuf 序列化 和 反序列化,提升传输效率。

gRPC 示例

前提条件

  • Go编译器
  • Protocol buffer 编译器(protoc,v3)
  • protoc 的 Go 语言插件。

步骤

  1. 定义 gRPC 服务
  2. 生成客户端、服务端代码
  3. 实现 gRPC 服务
  4. 实现 gRPC 客户端

目录结构

[going@dev apistyle]$ pwd
/home/going/workspace/golang/src/github.com/tiandh987/gopractise-demo/apistyle

[going@dev apistyle]$ tree greeter
greeter
├── client                       // 客户端
│   └── main.go
├── helloworld                   // 服务的 IDL 定义
│   ├── helloworld.pb.go         // 基于 .proto 文件,通过 protoc 编译工具生成(使用Go插件)
│   └── helloworld.proto
└── server                       // 服务端
    └── main.go

 定义 gRPC 服务

首先,进入 helloworld 目录,新建文件 helloworld.proto 。内容如下:

syntax = "proto3";

// option 关键字用来对 .proto 文件进行一些设置
// 其中 go_package 是必须的设置,而且 go_package 的值必须是包导入的路径
option go_package = "github.com/tiandh987/gopractise-demo/apistyle/greeter/helloworld";

// package 关键字指定生成的 .pb.go 文件所在的包名
package helloworld;

// 通过 service 关键字,定义服务(简单模式),然后再指定该服务拥有的 RPC 方法
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 通过 message 关键字,定义方法的请求结构体类型
message HelloRequest {
  string name = 1;
}

// 通过 message 关键字,定义方法的返回结构体类型 
message HelloReply {
  string message = 1;
}

生成客户端和服务器代码 

根据 .proto 服务定义文件,使用 protoc 编译工具,并指定使用其Go语言插件,生成 gRPC 客户端和服务端 接口。

$ protoc -I. --go_out=plugins=grpc:$GOPATH/src helloworld.proto

$ ls
helloworld.pb.go  helloworld.proto

报错:
--go_out: protoc-gen-go: plugins are not supported; use 'protoc --go-grpc_out=...' to generate gRPC

解决方案:
go get github.com/golang/protobuf/protoc-gen-go

参考链接:
https://www.pixelstech.net/article/1625819583-Fix---go_out%3A-protoc-gen-go%3A-plugins-are-not-supported

实现 gRPC 服务端

服务端源码

实现 gRPC 客户端

客户端源码

在创建连接时,我们可以指定不同的选项,用来控制创建连接的方式,例如 grpc.WithInsecure()、grpc.WithBlock() 等。gRPC 支持很多选项,更多的选项可以参考 grpc 仓库下 dialoptions.go 文件中以 With 开头的函数。

测试

分别在服务端源码、客户端源码目录下执行:go run main.go

// 服务端
[going@dev server]$ go run main.go 
2021/07/17 20:08:35 Received: world

// 客户端
[going@dev client]$ go run main.go 
2021/07/17 20:08:35 Greeting: Hello world

示例二

在做服务开发时,我们经常会遇到一个场景:定义一个接口,接口通过判断是否传入某个参数,决定接口行为。

例如,提供一个 GetUser 接口,期望 GetUser 接口在传入 username 参数时,根据 username 查询用户的信息,如果没有传入 username,则默认根据 userId 查询用户信息。

我们需要判断客户端有没有传入 username 参数。我们不能根据 username 是否为空值来判断,因为我们不能区分客户端传的是空值,还是没有传 username 参数

这是由 Go 语言的语法特性决定的:如果客户端没有传入 username 参数,Go 会默认赋值为所在类型的零值,而字符串类型的零值就是空字符串。

怎么判断客户端有没有传入 username 参数呢?最好的方法是通过指针来判断,如果是 nil 指针就说明没有传入,非 nil 指针就说明传入。具体实现步骤:

1. 编写 protobuf 定义文件

    注意:需要在设置为可选字段的前面添加 optional 标识。

syntax = "proto3";

package user;
option go_package = "github.com/tiandh987/gopractise-demo/apistyle/user/api";

service User {
  rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
}

message GetUserRequest {
  string class = 1;
  optional string username = 2;
  optional string user_id = 3;
}

message GetUserResponse {
  string class = 1;
  string user_id = 2;
  string username = 3;
  string address = 4;
  string sex = 5;
  string phone = 6;
}

2. 使用 protoc 工具编译 protobuf 文件

    在执行 protoc 命令时,需要传入 --experimental_allow_proto3_optional 参数

    以打开 optional 选项。

protoc --experimental_allow_proto3_optional --go_out=plugins=grpc:$GOPATH/src user.proto

// 命令执行完成后,会生成 user.pb.go 文件。 
[going@dev api]$ ls
user.pb.go  user.proto

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

3. 编写 gRPC 接口实现

    在 GetUser 方法中,通过判断 r.Username 是否为 nil,来判断客户端是否传入了 Username

    参数。

func (c *User) GetUser(ctx context.Context, r *pb.GetUserRequest) (*pb.GetUserResponse, error) {    
    // 判断 Username 是否为 nil
    if r.Username != nil {        
        return store.Client().Users().GetUserByName(r.Class, r.Username)    
    }  
    // Username 为nil,通过 UserId 获取  
    return store.Client().Users().GetUserByID(r.Class, r.UserId)
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值