python grpc拦截器

概述

python grpc拦截器的proposal:https://github.com/grpc/proposal/blob/master/L13-python-interceptors.md#server-side-implementation
python grpc拦截器的官方文档,除了API Reference, 就只有这个了,再次吐槽下grpc的官方文档
通过这个proposal来了解拦截器的使用方法,还是有些费劲

python grpc拦截器设计上,要求每个自定义拦截器要负责调用下一个拦截器,调用下一个拦截器的方法称作continuation, eg:

class _ExampleClientInterceptor(grpc.UnaryUnaryClientInterceptor):

    def intercept_unary_unary(self, continuation, client_call_details, request):
        new_details, new_request = 
        
        # 留意这里的调用,没有这个调用,后面的拦截器以及真正的rpc函数都不会执行了
        response = continuation(new_details, new_request)

        new_response = 
        return new_response  # It's both a grpc.Call and a grpc.Future.

拦截器包括客户端实现和服务端实现

client端拦截器

使用示例:

class ExampleClientInterceptor(grpc.UnaryUnaryClientInterceptor):
    def intercept_unary_unary(self, continuation, client_call_details, request):
        metadata = []
        if client_call_details.metadata is not None:
            metadata = list(client_call_details.metadata)
        metadata.append((
            'x-custom-1',
            '123',
        ))
        client_call_details = _ClientCallDetails(
            client_call_details.method, client_call_details.timeout, metadata,
            client_call_details.credentials, client_call_details.wait_for_ready,
            client_call_details.compression)
        return continuation(client_call_details, request)

with grpc.insecure_channel('localhost:50051') as channel:
    channel = grpc.intercept_channel(channel, ExampleClientInterceptor())
    stub = helloworld_pb2_grpc.GreeterStub(channel)
    response = stub.SayHello(helloworld_pb2.HelloRequest(name='you'))
print("Greeter client received: " + response.message)

客户端拦截器通过包装channel来实现, continuation封装了对原函数的调用,client_call_details就是把grpc调用的参数封装为一个对象逐层传递, 如下所示:
grpc函数的调用参数:

    def __call__(self,
                 request,
                 timeout=None,
                 metadata=None,
                 credentials=None,
                 wait_for_ready=None,
                 compression=None):
        state, call, = self._blocking(request, timeout, metadata, credentials,
                                      wait_for_ready, compression)
        return _end_unary_response_blocking(state, call, False, None)

client_call_details的封装:

 def _with_call(self,
                   request,
                   timeout=None,
                   metadata=None,
                   credentials=None,
                   wait_for_ready=None,
                   compression=None):
        client_call_details = _ClientCallDetails(self._method, timeout,
                                                 metadata, credentials,
                                                 wait_for_ready, compression)

        def continuation(new_details, request):
            (new_method, new_timeout, new_metadata, new_credentials,
             new_wait_for_ready,
             new_compression) = (_unwrap_client_call_details(
                 new_details, client_call_details))
            try:
                response, call = self._thunk(new_method).with_call(
                    request,
                    timeout=new_timeout,
                    metadata=new_metadata,
                    credentials=new_credentials,
                    wait_for_ready=new_wait_for_ready,
                    compression=new_compression)
                return _UnaryOutcome(response, call)
            except grpc.RpcError as rpc_error:
                return rpc_error
            except Exception as exception:  # pylint:disable=broad-except
                return _FailureOutcome(exception, sys.exc_info()[2])

        call = self._interceptor.intercept_unary_unary(continuation,
                                                       client_call_details,
                                                       request)
        return call.result(), call

客户端拦截器的执行流程

Application Invokes an RPC ->
    Interceptor A Start ->
        Interceptor B Start ->
            Interceptor C Start ->
                Invoke Original '*MultiCallable' ->
                Return the Response from the Server ->
            Interceptor C Returns ->
        Interceptor B Returns ->
    Interceptor A Returns ->
Application Gets the Response

客户端拦截器有如下4种:

class UnaryUnaryClientInterceptor(six.with_metaclass(abc.ABCMeta)):
    @abc.abstractmethod
    def intercept_unary_unary(self, continuation, client_call_details,
                              request): pass

class UnaryStreamClientInterceptor(six.with_metaclass(abc.ABCMeta)):
    @abc.abstractmethod
    def intercept_unary_stream(self, continuation, client_call_details,
                               request): pass

class StreamUnaryClientInterceptor(six.with_metaclass(abc.ABCMeta)):
    @abc.abstractmethod
    def intercept_stream_unary(self, continuation, client_call_details,
                               request_iterator): pass

class StreamStreamClientInterceptor(six.with_metaclass(abc.ABCMeta)):
    @abc.abstractmethod
    def intercept_stream_stream(self, continuation, client_call_details,
                                request_iterator): pass

sever端拦截器

服务端拦截器的执行流程:

Server Receives a Request ->
    Interceptor A Start ->
        Interceptor B Start ->
            Interceptor C Start ->
                The Original Handler
            Interceptor C Returns Updated Handler C ->
        Interceptor B Returns Updated Handler B ->
    Interceptor A Returns Updated Handler A ->

    Invoke the Updated Handler A with the Request ->
    Updated Handler A Returns Response ->
Server Replies

这里留意服务端拦截器的执行流程和客户端是不一样的,拦截器的逻辑执行完毕之后,实际返的是一个grpc.RpcMethodHandler, 再下一步才是拿这个grpc.RpcMethodHandler执行调用,返回Response

拦截器的定义如下:

class ServerInterceptor(six.with_metaclass(abc.ABCMeta)):
    @abc.abstractmethod
    def intercept_service(self, continuation, handler_call_details): 
        pass

这里的handler_call_details是一个grpc._server.HandlerCallDetails的实例, eg:

_HandlerCallDetails(method=u'/helloworld.Greeter/SayHello', invocation_metadata=(_Metadatum(key='user-agent', value='grpc-python/1.41.1 grpc-c/19.0.0 (osx; chttp2)'),))

和如下的grpc的方法中的context不是同一个东西

class GreeterServicer(object):
  """The greeting service definition.
  """

  def SayHello(self, request, context):
    """Sends a greeting
    """
    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
    context.set_details('Method not implemented!')
    raise NotImplementedError('Method not implemented!')

所以在拦截器的intercept_service方法中,无法访问request和context,如果拦截器中要求终止请求返回错误,实现方法如下:

class _ExampleServerInterceptor(grpc.ServerInterceptor):

    def intercept_service(self, continuation, handler_call_details):
        auth_metadata = ('custom-auth-header', 'secret-key')
        if auth_metadata in handler_call_details.invocation_metadata:
            return continuation(handler_call_details)
        else:
            return grpc.unary_unary_rpc_method_handler(
                lambda request, context: context.abort(, 
))

第三方库grpc-interceptor

git地址: https://github.com/d5h-foss/grpc-interceptor

从上面的介绍可以看到grpc的interceptor使用起来比较繁琐,且server端interceptor无法访问request、response和context,grpc-interceptor的目标是:Simplified Python gRPC interceptors

服务端例子:

from grpc_interceptor import ServerInterceptor
from grpc_interceptor.exceptions import GrpcException

class ExceptionToStatusInterceptor(ServerInterceptor):
    def intercept(
        self,
        method: Callable,
        request: Any,
        context: grpc.ServicerContext,
        method_name: str,
    ) -> Any:
        """Override this method to implement a custom interceptor.
         You should call method(request, context) to invoke the
         next handler (either the RPC method implementation, or the
         next interceptor in the list).
         Args:
             method: The next interceptor, or method implementation.
             request: The RPC request, as a protobuf message.
             context: The ServicerContext pass by gRPC to the service.
             method_name: A string of the form
                 "/protobuf.package.Service/Method"
         Returns:
             This should generally return the result of
             method(request, context), which is typically the RPC
             method response, as a protobuf message. The interceptor
             is free to modify this in some way, however.
         """
        try:
            return method(request, context)
        except GrpcException as e:
            context.set_code(e.status_code)
            context.set_details(e.details)
            raise
            
interceptors = [ExceptionToStatusInterceptor()]
server = grpc.server(
    futures.ThreadPoolExecutor(max_workers=10),
    interceptors=interceptors
)

客户端例子:

from grpc_interceptor import ClientCallDetails, ClientInterceptor

class MetadataClientInterceptor(ClientInterceptor):

    def intercept(
        self,
        method: Callable,
        request_or_iterator: Any,
        call_details: grpc.ClientCallDetails,
    ):
        """Override this method to implement a custom interceptor.

        This method is called for all unary and streaming RPCs. The interceptor
        implementation should call `method` using a `grpc.ClientCallDetails` and the
        `request_or_iterator` object as parameters. The `request_or_iterator`
        parameter may be type checked to determine if this is a singluar request
        for unary RPCs or an iterator for client-streaming or client-server streaming
        RPCs.

        Args:
            method: A function that proceeds with the invocation by executing the next
                interceptor in the chain or invoking the actual RPC on the underlying
                channel.
            request_or_iterator: RPC request message or iterator of request messages
                for streaming requests.
            call_details: Describes an RPC to be invoked.

        Returns:
            The type of the return should match the type of the return value received
            by calling `method`. This is an object that is both a
            `Call `_ for the
            RPC and a `Future `_.

            The actual result from the RPC can be got by calling `.result()` on the
            value returned from `method`.
        """
        new_details = ClientCallDetails(
            call_details.method,
            call_details.timeout,
            [("authorization", "Bearer mysecrettoken")],
            call_details.credentials,
            call_details.wait_for_ready,
            call_details.compression,
        )

        return method(request_or_iterator, new_details)
        
interceptors = [MetadataClientInterceptor()]
with grpc.insecure_channel("grpc-server:50051") as channel:
    channel = grpc.intercept_channel(channel, *interceptors)
    ...

参考

  • https://github.com/grpc/proposal/blob/master/L13-python-interceptors.md#server-side-implementation
  • https://github.com/d5h-foss/grpc-interceptor
  • https://realpython.com/python-microservices-grpc/#python-microservice-monitoring-with-interceptors

你可能感兴趣的:(python grpc拦截器)