目錄
- 一、grpc metadata機制
- 1.1 proto
- 1.2 生成go檔案
- 1.3 服務端
- 1.4 客戶端
- 二、grpc 攔截器interceptor
- 2.1 服務端攔截器grpc.UnaryInterceptor(interceptor)
- 2.2 客戶端攔截器
- 2.3 開源攔截器
- 三、透過metadata+攔截器實現認證
- 3.1 自定義
- (1)服務端
- (2)客戶端
- (3)proto
- 3.2 WithPerRPCCredentials
- (1)服務端(程式碼不變)
- (2)客戶端(使用內建的)
- 3.1 自定義
- 四、驗證器
- 4.1 linux/mac安裝
- 4.2 win安裝
- 4.3 程式碼
- (1)proto--hello.proto
- (2)proto--validate.proto
- (3)執行命令
- (4)服務端
- (5)客戶端
- 五、grpc 狀態碼
- 六、grpc 錯誤
- 6.1 服務端
- 6.2 客戶端
- 七、grpc超時機制
- 7.1 客戶端使用ctx
- 7.2 服務端睡5s
一、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 自定義
(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)
}
(2)客戶端
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)
}
(3)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
(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)
}
(2)客戶端(使用內建的)
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 程式碼
(1)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}$",
}];
}
(2)proto--validate.proto
https://github.com/envoyproxy/protoc-gen-validate/blob/main/validate/validate.proto
(3)執行命令
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
(4)服務端
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)
//}
(5)客戶端
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
}