grpc是一个高性能、开源、通用的RPC框架,由Google推出,基于http2协议标准设计开发,默认采用protocol buffer数据序列化协议,支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务,并且为客户端和服务端自动生成可靠的功能库。
本文主要介绍除了基本的rpc通信功能外,grpc支持的扩展功能。
gRPC默认内置了两种认证方式:
同时,gRPC提供了接口用于扩展自定义认证方式。
// TLS认证
creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem", "../../keys/server.key")
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
// 实例化grpc Server, 并开启TLS认证
s := grpc.NewServer(grpc.Creds(creds))
// TLS连接
creds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name")
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
md, ok := metadata.FromContext(ctx)
if !ok {
return nil, grpc.Errorf(codes.Unauthenticated, "无Token认证信息")
}
var (
appid string
appkey string
)
if val, ok := md["appid"]; ok {
appid = val[0]
}
if val, ok := md["appkey"]; ok {
appkey = val[0]
}
if appid != "101010" || appkey != "i am key" {
return nil, grpc.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
}
google.golang.org/grpc/credentials/oauth包已实现了用于Google API的oauth和jwt验证的方法
每次调用,token信息会通过请求的metadata传输到服务端。
// customCredential 自定义认证
type customCredential struct{}
// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "101010",
"appkey": "i am key",
}, nil
}
// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {
return OpenTLS
}
var opts []grpc.DialOption
// 使用自定义认证
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
conn, err := grpc.Dial(Address, opts...)
grpc服务端和客户端都提供了interceptor功能,功能类似middleware,很适合在这里处理验证、日志等流程。
Grpc同时支持在客户端和服务端添加拦截器。
还是以token认证为例
// auth 验证Token
func auth(ctx context.Context) error {
md, ok := metadata.FromContext(ctx)
if !ok {
return grpc.Errorf(codes.Unauthenticated, "无Token认证信息")
}
var (
appid string
appkey string
)
if val, ok := md["appid"]; ok {
appid = val[0]
}
if val, ok := md["appkey"]; ok {
appkey = val[0]
}
if appid != "101010" || appkey != "i am key" {
return grpc.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
}
return nil
}
// interceptor 拦截器
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
err := auth(ctx)
if err != nil {
return nil, err
}
// 继续处理请求
return handler(ctx, req)
}
// server启动时加载拦截器
var opts []grpc.ServerOption
// 注册interceptor
opts = append(opts, grpc.UnaryInterceptor(interceptor))
// 实例化grpc Server
s := grpc.NewServer(opts...)
添加打印日志的拦截器为例
// interceptor 客户端拦截器
func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
grpclog.Printf("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
return err
}
// 拨号时指定客户端interceptor
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
conn, err := grpc.Dial(Address, opts...)
streaming rpc相比于simple rpc来说可以很好的解决一个接口发送大量数据的场景。
比如一个订单导出的接口有20万条记录,如果使用simple rpc来实现的话。那么我们需要一次性接收到20万记录才能进行下一步的操作。但是如果我们使用streaming rpc那么我们就可以接收一条记录处理一条记录,直到所以的数据传输完毕。这样可以较少服务器的瞬时压力,也更有及时性
service StreamService {
rpc OrderList(OrderSearchParams) returns (stream OrderList){}; //服务端流式,下发订单列表
rpc UploadFile(stream ImageList) returns (uploadResponse){}; //客户端流式,上传图片
rpc SumData(stream SumData) returns (stream SumData){}; //双向流式
}
server := grpc.NewServer()
proto.RegisterStreamServiceServer(server, &StreamServices{})
streamClient = proto.NewStreamServiceClient(connect)
grpc流式接口的底层实现基于http2.0的stream,我们回忆http2.0的基本概念,从顶而下分为stream, message, frame。其中一个stream由若干message组成,因此它超越了原先的一条请求一条答复的传统交互模式。
grpc内置了客户端和服务端的请求追踪,基于golang.org/x/net/trace包实现,默认是开启状态,可以查看事件和请求日志,对于基本的请求状态查看调试也是很有帮助的,客户端与服务端基本一致
func startTrace() {
trace.AuthRequest = func(req *http.Request) (any, sensitive bool) {
return true, true
}
go http.ListenAndServe(":50051", nil)
grpclog.Println("Trace listen on 50051")
}
这里我们开启一个http服务监听50051端口,用来查看grpc请求的trace信息
访问:localhost:50051/debug/events,可以看到服务端注册的服务和服务正常启动的事件信息。
访问:localhost:50051/debug/requests,这里可以显示最近的请求状态,包括请求的服务、参数、耗时、响应,对于简单的状态查看还是很方便的,默认值显示最近10条记录。
Grpc原生并没有提供全套的服务注册与发现的组件,但在其api接口中仍体现了负载均衡的思想
在实现其上述思想时,通常需要借助zk,etcd等开源组件。
err = grpclb.Register(*serv, "127.0.0.1", *port, *reg, time.Second*10, 15) // 服务注册
grpclb.UnRegister() // 取消服务注册
r := grpclb.NewResolver(*serv) // watcher etcd
b := grpc.RoundRobin(r) // 创建负载均衡实例
conn, err := grpc.DialContext(ctx, *reg, grpc.WithInsecure(), grpc.WithBalancer(b)) // 与所有的下游服务建立连接
如果我们的grpc服务仍然需要接入部分http协议的请求,通常有两种做法:1是改造现有的服务,同时支持两种协议;2是引入http网关。Google官方提供一套流程直接支持第二种做法。
通过protobuf的自定义option实现了一个网关,服务端同时开启gRPC和HTTP服务,HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据返回给客户端。
使用这种方式,非常方便的接入了http协议请求。