一文了解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]