【gRPC】:快速上手gRPC与protobuf
本文目录
- 一、微服务
- 二、gRPC介绍
- 三、protobuf
- proto文件介绍
- proto消息嵌套
- 定义服务
- 四、gRPC实例
- 服务端编写
- 编写客户端
一、微服务
微服务出来之前,是单体架构,如下图所示。
一旦某个服务宕机,可能会引起整个应用都不能用,隔离性比较差。
只能整体应用进行伸缩,浪费资源,可伸缩性很差。
并且代码耦合在一起,可维护性很差。
那么就发展出了微服务架构,所有的请求就都访问网关,然后根据请求路由到不同的集群上,然后再返回请求到网关,网关再返回给用户。
拆分需要将每个服务的公共功能都放在网关实现,或者把公共功能单独做一个服务,形成新的服务,比如统一认证中心。这也算是服务拆分。
服务拆分之后,服务之间调用是进程和进程之间调用、服务器和服务器之间的调用。那么这个时候就需要发起网络调用,http是比较常见的协议,但是性能比较低,这个时候需要引入RPC-远程过程调用,通过自定义协议发起TCP调用,来加快传输效率。
同时每个服务由于可能分布在成干上百台机器上,服务和服务之间的调用,会出现一些问题,比如,如何知道应该调用哪台机器上的服务,调用方可能需要维护被调用方的地址,这个地址可能很多,增加了额外的负担,这时候就需要引入服务治理。
服务治理
是一个大的概念,这个概念中有一个比较重要的点就是 服务发现
,服务发现中 有个重要的东西 是 注册中心
。
服务提供商在启动服务的时候需要把服务注册到服务发现(注册中心),然后
服务消费方去注册中心要对应的服务的地址。
同时,服务和服务之间的调用会发生一些问题,为了避免产生连锁的雪崩反应,引入了服务容错,同时为了追踪一个调用所经过的服务,引入了链路追踪,这些就构成了一个微服务的生态。
二、gRPC介绍
服务和服务之间调用需要使用RPC,gRPC 是一款语言中立、平台中立、开源的远程过程调用系统, gRPC 客户端和服务端可以在多种环境中运行和交互,例如用java 写一个服务端,可以用 go 语言写客户端调用。
数据在进行网络传输的时候,需要进行序列化,序列化的协议有很多种,比如xml、json、protobuf等,gRPC默认用Protocol buffers,这是google开源的一套成熟的结构数据序列化机制。
序列化:将数据结构或对象转换成二进制串的过程。
反序列化:将在序列化过程中所产生的二进制串转换成数据结构或对象的过程。
三、protobuf
protobuf是二进制数据格式,需要编码和解码,本身不具有可读性,所以只能反序列化之后才能得到真正可读的数据。
序列化之后的提及比json和xml很小,适合网络传输,同时支持跨平台多种语言,并且序列化、反序列化的速度很快。
到github中下载protoc通用编译器,https://github.com/protocolbuffers/protobuf/releases?page=1
,找到对应版本并下载即可。下载之后解压到某处,并且将bin目录添加到环境变量中。
然后安装go专用的protoc生成器。
在Go Path下安装命令:
(base) PS D:\GoProject\bin> go install github.com/golang/protobuf/protoc-gen-go@latest
安装成功后会生成一个可执行文件,也就是protoc-gen-go.exe
文件,执行protoc命令会自动调用这个插件。
定义一种源文件,扩展名为 .proto ,使用这种源文件,可以定义存储类的内容(消息类型)。
protobuf有自己的编译器 protoc,可以将.proto 编译成对应语言的文件,就可以进行使用了。
现在我们尝试下写一个简单demo。
syntax = "proto3";
// go_package ="path;name"; path表示生成的go文件的存放地址,会自动生成目录。
// name 表示生成的go文件所属的包名,如果不进行配置,那么默认使用这个proto文件的名称。
option go_package = "../service";
// 指定待会文件生成出来的package
package service;
// 传输的对象
message User{
string username = 1;
int32 age = 2;
}
在pbfile路径下输入protoc命令,会自动帮我们生成service文件夹和对应的文件。
PS D:\GoProject\protobuflearn\pbfile> protoc --go_out=./ .\user.proto
编写简单的main demo来试试proto用法。
package main
import (
"fmt"
"google.golang.org/protobuf/proto"
"protobuflearn/service"
)
func main() {
user := &service.User{
Username: "zhou",
Age: 18,
}
//序列化
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())
}
运行main,得到输出如下:username:"zhou" age:18
proto文件介绍
message是通用的关键字,消息类型是通过关键字message指定的。
类似C++中的class、go中的struct(这也是为什么会生成service中的struct类型。)
在message里面,有required(必填,默认就是)、optional(可选,生成指针)和repeated(重复,会生成切片)选项可以设置。
message User{
string username = 1;
int32 age = 2;
optional string password = 3;
repeated string addresses = 4;
}
重新生成上面的message,看看生成什么。
消息体的每一个字段都要有一个标识号,范围是[0,2^29-1]。
proto消息嵌套
可以定义多个消息类型。
也可以进行消息嵌套。
UserInfo 是一个顶层消息,表示用户信息的集合。它包含一个字段 info,字段类型为 repeated User,表示 info 是一个 User 类型的列表(数组)。
定义服务
如果想将消息类型用在PRC系统中,可以在.proto文件中定一个PRC服务接口,protocol buffer编译器会根据所选择的不同语言生成服务接口代码及存根。
比如下面的代码表示定义了一个RPC服务,接收Request返回Response。
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
四、gRPC实例
下图是RPC调用的过程说明。RPC跨越了传输层和应用层,采用客户端/服务端的模式,通过request-response消息模式实现。
gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,能够更容易地创建分布式应用和服务。与许多 RPC系统类似,gRPC也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
gRPC是基于HTTP2的。 但是gRPC是开放的,可以更换其底层协议。
HTTP2的安全性有保证,天然支持SSL,当然gRPC可以跑在不加密的协议上。在公网传输上有保证,比如CRIME攻击。
服务端编写
定义一个product.proto
文件。
syntax = "proto3";
// go_package ="path;name"; path表示生成的go文件的存放地址,会自动生成目录。
// name 表示生成的go文件所属的包名,如果不进行配置,那么默认使用这个proto文件的名称。
option go_package = "../service";
// 指定待会文件生成出来的package
package service;
message ProductRequest{
int32 prod_id = 1;
}
message ProductResponse{
int32 prod_stock = 1;
}
//定义服务主体
service ProdService{
// 定义方法
rpc GetProductStock(ProductRequest) returns (ProductResponse);
}
使用下面的命令进行生成,这里需要插件来帮助我们定向生成grpc的代码。
protoc --go_out=plugins=grpc:./ .\product.proto
然后我们需要实现product.go
文件。
package service
import "context"
var ProductService = &productService{}
type productService struct {
}
func (p *productService) GetProductStock(context context.Context, request *ProductRequest) (*ProductResponse, error) {
//实现具体的业务逻辑
stock := p.GetStockById(request.ProdId)
return &ProductResponse{ProdStock: stock}, nil
}
func (p *productService) GetStockById(id int32) int32 {
return 100
}
为什么需要生成product.go
文件呢?因为RegisterProdServiceServer
的参数是一个接口,我们需要实例化一个对象来实现这个接口。
以及服务端文件。
package main
import (
"google.golang.org/grpc"
"log"
"net"
"protobuflearn/service"
)
func main() {
//创建一个 gRPC 服务器实例。
rpcServer := grpc.NewServer()
//将自定义的服务注册到 gRPC 服务器中。
//service.RegisterProdServiceServer: 是一个由 Protobuf 编译器生成的函数,用于将服务注册到 gRPC 服务器。
//函数签名如下 func RegisterProdServiceServer(server *grpc.Server, srv ProdServiceServer)
//srv:ProdServiceServer 类型,表示实现服务接口的具体实例。
service.RegisterProdServiceServer(rpcServer, service.ProductService)
listener, err := net.Listen("tcp", ":8002") //创建一个 TCP 监听器,用于监听指定的网络地址。
if err != nil {
log.Fatalf("net.Listen err: %v", err)
}
rpcServer.Serve(listener) //将监听器传递给 gRPC 服务器,开始处理传入的连接。
}
此时项目结构如下图所示。
编写客户端
package client
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"protobuflearn/client/service"
)
func main() {
conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("服务端出错,连接不上", err)
}
defer conn.Close()
//调用product.pb.go中的NewProdServiceClient方法
productServiceClient := service.NewProdServiceClient(conn)
request := &service.ProductRequest{
ProdId: 123,
}
resp, err := productServiceClient.GetProductStock(context.Background(),request)
if err != nil {
log.Fatal("调用gRPC方法错误", err)
}
fmt.Println("调用gRPC方法成功,ProdStock = ", resp.ProdStock)
}