Baste 发表于 2023-2-9 14:25

一文了解gRPC

1. GRPC简介

github地址Star:57.9k:https://github.com/protocolbuffers/protobuf
Protocol buffers

gRPC使用protocol buffers作为接口定义语言来描述服务接口和消息的结构。
Protocol buffers是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。它类似于JSON,只是它更小、更快,而且它生成本地语言绑定。您只需一次定义数据的结构方式,然后就可以使用特殊的生成源代码,使用各种语言轻松地在各种数据流之间读写结构化数据。
gRPC开发步骤:

[*]定义服务
[*]编译生成代码
[*]编写业务逻辑
[*]运行服务
2. 安装

grpc需要安装protocol buffer编译器,以及生成对应语言的插件
安装Protocol Buffers v3

protobuffer编译器用于编译.proto文件,包含服务和消息类型定义。
查看安装目录文件,其中:
bin 目录下的 protoc 是可执行文件。
include 目录下的是 google 定义的.proto文件,当import "google/protobuf/timestamp.proto"时就是从此处导入。
安装说明:https://grpc.io/docs/protoc-installation/
$ cd /tmp
# 1.下载Protobuf v3版本的zip包
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-x86_64.zip
# 2.解压到/usr/local/protobuf目录
$ sudo unzip protoc-21.12-osx-x86_64.zip -d /usr/local/protobuf

# 3.安装完成后将protobuf/bin加入到path环境变量
# 编辑~/.bash_profile 并添加export PATH="$PATH:/usr/local/protobuf/bin"

验证:
$ protobuf --version #libprotoc 3.21.12安装插件

接下来需要安装两个protoc的go插件,分别为:

[*]protoc-gen-go插件:
会根据.proto文件生成后缀为.pb.go的文件,该文件定义了定义的类型机器序列化方法。
[*]protoc-gen-go-grpc插件:该插件生成后缀为_grpc.pb.go的文件,该文件包含服务端要实现的接口类型以及供客户端调用的存根方法。
执行下面插件安装:
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2这两个插件会自动安装到$GOPATH的bin目录下
验证是否安装成功
$ protoc-gen-go --version #protoc-gen-go v1.28.1
$ protoc-gen-go-grpc --version #protoc-gen-go-grpc 1.2.0
安装grpc依赖

在项目目录下执行下面命令,获取grpc作为项目依赖
go get "google.golang.org/grpc"3.入门案例

定义服务

我们的第一步是gRPC服务以及方法请求和响应类型。
grpc服务是使用protocol buffers定义的。更多了解如何在.proto文件中定义服务,可以看Protocol Buffers基础教程。
//examples/proto/helloworld.proto
syntax = "proto3"; //版本声明,使用v3版本
package pb;//包名

ption go_package = "examples/pb";// 指定生成的Go代码在项目中的导入路径,代码的Go包名将用导入路径的最后一个路径组件。例如这里,代码包名将是pb。

//定义服务
service HelloService {
//定义服务方法
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}
// 定义请求消息类型.
message HelloRequest {
string name = 1;
}

// 定义响应消息类型.
message HelloReply {
string message = 1;
}
生成服务端和客户端存根代码

定义完.proto文件之后,下一步就可以使用使用proto buffer编译器生成服务端以及客户端的存根代码了。当每次重新定义服务方法后都需要重新编译.proto文件。
假如.proto文件放在$project/proto文件夹,生成的代码放在$project/pb文件夹。则执行以下命令,即可:
# 在proto目录下执行
$ protoc --go_out=../pb --go_opt=paths=source_relative \
    --go-grpc_out=../pb --go-grpc_opt=paths=source_relative \
    helloworld.proto

#或者使用 -I指定要搜索的目录。可多次指定,并将按顺序搜索目录。如果没有给定,使用当前工作目录。
$ protoc -I ./proto --go_out=./pb --go_opt=paths=source_relative   --go-g
rpc_out=./pb --go-grpc_opt=paths=source_relative helloworld.proto


go_out指定.pb.go文件的生成路径,go-grpc_out指定_grpc.pb.go文件的生成路径。编译器会自动生成服务的代码以及供客户端调用的存根代码。
//examples/pb/hello_grpc.pb.go
//生成的HelloService服务接口,就是需要在业务逻辑中实现这个接口
type HelloServiceServer interface {
// SayHello服务方法
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
// SayHelloAgain服务方法
SayHelloAgain(context.Context, *HelloRequest) (*HelloReply, error)
mustEmbedUnimplementedHelloServiceServer()
}

//这里给了一个默认的空实现结构体,需要在我们自己的Service结构体中嵌入它。
type UnimplementedHelloServiceServer struct {
}
//sayHello默认实现方法
func (UnimplementedHelloServiceServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
//SayHelloAgain默认实现方法
func (UnimplementedHelloServiceServer) SayHelloAgain(context.Context, *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHelloAgain not implemented")
}
func (UnimplementedHelloServiceServer) mustEmbedUnimplementedHelloServiceServer() {}

编写业务逻辑

当你重新生成pb文件后,你需要实现_grpc.pb.go文件中定义的服务接口。
假如业务代码放在$project/internal/helloworld.go下
package service

import (
        "context"
        "user-service/pb"
)

type HelloService struct {
        pb.UnimplementedHelloServiceServer //潜入默认的结构体
}
//在服务方法内编写业务逻辑
func (s *HelloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
        return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
//在服务方法内编写业务逻辑
func (s *HelloService) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
        return &pb.HelloReply{Message: "Hello again " + in.GetName()}, nil
}运行服务

server端:
//examples/main.go
package main

import (
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        "log"
        "net"
        "user-service/internal/service"
        "user-service/pb"
)

func main() {
        lis, err := net.Listen("tcp", "127.0.0.1:8881")
        if err != nil {
                log.Fatalf("failed to listen: %v", err)
                return
        }
        //New一个grpc Server对象,这里禁用认证
        grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
        //注册自己的服务
        pb.RegisterHelloServiceServer(grpcServer, &service.HelloService{})
        //启动监听程序
        if grpcServer.Serve(lis) != nil {
                log.Fatalf("start grpc fail,err: %v", err)
                return
        }
}Client端:
package main

import (
        "context"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        "log"
        "user-service/pb"
)

func main() {
        // 连接到server端,此处禁用安全传输
        conn, err := grpc.Dial("127.0.0.1:8881", grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("connect grpc fail,err:%v", err)
        }
        defer conn.Close()
        c := pb.NewHelloServiceClient(conn)
        ctx := context.Background()
        r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "zhangsan"})
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.GetMessage())
}至此,一个基础的rpc服务就开发完成了。
4. 高级功能

本节将简单介绍其他的一些功能、特性,开发中需要的时候,再查阅相关使用。
4.1 grpc流

前面介绍的只是普通的grpc,除此之外,依托于HTTP2,gRPC还支持流式RPC(Streaming RPC)。
// 普通 RPC
rpc SimplePing(PingRequest) returns (PingReply);
// 客户端流式 RPC
rpc ClientStreamPing(stream PingRequest) returns (PingReply);
// 服务器端流式 RPC
rpc ServerStreamPing(PingRequest) returns (stream PingReply);
// 双向流式 RPC
rpc BothStreamPing(stream PingRequest) returns (stream PingReply);4.2 metadata

元数据(metadata)是指在处理RPC请求和响应过程中需要但又不属于具体业务(例如身份验证详细信息)的信息,采用键值对列表的形式,其中键是string类型,值通常是[]string类型,但也可以是二进制数据。gRPC中的 metadata 类似于我们在 HTTP headers中的键值对,元数据可以包含认证token、请求标识和监控标签等。
metadata中的键是大小写不敏感的,由字母、数字和特殊字符-、_、.组成并且不能以grpc-开头(gRPC保留自用),二进制值的键名必须以-bin结尾。
元数据对 gRPC 本身是不可见的,我们通常是在应用程序代码或中间件中处理元数据,我们不需要在.proto文件中指定元数据。
4.3错误处理

gRPC提供了自身的错误实现。在rpc服务的方法中应该返回nil或来自status.Status类型的错误。
如下面代码status.Errorf实际是一个status.Status对象。
func (UnimplementedHelloServiceServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
        return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}status包源码
//package path "google.golang.org/genproto/googleapis/rpc/status"
type Status struct {
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`

Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Details []*anypb.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"`
}

//google.golang.org/grpc@v1.52.3/internal/status/status.go
type Status struct {
s *spb.Status
}
//google.golang.org/grpc@v1.52.3/internal/status/status.go
//grpc返回的Error对象
type Error struct {
    //Status表示RPC状态代码、消息和详细信息。
s *Status
}

func (e *Error) Error() string {
return e.s.String()
}

// GRPCStatus returns the Status represented by se.
func (e *Error) GRPCStatus() *Status {
return e.s
}
创建错误对象
err := status.Error(codes.NotFound, "some description")
//status.Error函数实现
//func Error(c codes.Code, msg string) error {
//        return New(c, msg).Err()
//}
或者
st := status.New(codes.NotFound, "some description")
err := st.Err()// 转为error类型
客户端可以通过下面的方法将error类型的接口转换回status.Status以获取code和Message等信息。
r, err := client.Login(context.Background(), &request)
if err != nil {
        s := status.Convert(err)
        fmt.Printf("code=%d,message=%s\n", s.Code(), s.Message())
        return
}4.4加密或认证(grpc.Creds)

前面我们的 gRPC 配置加密或认证,属于不安全的连接(insecure connection)。如:
服务端:
//New一个grpc Server对象,这里禁用认证
s := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
lis, _ := net.Listen("tcp", "127.0.0.1:8972")
// error handling omitted
s.Serve(lis)客户端:
conn, _ := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
client := pb.NewGreeterClient(conn)使用服务器身份验证 SSL/TLS

gRPC 内置支持 SSL/TLS,可以通过 SSL/TLS证书建立安全连接,对传输的数据进行加密处理。
Server端使用credentials.NewServerTLSFromFile函数分别加载证书server.cert和秘钥server.key。
creds, _ := credentials.NewServerTLSFromFile(certFile, keyFile)
s := grpc.NewServer(grpc.Creds(creds))
lis, _ := net.Listen("tcp", "127.0.0.1:8972")
// error handling omitted
s.Serve(lis)而client端使用上一步生成的证书文件——server.cert与服务端建立安全连接。
creds, _ := credentials.NewClientTLSFromFile(certFile, "")
conn, _ := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(creds))
// error handling omitted
client := pb.NewGreeterClient(conn)
// ...注意:在生产环境对外通信时通常需要使用受信任的CA证书。
4.5拦截器(中间件)

gRPC 为在每个 ClientConn/Server 基础上实现和安装拦截器提供了一些简单的 API。 拦截器拦截每个 RPC 调用的执行。用户可以使用拦截器进行日志记录、身份验证/授权、指标收集以及许多其他可以跨 RPC 共享的功能。
在gRPC中拦截器分为普通拦截器和流拦截器两类。普通拦截器也成为一元拦截器。流拦截器用于流式调用。而客户端和服务端都有自己的普通拦截器和流拦截器类型。
自定义拦截器

//一元服务器拦截器函数类型
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

//流式服务器拦截器函数类型
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error这里自定义一个recover拦截器
func RecoverInterceptorDemo() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
defer func() {
   if r := recover(); r != nil {
    fmt.Println("recover panic...")
    err = status.Errorf(codes.Internal, "Panic,%v", r)
   }
}()
fmt.Println("in interceptor...")
resp, err := handler(ctx, req)
fmt.Println("leave interceptor...")
return resp, err
}
}
社区中有很多开源的常用的grpc中间件——go-grpc-middleware,像recover拦截器、promethues等拦截器。
使用拦截器

服务端注册拦截器
s := grpc.NewServer(
        grpc.Creds(creds),
        grpc.UnaryInterceptor(unaryInterceptor),
        grpc.StreamInterceptor(streamInterceptor),
)客户端注册拦截器
conn, err := grpc.Dial("127.0.0.1:8972",
        grpc.WithTransportCredentials(creds),
        grpc.WithUnaryInterceptor(unaryInterceptor),
        grpc.WithStreamInterceptor(streamInterceptor),
)示例:
var opts []grpc.ServerOption = []grpc.ServerOption{
        //证书
        grpc.Creds(insecure.NewCredentials()),
        //流式拦截器
        //grpc.StreamInterceptor(nil),
        //一元拦截器
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
                grpc_recovery.UnaryServerInterceptor(),
                grpc_prometheus.UnaryServerInterceptor,
        )),
}
grpcServer := grpc.NewServer(opts...)5.相关阅读:


[*]github用户rpc服务案例:https://github.com/jacky-htg/user-service
[*]protobuf文档:https://protobuf.dev/programming-guides/proto3/
[*]官方文档地址:https://grpc.io/docs/languages/go/quickstart/
页: [1]
查看完整版本: 一文了解gRPC