來(lái)瞧一瞧 gRPC的攔截器

[TOC]

瞧一瞧 gRPC的攔截器

攔截器.jpg
image

上一次說(shuō)到gRPC的認(rèn)證總共有4種,其中介紹了常用且重要的2種

  • 可以使用openssl做認(rèn)證證書,進(jìn)行認(rèn)證
  • 客戶端還可以將數(shù)據(jù)放到metadata中,服務(wù)器進(jìn)行認(rèn)證

可是朋友們,有沒(méi)有想過(guò),要是每一個(gè)客戶端與服務(wù)端通信的接口都進(jìn)行一次認(rèn)證,那么這是否會(huì)非常多余呢,且每一個(gè)接口的實(shí)現(xiàn)都要做一次認(rèn)證,這真的太難受了

咱作為程序員,就應(yīng)該要探索高效的方法來(lái)解決一些繁瑣復(fù)雜冗余的事情。

今天我們來(lái)分享一下gRPC的interceptor,即攔截器 ,類似于web框架里的中間件。

中間件是什么?

是一類提供系統(tǒng)軟件和應(yīng)用軟件之間連接、便于軟件各部件之間的溝通的計(jì)算機(jī)軟件,它為軟件應(yīng)用程序提供操作系統(tǒng)以外的服務(wù),被形象的描述為“軟件膠水”

直白的說(shuō),中間件即是一個(gè)系統(tǒng)軟件和應(yīng)用軟件之間的溝通橋梁。例如他可以記錄響應(yīng)時(shí)長(zhǎng)、記錄請(qǐng)求和響應(yīng)數(shù)據(jù)日志

中間件可以在攔截到發(fā)送給 handler 的請(qǐng)求,且可以攔截 handler 返回給客戶端的響應(yīng)

攔截器是什么?

攔截器是gRPC生態(tài)中的中間件

可以對(duì)RPC的請(qǐng)求和響應(yīng)進(jìn)行攔截處理,而且既可以在客戶端進(jìn)行攔截,也可以對(duì)服務(wù)器端進(jìn)行攔截。

image

攔截器能做什么?

哈哈,他能做的可多了,最終要的一點(diǎn)是,攔截器可以做統(tǒng)一接口的認(rèn)證工作,再也不需要每一個(gè)接口都做一次認(rèn)證了,多個(gè)接口多次訪問(wèn),只需要在統(tǒng)一個(gè)地方認(rèn)證即可

這是不是大大的提高了接口的使用和認(rèn)證效率了呢,同時(shí)還可以減少代碼的冗余度

攔截器有哪些分類呢?

根據(jù)不同的側(cè)重點(diǎn),會(huì)有如下2種分類:

image

側(cè)重點(diǎn)不同,分類的攔截器也不同,不過(guò)使用的方式都是大同小異的。

如何使用攔截器?

服務(wù)端會(huì)用到的方法

image

UnaryServerInterceptor提供了一個(gè)鉤子來(lái)攔截服務(wù)器上單一RPC的執(zhí)行,攔截器負(fù)責(zé)調(diào)用處理程序來(lái)完成RPC

其中參數(shù)中的UnaryHandler定義了由UnaryServerInterceptor調(diào)用的處理程序

客戶端會(huì)用到的方法

image
type UnaryClientInterceptor func(
    ctx context.Context,        // 上下文
    method string,              // RPC的名字,例如此處我們使用的是gRPC
    req, reply interface{},     // 對(duì)應(yīng)的請(qǐng)求和響應(yīng)消息
    cc *ClientConn,             // cc是調(diào)用RPC的ClientConn
    invoker UnaryInvoker,       // invoker是完成RPC的處理程序,主要是調(diào)用它是攔截器
    opts ...CallOption) error   // opts包含所有適用的調(diào)用選項(xiàng),包括來(lái)自ClientConn的默認(rèn)值以及每個(gè)調(diào)用選項(xiàng)

整體案例代碼結(jié)構(gòu)

代碼結(jié)構(gòu)與上2篇分享到的結(jié)構(gòu)一致,本次攔截器,是統(tǒng)一做認(rèn)證,把認(rèn)證的地方統(tǒng)一放在同一個(gè)位置,而不是分散到每一個(gè)接口

若需要具體的proto源碼,可以查看我的上一期文章,如下為代碼結(jié)構(gòu)圖示

image

開(kāi)始書寫案例

  • 在原有代碼基礎(chǔ)上加入interceptor的功能,目前案例中注冊(cè)一個(gè)攔截器
  • gRPC + openssl + token + interceptor

server.go

  • 主要加入UnaryServerInterceptor來(lái)對(duì)攔截器的應(yīng)用
package main

import (
   "fmt"
   "google.golang.org/grpc/codes"
   "google.golang.org/grpc/metadata"
   "log"
   "net"

   pb "myserver/protoc/hi"

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入grpc認(rèn)證包
)

const (
   // Address gRPC服務(wù)地址
   Address = "127.0.0.1:9999"
)

// 定義helloService并實(shí)現(xiàn)約定的接口
type HiService struct{}

// HiService Hello服務(wù)
var HiSer = HiService{}

// SayHello 實(shí)現(xiàn)Hello服務(wù)接口
func (h HiService) SayHi(ctx context.Context, in *pb.HiRequest) (*pb.HiResponse, error) {

   // 解析metada中的信息并驗(yàn)證
   md, ok := metadata.FromIncomingContext(ctx)
   if !ok {
      return nil, grpc.Errorf(codes.Unauthenticated, "no token ")
   }

   var (
      appId  string
      appKey string
   )

   // md 是一個(gè) map[string][]string 類型的
   if val, ok := md["appid"]; ok {
      appId = val[0]
   }

   if val, ok := md["appkey"]; ok {
      appKey = val[0]
   }

   if appId != "myappid" || appKey != "mykey" {
      return nil, grpc.Errorf(codes.Unauthenticated, "token invalide: appid=%s, appkey=%s", appId, appKey)
   }

   resp := new(pb.HiResponse)
   resp.Message = fmt.Sprintf("Hi %s.", in.Name)

   return resp, nil
}

// 認(rèn)證token
func myAuth(ctx context.Context) error {
   md, ok := metadata.FromIncomingContext(ctx)
   if !ok {
      return grpc.Errorf(codes.Unauthenticated, "no token ")
   }

   log.Println("myAuth ...")

   var (
      appId  string
      appKey string
   )

   // md 是一個(gè) map[string][]string 類型的
   if val, ok := md["appid"]; ok {
      appId = val[0]
   }

   if val, ok := md["appkey"]; ok {
      appKey = val[0]
   }

   if appId != "myappid" || appKey != "mykey" {
      return grpc.Errorf(codes.Unauthenticated, "token invalide: appid=%s, appkey=%s", appId, appKey)
   }

   return nil
}

// interceptor 攔截器
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
   // 進(jìn)行認(rèn)證
   log.Println("interceptor...")
   err := myAuth(ctx)
   if err != nil {
      return nil, err
   }

   // 繼續(xù)處理請(qǐng)求
   return handler(ctx, req)
}

func main() {
   log.SetFlags(log.Ltime | log.Llongfile)

   listen, err := net.Listen("tcp", Address)
   if err != nil {
      log.Panicf("Failed to listen: %v", err)
   }

   var opts []grpc.ServerOption

   // TLS認(rèn)證
   creds, err := credentials.NewServerTLSFromFile("./keys/server.pem", "./keys/server.key")
   if err != nil {
      log.Panicf("Failed to generate credentials %v", err)
   }

   opts = append(opts, grpc.Creds(creds))

   // 注冊(cè)一個(gè)攔截器
   opts = append(opts, grpc.UnaryInterceptor(interceptor))

   // 實(shí)例化grpc Server, 并開(kāi)啟TLS認(rèn)證,其中還有攔截器
   s := grpc.NewServer(opts...)

   // 注冊(cè)HelloService
   pb.RegisterHiServer(s, HiSer)

   log.Println("Listen on " + Address + " with TLS and interceptor")

   s.Serve(listen)
}

client.go

  • 主要加入UnaryClientInterceptor來(lái)對(duì)攔截器的應(yīng)用
package main

import (
   "log"
   pb "myclient/protoc/hi" // 引入proto包
   "time"

   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/credentials" // 引入grpc認(rèn)證包
   "google.golang.org/grpc/grpclog"
)

const (
   // Address gRPC服務(wù)地址
   Address = "127.0.0.1:9999"
)

var IsTls = true

// myCredential 自定義認(rèn)證
type myCredential struct{}

// GetRequestMetadata 實(shí)現(xiàn)自定義認(rèn)證接口
func (c myCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
   return map[string]string{
      "appid":  "myappid",
      "appkey": "mykey",
   }, nil
}

// RequireTransportSecurity 自定義認(rèn)證是否開(kāi)啟TLS
func (c myCredential) RequireTransportSecurity() bool {
   return IsTls
}

// 客戶端攔截器
func Clientinterceptor(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...)
   log.Printf("method == %s ; req == %v ; rep == %v ; duration == %s ; error == %v\n", method, req, reply, time.Since(start), err)
   return err
}

func main() {
   log.SetFlags(log.Ltime | log.Llongfile)
   // TLS連接  記得把xxx改成你寫的服務(wù)器地址

   var err error
   var opts []grpc.DialOption

   if IsTls {
      //打開(kāi)tls 走tls認(rèn)證
      creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "www.eline.com")
      if err != nil {
         log.Panicf("Failed to create TLS mycredentials %v", err)
      }
      opts = append(opts, grpc.WithTransportCredentials(creds))
   } else {
      opts = append(opts, grpc.WithInsecure())
   }

   // 自定義認(rèn)證,new(myCredential 的時(shí)候,由于我們實(shí)現(xiàn)了上述2個(gè)接口,因此new的時(shí)候,程序會(huì)執(zhí)行我們實(shí)現(xiàn)的接口
   opts = append(opts, grpc.WithPerRPCCredentials(new(myCredential)))

   // 加上攔截器
   opts = append(opts, grpc.WithUnaryInterceptor(Clientinterceptor))

   conn, err := grpc.Dial(Address, opts...)
   if err != nil {
      grpclog.Fatalln(err)
   }

   defer conn.Close()

   // 初始化客戶端
   c := pb.NewHiClient(conn)

   // 調(diào)用方法
   req := &pb.HiRequest{Name: "gRPC"}
   res, err := c.SayHi(context.Background(), req)
   if err != nil {
      log.Panicln(err)
   }
   log.Println(res.Message)

   // 故意再調(diào)用一次
   res, err = c.SayHi(context.Background(), req)
   if err != nil {
      log.Panicln(err)
   }

   log.Println(res.Message)
}

實(shí)際效果展示

image
image

注意,服務(wù)器只能配置一個(gè) UnaryInterceptorStreamClientInterceptor,否則會(huì)報(bào)錯(cuò),客戶端也是,雖然不會(huì)報(bào)錯(cuò),但是只有最后一個(gè)才起作用。 如果你想配置多個(gè),可以使用攔截器鏈,如go-grpc-middleware,或者自己實(shí)現(xiàn)。

  • 服務(wù)端的攔截器
    • UnaryServerInterceptor -- 單向調(diào)用的攔截器
    • StreamServerInterceptor -- stream調(diào)用的攔截器
  • 客戶端的攔截器
    • UnaryClientInterceptor
    • StreamClientInterceptor

上述攔截器無(wú)論是單向調(diào)用的攔截器 還是 stream調(diào)用的攔截器 用法都大同小異

// 服務(wù)端
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

// 客戶端
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

type StreamClientInterceptor func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

最后分享社區(qū)內(nèi)用到的攔截器(還應(yīng)該有更多...)

最后與大家分享幾個(gè)社區(qū)內(nèi)用到的攔截器

用于身份驗(yàn)證攔截器

interceptor鏈?zhǔn)焦δ艿膸?kù),可以將單向的或者流式的攔截器組合

為上下文增加Tag map對(duì)象

日志框架

可以為客戶端增加重試的功能

好了,本次就到這里,下一次分享 gRPC的請(qǐng)求追蹤,

技術(shù)是開(kāi)放的,我們的心態(tài),更應(yīng)是開(kāi)放的。擁抱變化,向陽(yáng)而生,努力向前行。

我是小魔童哪吒,歡迎點(diǎn)贊關(guān)注收藏,下次見(jiàn)~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • gRPC 作為一套獨(dú)立的 RPC 框架,像攔截器這種功能當(dāng)然也是不可或缺的,框架自帶的攔截器更多是基于框架本身出發(fā)...
    BeckJin閱讀 2,795評(píng)論 0 3
  • 在微服務(wù)架構(gòu)中,調(diào)用鏈?zhǔn)锹L(zhǎng)而復(fù)雜的,要了解其中的每個(gè)環(huán)節(jié)及其性能,你需要全鏈路跟蹤。它的原理很簡(jiǎn)單,你可以在每個(gè)...
    51reboot閱讀 1,015評(píng)論 0 1
  • 文檔不全仍需要補(bǔ)充,具體可以參考 Grpc.Core.Api/Interceptors .Net 中的 Grpc ...
    ZeroingX閱讀 4,718評(píng)論 0 2
  • 在微服務(wù)架構(gòu)中,調(diào)用鏈?zhǔn)锹L(zhǎng)而復(fù)雜的,要了解其中的每個(gè)環(huán)節(jié)及其性能,你需要全鏈路跟蹤。 它的原理很簡(jiǎn)單,你可以在每...
    倚天碼農(nóng)閱讀 1,102評(píng)論 0 0
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來(lái)的情緒。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了,難過(guò)就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,459評(píng)論 2 7

友情鏈接更多精彩內(nèi)容