概述
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