gRPC-攔截器簡單使用


概述

gRPC作為通用RPC框架,內置了攔截器功能。包括服務器端的攔截器和客戶端攔截器,使用上大同小異。主要作用是在rpc調用的前后進行額外處理。

從客戶端角度講,可以在請求發起前,截取到請求參數並修改;也可以修改服務器的響應參數。

示例

以下寫一個簡單的示例來描述具體的功能實現。以Go語言為例,其它語言的gRPC庫應該也有類似功能,具體請參考文檔。

為使示例簡單,簡化了對錯誤的處理。並且只展示了部分代碼,完整項目請參考GitHub倉庫pnnh/suji-go

接口描述文件

syntax = "proto3";

package suji;

service Suji {
    rpc Say(SayRequest) returns (SayReply) {}
}

message SayRequest {
    string msg = 1;
}

message SayReply {
    string msg = 1;
}

最初實現

服務器main方法

func main() {
	lis, err := net.Listen("tcp", "0.0.0.0:1301")
	if err != nil {
		log.Fatalln("監聽出錯", err)
		return
	}

	grpcServer := grpc.NewServer()
	suji.RegisterSujiServer(grpcServer, &server.SujiServer{})

	if err = grpcServer.Serve(lis); err != nil {
		log.Fatalln("服務停止", err)
	}
}

客戶端main方法

func main() {
	addr := "127.0.0.1:1301"
	c := client.LinkSujiServer(addr)
    
    rep := client.Say(c, msg)
	log.Println("收到:", rep.Msg)
}

這里通過LinkSujiServer方法來連接至gRPC服務器,調用了Say接口,並打印了服務器返回值。

LinkSujiServer方法如下

func LinkSujiServer(target string) suji.SujiClient {
	conn, err := grpc.DialContext(context.Background(), target, grpc.WithInsecure())
	if err != nil {
		log.Fatalln("鏈接至服務出錯", err, target)
	}
	return suji.NewSujiClient(conn)
}

Say接口客戶端調用方式如下:


func Say(client suji.SujiClient, msg string) *suji.SayReply {
	request := &suji.SayRequest{Msg: msg}

	reply, err := client.Say(context.Background(), request)
	if err != nil {
		log.Fatalln("調用出錯", err)
	}
	return reply
}

Say接口服務端實現如下,將收到的內容原樣返回給調用者:

func (s *SujiServer) Say(ctx context.Context, req *suji.SayRequest) (*suji.SayReply, error) {
	log.Println("收到:", req.Msg)

	reply := &suji.SayReply{Msg: req.Msg}

	return reply, nil
}

運行這段代碼,將分別打印以下結果

客戶端:

2019/08/15 18:19:59 發送: 你好
2019/08/15 18:19:59 收到: 你好

服務器:

2019/08/15 18:19:59 收到: 你好
2019/08/15 18:19:59 回復: 你好

攔截器實現

原本很簡單的接口調用,現在我們通過gRPC客戶端攔截器給這段對話加點料。

我們將通過攔截器,截取並篡改客戶端發送給服務器的內容,然后把服務器返回的內容也篡改掉。這一切是悄悄在攔截器中進行的,調用的發起方和接收方並不知曉。

定義攔截器方法

func callInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn,
	invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {

	if reqParam, ok := req.(*suji.SayRequest); ok {
		newMsg := strings.Replace(reqParam.Msg, "喜歡", "討厭", 1)
		req = &suji.SayRequest{Msg: newMsg}
	}

	err := invoker(ctx, method, req, reply, cc, opts...)
	if err != nil {
		log.Println("接口調用出錯", method, err)
		return err
	}

	if replyParam, ok := reply.(*suji.SayReply); ok {
		newMsg := strings.Replace(replyParam.Msg, "討厭", "喜歡", 1)
		replyParam.Msg = newMsg
	}

	return nil
}

方法稍后解釋,這里先修改連接服務器的方法,加入攔截器選項:


func LinkSujiServer(target string) suji.SujiClient {
	conn, err := grpc.DialContext(context.Background(), target, grpc.WithInsecure(),
		grpc.WithUnaryInterceptor(callInterceptor))
	if err != nil {
		log.Fatalln("鏈接至服務出錯", err, target)
	}
	return suji.NewSujiClient(conn)
}

注意新增的grpc.WithUnaryInterceptor(callInterceptor)這一行。

gRPC運行時將會為我們定義的callInterceptor傳入幾個有用的參數。其中method是調用接口的路徑,req和reply分別為對應接口的請求和輸出參數。而invoker參數是一個方法,用於執行原本的RPC請求,如果調用這個方法,則RPC請求就不會發到服務器。

在這里,我們通過判斷請求和響應類型,並對參數進行篡改。同時為了使示例更有趣,簡單修改了下main函數代碼。

客戶端main方法

func main() {
	addr := "127.0.0.1:1301"

	c := client.LinkSujiServer(addr)

	msg := "我喜歡你"
	log.Println("發送:", msg)
	rep := client.Say(c, msg)

	log.Println("收到:", rep.Msg)

	if strings.Contains(rep.Msg, "喜歡") {
		log.Println("內心:", "好開心啊")
	}
}

服務器Say方法

func (s *SujiServer) Say(ctx context.Context, req *suji.SayRequest) (*suji.SayReply, error) {
	log.Println("收到:", req.Msg)

	reply := &suji.SayReply{}
	if strings.Contains(req.Msg, "討厭") {
		reply.Msg = "我也討厭你"
	}
	log.Println("回復:", reply.Msg)
	log.Println("內心:", "沙雕")

	return reply, nil
}

來看下輸出感受下雙方的內心吧:

客戶端輸出:

2019/08/15 19:07:14 發送: 我喜歡你
2019/08/15 19:07:14 收到: 我也喜歡你
2019/08/15 19:07:14 內心: 好開心啊

服務器輸出:

2019/08/15 19:07:14 收到: 我討厭你
2019/08/15 19:07:14 回復: 我也討厭你
2019/08/15 19:07:14 內心: 沙雕

最后

gRPC除了一元攔截器以外也提供了流攔截器設置方法,通過grpc.WithStreamInterceptor方法在建立連接時設置。流攔截器與一元攔截器功能大致相同,具體應用可參考庫源碼或相關文檔。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM