一 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 client默认的超时时间_客户端

案例(服务端)

// 使用第三方拦截器,使用了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目录下

0.6.7版本exe

最新版本查找

下载完成,放到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 client默认的超时时间_客户端_02

六 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
}