概述
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方法在建立连接时设置。流拦截器与一元拦截器功能大致相同,具体应用可参考库源码或相关文档。