分布式系统应用——gRPC教程

此文章用于本人分布式第一次作业的参考手册

一、RPC介绍

RPCRemote Procedure Call的简称,中文叫远程过程调用。

简单理解:现在有两台服务器A和B。部署在A服务器上的应用,想调用部署在B服务器上的另一个应用提供的方法,由于不在一个内存空间,不能直接调用,需要通过网络来达到调用的效果。现在,我们在A服务的一个本地方法中封装调用B的逻辑,然后只需要在本地使用这个方法,就达到了调用B的效果。

对使用者来说,屏蔽了细节。你只需要知道调用这个方法返回的结果,而无需关注底层逻辑。从封装的那个方法角度来看,调用B之前我们需要如下约定:

  • 调用的语义,也可以理解为接口规范。(比如RESTful)
  • 网络传输协议 (比如HTTP)
  • 数据序列化反序列化规范(比如JSON)。

grpc python 分布式 grpc分布式部署_运维

图1 RPC示例

RPC是一种客户端-服务端(Client/Server)模式。从某种角度来看,所有本身应用程序之外的调用都可以归类为RPC。无论是微服务、第三方HTTP接口,还是读写数据库中间件MysqlRedis

HTTP只是一个通信协议,工作在计算机网络的最高层。而RPC是一个完整的远程调用方案。它包含了:接口规范、传输协议、数据序列化反序列化规范。

二、什么是gRPC和protobuf

2.1 gRPC

gRPC官网:https://grpc.io/docs/

gRPC是一个高性能、开源和通用的RPC框架,面向移动和HTTP/2设计。支持的语言如下:

Language

OS

Compilers / SDK

C/C++

Linux, Mac

GCC 6.3+, Clang 6+

C/C++

Windows 10+

Visual Studio 2017+

C#

Linux, Mac

.NET Core, Mono 4+

C#

Windows 10+

.NET Core, NET 4.5+

Dart

Windows, Linux, Mac

Dart 2.12+

Go

Windows, Linux, Mac

Go 1.13+

Java

Windows, Linux, Mac

Java 8+ (KitKat+ for Android)

Kotlin

Windows, Linux, Mac

Kotlin 1.3+

Node.js

Windows, Linux, Mac

Node v8+

Objective-C

macOS 10.10+, iOS 9.0+

Xcode 12+

PHP

Linux, Mac

PHP 7.0+

Python

Windows, Linux, Mac

Python 3.5+

Ruby

Windows, Linux, Mac

Ruby 2.3+

优点

  • 性能:体积小、速度快
  • 使用简单:编译器负责序列化和反序列化;向后兼容性好
  • 跨平台、跨语言、可扩展

缺点

  • 不适合对基于文本的文档建模:例如html
  • 自解释性差,必须有proto文件才能知道数据结构

2.2 架构

在gRPC中,客户端应用程序可以直接调用不同机器上的服务器应用程序的方法,就像它是一个本地对象一样,使你更容易创建分布式应用程序和服务。正如许多RPC系统一样,gRPC是围绕着定义一个服务的想法,指定可以远程调用的方法及其参数和返回类型。在服务器端,服务器实现这个接口并运行gRPC服务器来处理客户端的调用。在客户端,客户端有一个stub桩代码提供与服务器相同的方法。


grpc python 分布式 grpc分布式部署_1024程序员节_02

图2 gRPC架构

gRPC客户端和服务器可以在各种环境中运行并相互沟通——从Google内部的服务器到你自己的桌面——并且可以用任何gRPC支持的语言编写。因此,你可以很容易地用Java创建一个gRPC服务器,客户端用Go、Python或Ruby。此外,最新的谷歌API将有gRPC版本的接口,让你轻松地在你的应用程序中建立谷歌功能。

2.3 protobuf

2.3.1 简介

默认情况下,gRPC使用谷歌成熟的开源机制Protocol Buffers来序列化 结构化的数据(尽管它也可以用于其他数据格式,如JSON)。

序列化:将数据结构或对象转换成二进制的过程

反序列化:将在序列化过程中所产生的二进制串转换成数据结构或对象的过程。

protobuf是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因profobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。

优势:

  1. 序列化后体积相比)son和XML很小,适合网络传输
  2. 支持跨平台多语言
  3. 消息格式升级和兼容性还不错
  4. 序列化反序列化速度很快

protobuf的文档地址:[https://developers.google.com/protocol-buffers/docs/overview

在使用 Protocol Buffers时,第一步是为你想在proto file中序列化的数据定义结构:这是一个以.proto为扩展名的普通文本文件。Protocol Buffers的结构是messages,每个message是一个小的逻辑信息记录,包含一系列称为fields的name-value对。下面是一个简单的例子。

message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

然后,一旦你指定了你的数据结构,你就可以使用协议缓冲区编译器protoc从你的proto定义中用你喜欢的语言生成数据访问类。这些类为每个字段提供简单的访问器,如name()set_name(),以及将整个结构序列化/解析为原始字节的方法。因此,如果你选择的语言是C++,在上面的例子中运行编译器将生成一个名为Person的类。然后你可以在你的应用程序中使用这个类来填充、序列化和检索Protocol Buffers的信息。

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

gRPC使用带有一个特殊的gRPC插件的protoc,从你的proto文件中生成代码:你会得到生成的gRPC客户端和服务器代码,以及用于填充、序列化和检索你的消息类型的常规protocol buffer。

2.3.2 安装

1️⃣ 第一步:下载通用编译器

地址:https://github.com/protocolbuffers/protobuf/releases

根据不同的操作系统,下载不同的包,64位windows电脑,就下载protoc-3.20.3-win64.zip

2️⃣ 第二步:配置环境变量

下载完毕后解压缩放至任意目录下,将bin目录配置到Path环境变量中


grpc python 分布式 grpc分布式部署_1024程序员节_03

3️⃣ 第三步:安装go专用的protoc的生成器

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
 
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
 
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
 
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

4️⃣ 第四步:vscode上安装vscode-proto3

grpc python 分布式 grpc分布式部署_运维_04

2.3.3 初体验

假设,我们现在需要传输用户信息,其中有username和age两个字段

//指定的当前proto语法的版本,有2和3
syntax = "proto3";
//option go_package = "path;name"; ath 表示生成的go文件的存放地址,会自动生成目录的
// name 表示生成的go文件所属的包名
option go_package="../service";
// 指定等会文件生成出来的package
package service;

message User {
  string username = 1;
  int32 age = 2;
}

运行protoc命令编译成go中间文件

# 编译user.proto之后输出到service文件夹
protoc --go_out=../service user.proto

测试

package main

import (
	"fmt"
	"google.golang.org/protobuf/proto"
	"testProto/service"
)

func main()  {
	user := &service.User{
		Username: "mszlu",
		Age: 20,
	}
	//转换为protobuf
	marshal, err := proto.Marshal(user)
	if err != nil {
		panic(err)
	}
	newUser := &service.User{}
	err = proto.Unmarshal(marshal, newUser)
	if err != nil {
		panic(err)
	}
	fmt.Println(newUser.String())
}

输出

username:"mszlu"  age:20

2.3.4 proto文件介绍

1. message

messageprotobuf中定义一个消息类型是通过关键字message字段指定的。

消息就是需要传输的数据格式的定义。

message关键字类似于C++中的class,Java中的class,go中的struct,例如:

message User {
  string username = 1;
  int32 age = 2;
}

在消息中承载的数据分别对应于每一个字段。

其中每个字段都有一个名字和一种类型 。

2. 字段规则
  • required:消息体中必填字段,不设置会导致编解码异常。(默认,protobuf2中才用写)
  • optional: 消息体中可选字段。
  • repeated: 消息体中可重复字段,重复的值的顺序会被保留,在go中重复的会被定义为切片。
message User {
  string username = 1;
  int32 age = 2;
  optional string password = 3;
  repeated string address = 4;
}
3. 字段映射

proto Type

Notes

C++ Type

Python Type

Go Type

double

double

float

float64

float

float

float

float32

int32

使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代

int32

int

int32

uint32

使用变长编码

uint32

int/long

uint32

uint64

使用变长编码

uint64

int/long

uint64

sint32

使用变长编码,这些编码在负值时比int32高效的多

int32

int

int32

sint64

使用变长编码,有符号的整型值。编码时比通常的 int64高效。

int64

int/long

int64

fixed32

总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。

uint32

int

uint32

fixed64

总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。

uint64

int/long

uint64

sfixed32

总是4个字节

int32

int

int32

sfixed32

总是4个字节

int32

int

int32

sfixed64

总是8个字节

int64

int/long

int64

bool

bool

bool

bool

string

一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。

string

str/unicode

string

bytes

可能包含任意顺序的字节数据。

string

str

[]byte

4.默认值

protobuf3 删除了 protobuf2 中用来设置默认值的 default 关键字,取而代之的是protobuf3为各类型定义的默认值,也就是约定的默认值,如下表所示:

类型

默认值

bool

false

整型

0

string

空字符串""

枚举enum

第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0;

message

不是null,而是DEFAULT_INSTANCE

5. 标识号

标识号:在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是[0,2^29-1]范围内的一个整数。

message Person { 

  string name = 1;  // (位置1)
  int32 id = 2;  
  optional string email = 3;  
  repeated string phones = 4; // (位置4)
}

以Person为例,name=1,id=2, email=3, phones=4 中的1-4就是标识号。

6. 定义多个消息类型

一个proto文件中可以定义多个消息类型

message UserRequest {
  string username = 1;
  int32 age = 2;
  optional string password = 3;
  repeated string address = 4;
}

message UserResponse {
  string username = 1;
  int32 age = 2;
  optional string password = 3;
  repeated string address = 4;
}
7、 嵌套消息

可以在其他消息类型中定义、使用消息类型,在下面的例子中,Person消息就定义在PersonInfo消息内,如 :

message PersonInfo {
    message Person {
        string name = 1;
        int32 height = 2;
        repeated int32 weight = 3;
    } 
	repeated Person info = 1;
}

如果你想在它的父消息类型的外部重用这个消息类型,你需要以PersonInfo.Person的形式使用它,如:

message PersonMessage {
	PersonInfo.Person info = 1;
}

当然,你也可以将消息嵌套任意多层,如 :

message Grandpa { // Level 0
    message Father { // Level 1
        message son { // Level 2
            string name = 1;
            int32 age = 2;
    	}
	} 
    message Uncle { // Level 1
        message Son { // Level 2
            string name = 1;
            int32 age = 2;
        }
    }
}
8. 定义服务

如果想要将消息类型用在RPC系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer 编译器将会根据所选择的不同语言生成服务接口代码及存根。

service SearchService {
	//rpc 服务的函数名 (传入参数)返回(返回参数)
	rpc Search (SearchRequest) returns (SearchResponse);
}

上述代表表示,定义了一个RPC服务,该方法接收SearchRequest返回SearchResponse

三、gRPC实例

3.1 protobuf文件

hello.proto

syntax = "proto3";

package service;

option go_package = '../service';

// 定义request model
message ProductRequest {
    int32 prod_id = 1; // 1代表顺序
}
// 定义response model
message ProductResponse {
    int32 prod_stock = 1; // 1代表顺序
}

// 定义服务主体
service ProdService {
    // 定义方法
    rpc GetProductStock(ProductRequest) returns(ProductResponse);
}

执行命令:protoc --go_out=. --go-grpc_out=. *.proto

生成service文件夹hello.pb.gohello_grpc.pb.go

hello.pb.go:结构体相关,每个结构体都有5个相同的方法,分别是Reset()String()ProtoMessage()ProtoReflect()Descriptor()

hello_grpc.pb.go:rpc相关的方法,分为(Client)客户端,(Server)服务端。

  • Client端:有接口,结构体,建立客户端连接的方法。
  • Server端:有接口,结构体,方法等6个,需要注意的是服务端在使用前需要重写结构体并实现接口方法才行,否则会报错。

3.2 服务端实现

服务端需要创建一个结构体,同时实现在hello.proto种声明的接口

server.go

package main

import (
	"context"
	"grpc01/service"
	"log"
	"net"
	"google.golang.org/grpc"
)

type server struct {
	service.UnimplementedProdServiceServer
}

func (s *server) GetProductStock(ctx context.Context, in *service.ProductRequest) (*service.ProductResponse, error) {
	return &service.ProductResponse{ProdStock: in.GetProdId()}, nil
}

func main() {
	//1、创建服务
	s := grpc.NewServer()
	//2、注册服务
	service.RegisterProdServiceServer(s, &server{})
	//3、监听服务端口
	listener, err := net.Listen("tcp", ":8002")

	if err != nil {
		log.Fatal("服务监听端口失败", err)
	}
	//4、启动服务
	_ = s.Serve(listener)
}

3.3 客户端实现

client.go

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	service "grpc01/service"
	"log"
)


func main() {
	//1、 建立连接
	conn, err := grpc.Dial(":8002", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	// 2. 调用 hello_grpc.pb.go 中的NewProdServiceClient方法
	productServiceClient := service.NewProdServiceClient(conn)

	//3、调用rpc方法
	resp, err := productServiceClient.GetProductStock(context.Background(), &service.ProductRequest{
		ProdId: 233456,
	})

	if err != nil {
		log.Fatal("调用gRPC方法错误: ", err)
	}
	fmt.Println("调用gRPC方法成功,ProdStock = ", resp.ProdStock)

}