11-gRPC进阶
一 grpc metadata机制
gRPC让我们可以像本地调用一样实现远程调用,对于每一次的RPC调用中,都可能会有一些有用的数据,而这些数据就可以通过metadata来传递。metadata是以key-value的形式存储数据的,其中key是string类型,而value是[]string,即一个字符串数组类型。
metadata使得client和server能够为对方提供关于本次调用的一些信息,就像一次http请求的RequestHeader和ResponseHeader一样。
http中header的生命周周期是一次http请求,那么metadata的生命周期就是一次RPC调用
// ****************1创建metadata****************
//MD 类型实际上是map,key是string,value是string类型的slice。
type MD map[string][]string
//创建的时候可以像创建普通的map类型一样使用new关键字进行创建:
//第一种方式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
//第二种方式 key不区分大小写,会被统一转成小写。
md := metadata.Pairs(
"key1", "val1",
"key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
"key2", "val2",
)
// ****************2发送metadata*****************
md := metadata.Pairs("key", "val")
// 新建一个有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 单向 RPC
response, err := client.SomeRPC(ctx, someRequest)
// ****************3接收metadata*****************
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
md, ok := metadata.FromIncomingContext(ctx)
// do something with metadata
}
1.1 proto
syntax = "proto3";
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string reply = 1;
}
1.2 生成go文件
protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto
1.3 服务端
package main
import (
"context"
"fmt"
"go_test_learn/meta_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
// 取出meta
md, ok := metadata.FromIncomingContext(context)
if ok {
fmt.Println("获取meta失败")
// 循环打印出来
for key,value :=range md{
fmt.Println(key,value)
}
// 只取出password来
fmt.Println(md["password"][0])
}
fmt.Println(request.Name)
return &proto.HelloResponse{
Reply: "收到客户端的数据:"+ request.Name,
}, nil
}
func main() {
g := grpc.NewServer()
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
1.4 客户端
package main
import (
"context"
"fmt"
"go_test_learn/meta_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := proto.HelloRequest{Name: "lqz",}
// 1 方式一:创建md对象
//md := metadata.Pairs("name", "lqz","password","123")
// 1 方式二:创建md对象
md := metadata.New(map[string]string{"name": "lqznb", "password": "456"})
// 2 新建一个有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
//3 发送
res, err := client.Hello(ctx, &request)
if err != nil {
panic("调用方法异常")
}
fmt.Println(res.Reply)
}
二 grpc 拦截器interceptor
在 gRPC 调用过程中,我们可以拦截 RPC 的执行,在 RPC 服务执行前或执行后运行一些自定义逻辑,这在某些场景下很有用,例如身份验证、日志等,我们可以在 RPC 服务执行前检查调用方的身份信息,若未通过验证,则拒绝执行,也可以在执行前后记录下详细的请求响应信息到日志。这种拦截机制与 Gin 中的中间件技术类似,在 gRPC 中被称为 拦截器,它是 gRPC 核心扩展机制之一
2.1 服务端拦截器grpc.UnaryInterceptor(interceptor)
interceptor是自定义的拦截器函数,追踪函数的参数可知,interceptor是一个:
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
参数含义:
ctx context.Context:请求上下文
req interface{}:RPC 方法的请求参数
info *UnaryServerInfo:RPC 方法的所有信息
handler UnaryHandler:RPC 方法真正执行的逻辑
案例
//拦截器
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error){
fmt.Println("接收到了一个新的请求")
ctime:=time.Now()
res,err := handler(ctx, req)
fmt.Println("请求已完成")
fmt.Println("耗时为:",time.Since(ctime))
return res, err
}
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt)
//...
2.2 客户端拦截器
interceptor是自定义的拦截器函数,追踪函数的参数可知,interceptor是一个:
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
案例
// 创建客户端拦截器
interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error{
start := time.Now()
fmt.Println("客户端拦截器")
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Printf("耗时:%s\n", time.Since(start))
return err
}
opt := grpc.WithUnaryInterceptor(interceptor)
conn, err := grpc.Dial("127.0.0.1:50052", opt,grpc.WithTransportCredentials(insecure.NewCredentials()))
2.3 开源拦截器
https://github.com/grpc-ecosystem/go-grpc-middleware
案例(服务端)
// 使用第三方拦截器,使用了grpc_auth和grpc_recovery
myAuthFunction := func(ctx context.Context) (context.Context, error) {
fmt.Println("走了认证")
return ctx, nil
}
opt := grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_auth.UnaryServerInterceptor(myAuthFunction),
grpc_recovery.UnaryServerInterceptor(),
))
g := grpc.NewServer(opt)
// 使用拦截器结束
三 通过metadata+拦截器实现认证
3.1 自定义
服务端
package main
import (
"context"
"fmt"
"go_test_learn/interpret_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name)
return &proto.HelloResponse{
Reply: "收到客户端的数据:" + request.Name,
}, nil
}
func main() {
//服务端拦截器
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error){
fmt.Println("进行用户名密码认证")
// 取出MD
md, ok := metadata.FromIncomingContext(ctx)
if !ok { // 没有取出meta,返回错误-->这个错误的rpc的错误,状态码也是rpc的状态码
return resp,status.Error(codes.Unauthenticated,"没有携带认证信息")
}
// 携带meta,取出name和password
// 取出name来
var (
name string
password string
)
if names,ok:=md["name"];ok{
name=names[0]
}
// 取出password来
if passwords,ok:=md["password"];ok{
password=passwords[0]
}
fmt.Println(name,password)
if name=="lqz" && password=="123"{
res,err := handler(ctx, req)
return res, err
}
return resp, status.Error(codes.Unauthenticated,"用户名或密码错误")
}
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt)
// 使用拦截器结束
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
客户端
package main
import (
"context"
"fmt"
"go_test_learn/interpret_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"time"
)
func main() {
// 创建客户端拦截器
interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error{
start := time.Now()
fmt.Println("在客户端拦截器中加入用户名密码")
md := metadata.New(map[string]string{"name": "lqz", "password": "123"})
ctx = metadata.NewOutgoingContext(context.Background(), md)
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Printf("耗时:%s\n", time.Since(start))
return err
}
opt := grpc.WithUnaryInterceptor(interceptor)
conn, err := grpc.Dial("127.0.0.1:50052", opt,grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := proto.HelloRequest{Name: "lqz",}
res, err := client.Hello(context.Background(), &request)
if err != nil {
fmt.Println(err)
panic("调用方法异常")
}
fmt.Println(res.Reply)
}
proto
syntax = "proto3";
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string reply = 1;
}
3.2 WithPerRPCCredentials
服务端(代码不变)
package main
import (
"context"
"fmt"
"go_test_learn/interpret_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name)
return &proto.HelloResponse{
Reply: "收到客户端的数据:" + request.Name,
}, nil
}
func main() {
//服务端拦截器
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error){
fmt.Println("进行用户名密码认证")
// 取出MD
md, ok := metadata.FromIncomingContext(ctx)
if !ok { // 没有取出meta,返回错误-->这个错误的rpc的错误,状态码也是rpc的状态码
return resp,status.Error(codes.Unauthenticated,"没有携带认证信息")
}
// 携带meta,取出name和password
// 取出name来
var (
name string
password string
)
if names,ok:=md["name"];ok{
name=names[0]
}
// 取出password来
if passwords,ok:=md["password"];ok{
password=passwords[0]
}
fmt.Println(name,password)
if name=="lqz" && password=="123"{
res,err := handler(ctx, req)
return res, err
}
return resp, status.Error(codes.Unauthenticated,"用户名或密码错误")
}
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt)
// 使用拦截器结束
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
客户端(使用内置的)
package main
import (
"context"
"fmt"
"go_test_learn/interpret_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// 1 第一步,定义结构体,实现GetRequestMetadata和RequireTransportSecurity方法
type CommonCredential struct {
}
func (c CommonCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"name": "lqz1",
"password": "123",
}, nil
}
func (c CommonCredential) RequireTransportSecurity() bool {
//是否需要基于 TLS 认证进行安全传输
return false
}
func main() {
// 使用内置的WithPerRPCCredentials
// 1 第一步,定义结构体,实现GetRequestMetadata和RequireTransportSecurity方法
//2 第二步,生成DialOption对象
opt := grpc.WithPerRPCCredentials(CommonCredential{})
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()),opt)
if err != nil {
fmt.Println(err)
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := proto.HelloRequest{Name: "lqz"}
res, err := client.Hello(context.Background(), &request)
if err != nil {
fmt.Println(err)
panic("调用方法异常")
}
fmt.Println(res.Reply)
}
四 验证器
我们使用第三方:https://github.com/envoyproxy/protoc-gen-validate
4.1 linux/mac安装
// 执行命令
go get -d github.com/envoyproxy/protoc-gen-validate
make build
//执行make build之前需要先切换到protoc-gen-validate路径下;因为make build执行的就是这个路径下的Makefile;一定要确保在对应的路径下,这样make build才不会出错
/*
mac位置在:Users/liuqingzheng/go/pkg/mod/github.com/envoyproxy/protoc-gen-validate@v0.6.7
// 权限问题,cp到go路径下
cp -r protoc-gen-validate@v0.6.7 /Users/liuqingzheng/go/validate
// export GO111MODULE=on 开启go mod模式
*/
4.2 win安装
下载exe,将exe文件拷贝到 go的根目录的bin目录下
下载完成,放到gopath的bin路径下,加入环境变量
4.3 代码
proto--hello.proto
syntax = "proto3";
import "validate.proto";
option go_package = "./;proto";
service Hello{
rpc Hello(Person) returns(Person);
}
message Person {
uint64 id = 1 [(validate.rules).uint64.gt = 999];
string email = 2 [(validate.rules).string.email = true];
string mobile = 3 [(validate.rules).string = {
pattern: "^1[3-9][0-9]{9}$",
}];
}
proto--validate.proto
https://github.com/envoyproxy/protoc-gen-validate/blob/main/validate/validate.proto
执行命令
protoc --go_out=. --validate_out="lang=go:." hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false --validate_out="lang=go:." hello.proto
服务端
package main
import (
"awesomeProject/valdiate_proto/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.Person) (*proto.Person, error) {
fmt.Println(request.Id)
err:=request.Validate()
if err != nil {
panic(err)
}else {
return &proto.Person{
Id:1000,
Email:" 3@qq.com",
Mobile: "18953675221",
}, nil
}
}
type Validator interface {
Validate() error
}
func main() {
//服务端拦截器
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error){
fmt.Println("进行数据校验")
if r,ok:=req.(Validator);ok{
if err:=r.Validate();err!=nil{
return resp, status.Error(codes.Unauthenticated,err.Error())
}
}
return handler(ctx,req)
}
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt)
// 使用拦截器结束
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
//func main() {
// per:=new(proto.Person)
// err:=per.Validate()
// fmt.Println(err)
//}
客户端
package main
import (
"context"
"fmt"
"awesomeProject/valdiate_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
fmt.Println(err)
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := proto.Person{
Id:1000,
Email:" 3@qq.com",
Mobile: "1895636255144",
}
res, err := client.Hello(context.Background(), &request)
if err != nil {
fmt.Println(err)
panic("调用方法异常")
}
fmt.Println(res.Mobile)
}
五 grpc 状态码
gRPC提供的:https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
六 grpc 错误
6.1 服务端
status.Error(codes.Unauthenticated,"没有携带认证信息")
status.New(codes.Unauthenticated,"没有携带认证信息").Err()
status.Newf(codes.Unauthenticated,"没有携带认证信息%s","lqz").Err()
6.2 客户端
s,ok:=status.FromError(err)
if !ok{
fmt.Println(s.Message())
fmt.Println(s.Code())
}
七 grpc超时机制
7.1 客户端使用ctx
ctx,_:=context.WithTimeout(context.Background(),time.Second*3)
res, err := client.Hello(ctx, &request)
fmt.Println(err)
s,ok:=status.FromError(err)
if !ok{
fmt.Println("ok")
fmt.Println(s.Message())
fmt.Println(s.Code())
}
fmt.Println(s.Message())
fmt.Println(s.Code())
7.2 服务端睡5s
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name)
fmt.Println("睡5s")
time.Sleep(5*time.Second)
return &proto.HelloResponse{
Reply: "收到客户端的数据:" + request.Name,
}, nil
}