前言
其實Grpc攔截器是我以前研究過,但是我看網上相關C#版本的原始碼解析相對少一點,所以筆者借這篇文章給大家分享下Grpc攔截器的實現,廢話不多說,直接開講(Grpc的原始碼看著很方便,包自動都能還原成功。.Net原始碼就硬生啃。。。弄了半天沒還原成功?)。
ps:
- 本篇文章主要是講解原始碼,並不進行舉例Demo,所以讀者儘量先寫一個小Demo,看看生成的程式碼,然後伴隨著看文章。
- 如果沒有用過Grpc的讀者,可以先寫個小Demo,可以看官網點選這裡,主要是檢視下通過Proto檔案生成的程式碼的格式。
- 這篇文章講解分別從客戶端和服務端兩部分講解(實現有點不一樣),篇幅原因只講解一元呼叫的示例,其他形式的呼叫其實是類似的。
Client端
Interceptor和CallInvoker抽象類
public abstract class Interceptor
{
//一元呼叫同步攔截器
public virtual TResponse BlockingUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, BlockingUnaryCallContinuation<TRequest, TResponse> continuation)
where TRequest : class
where TResponse : class
{
return continuation(request, context);
}
//一元呼叫非同步攔截器
public virtual AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
where TRequest : class
where TResponse : class
{
return continuation(request, context);
}
}
public abstract class CallInvoker
{
//一元呼叫同步攔截器
public abstract TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request)
where TRequest : class
where TResponse : class;
//一元呼叫非同步攔截器
public abstract AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request)
where TRequest : class
where TResponse : class;
}
首先我們要理解這兩個抽象類分別是幹什麼的,上述程式碼講解:
- Interceptor我們知道,在實現自定義的攔截器時,需要繼承這個類,並對某些方法進行自定義的實現,而continuation就是呼叫下一個攔截器。
- 其實CallInvoker其實就是客戶端構造的物件,主要用於呼叫遠端服務,通過你自己實現的Demo可以看到,先建立Channel,然後通過Channe建立預設的CallInvoker,而在建立Client通過proto生成的檔案裡可以看到對應的過載建構函式。
新增攔截器
public static class CallInvokerExtensions
{
//增加一個攔截器
public static CallInvoker Intercept(this CallInvoker invoker, Interceptor interceptor)
{
return new InterceptingCallInvoker(invoker, interceptor);
}
//增加一組攔截器
public static CallInvoker Intercept(this CallInvoker invoker, params Interceptor[] interceptors)
{
//檢查是否為Null
GrpcPreconditions.CheckNotNull(invoker, nameof(invoker));
GrpcPreconditions.CheckNotNull(interceptors, nameof(interceptors));
//反轉集合,構造物件
foreach (var interceptor in interceptors.Reverse())
{
invoker = Intercept(invoker, interceptor);
}
return invoker;
}
//篇幅原因,這種方式這裡不進行講解,大家可以自己翻下原始碼看下,主要作用就是增加使用者自定義的額外報文值,類似Http請求中的Header
public static CallInvoker Intercept(this CallInvoker invoker, Func<Metadata, Metadata> interceptor)
{
return new InterceptingCallInvoker(invoker, new MetadataInterceptor(interceptor));
}
}
上述程式碼總結:
- 新增一個攔截器,則直接建立一個InterceptingCallInvoker物件返回,而它必定繼承CallInvoker。
- 新增一組攔截器,則將集合反轉,然後構造Invoker。
- 而在客戶端proto生成的程式碼中可以看到,方法的呼叫是通過CallInvoker物件呼叫的,讀者可以看一下你自己生成的程式碼。
InterceptingCallInvoker類
internal class InterceptingCallInvoker : CallInvoker
{
//下一個invoker物件
readonly CallInvoker invoker;
//當前的攔截器
readonly Interceptor interceptor;
public InterceptingCallInvoker(CallInvoker invoker, Interceptor interceptor)
{
this.invoker = GrpcPreconditions.CheckNotNull(invoker, nameof(invoker));
this.interceptor = GrpcPreconditions.CheckNotNull(interceptor, nameof(interceptor));
}
//一元同步呼叫
public override TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request)
{
return interceptor.BlockingUnaryCall(
request,
new ClientInterceptorContext<TRequest, TResponse>(method, host, options),
//當前請求引數和上下文,呼叫下一個BlockingUnaryCall
(req, ctx) => invoker.BlockingUnaryCall(ctx.Method, ctx.Host, ctx.Options, req));
}
//一元非同步呼叫
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request)
{
return interceptor.AsyncUnaryCall(
request,
new ClientInterceptorContext<TRequest, TResponse>(method, host, options),
//當前請求引數和上下文,呼叫下一個BlockingUnaryCall
(req, ctx) => invoker.AsyncUnaryCall(ctx.Method, ctx.Host, ctx.Options, req));
}
}
//預設的CallInvoker,也就是不加任何攔截器時候的實現
public class DefaultCallInvoker : CallInvoker
{
readonly Channel channel;
public DefaultCallInvoker(Channel channel)
{
this.channel = GrpcPreconditions.CheckNotNull(channel);
}
//一元同步呼叫
public override TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request)
{
var call = CreateCall(method, host, options);
return Calls.BlockingUnaryCall(call, request);
}
//一元非同步呼叫
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request)
{
var call = CreateCall(method, host, options);
return Calls.AsyncUnaryCall(call, request);
}
}
上述程式碼總結:
- 構建InterceptingCallInvoker物件時,會保留當前攔截器物件和下一個invoker物件,以方便呼叫。
- 當前攔截器物件的在呼叫方法時,第三個引數是委託,而這個委託就是Interceptor對應方法裡面的continuation引數,客戶端通過它來呼叫下一個攔截器。
- 而DefaultInvoker裡面其實是內部呼叫遠端服務,也就是預設實現,而這個是在通過Channel來構造Client的時候構造出來的。
Client總結
- 貫穿上面的程式碼可以看出,不管是呼叫單個新增攔截器,或者鏈式新增單個攔截器,又或者是新增一組攔截器,最終必然返回CallInvoker物件,而CallInvoker物件是在proto生成的程式碼中可以看到,在呼叫對應方法時是由CallInvoker物件呼叫的。
- 關於構建InterceptingCallInvoker ,其實可以和設計模式中的裝飾著模式關聯下,剛開始只構建了預設的DefaultInvoke(這個裡面其實是構建連線,呼叫server端),然後在這基礎上新增其他不同的攔截器功能,返回最終的CallInvoker物件。
- 需要注意的是,當鏈式新增單個攔截器時,比如Intercept(a).Intercept(b).Intercept(c),那麼最終執行的順序是c(continuation前)->b(continuation前)->a->b(continuation後)->c(continuation後)。如果一次新增一組攔截器Intercept(a,b,c),那麼最終執行的順序是:a(continuation前)->b(continuation前)->c->b(continuation後)->a(continuation後)。
Server端
Interceptor抽象類和ServerServiceDefinition類
public abstract class Interceptor
{
//服務端一元呼叫攔截器
public virtual Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
where TRequest : class
where TResponse : class
{
return continuation(request, context);
}
}
public class ServerServiceDefinition
{
//方法列表,也就是服務端寫的那些方法
readonly IReadOnlyList<Action<ServiceBinderBase>> addMethodActions;
internal ServerServiceDefinition(List<Action<ServiceBinderBase>> addMethodActions)
{
this.addMethodActions = addMethodActions.AsReadOnly();
}
//給方法繫結服務,也就是繫結攔截器,一會的原始碼會提到
internal void BindService(ServiceBinderBase serviceBinder)
{
//給每個方法都繫結一下攔截器
foreach (var addMethodAction in addMethodActions)
{
addMethodAction(serviceBinder);
}
}
//建立Builder,可以在proto檔案中生成的程式碼看到,會有呼叫這個方法
public static Builder CreateBuilder()
{
return new Builder();
}
public class Builder
{
//檢測是否有同名方法,這是不被允許的
readonly Dictionary<string, object> duplicateDetector = new Dictionary<string, object>();
//服務端方法集合
readonly List<Action<ServiceBinderBase>> addMethodActions = new List<Action<ServiceBinderBase>>();
public Builder()
{
}
//可以看到在proto生成的程式碼中,有呼叫AddMethod,將方法新增到集合中
public Builder AddMethod<TRequest, TResponse>(
Method<TRequest, TResponse> method,
UnaryServerMethod<TRequest, TResponse> handler)
where TRequest : class
where TResponse : class
{
duplicateDetector.Add(method.FullName, null);
addMethodActions.Add((serviceBinder) => serviceBinder.AddMethod(method, handler));
return this;
}
//這中間省略了除一元呼叫的其他呼叫,有興趣的可以自己翻下原始碼
//初始化build,將上面的方法列表新增到其中
public ServerServiceDefinition Build()
{
return new ServerServiceDefinition(addMethodActions);
}
}
}
上述程式碼總結:
- 對應每個service,都會維護一個方法的集合,然後把使用者定義的方法新增到集合中(在proto生成的程式碼中可以看到)。
- 在給每個方法新增攔截器時(當然目前看不出來,下面會說),會給每個方法都加上,也就是說,它們之間是互不影響的。
新增攔截器
public static class ServerServiceDefinitionExtensions
{
//單個新增攔截器
public static ServerServiceDefinition Intercept(this ServerServiceDefinition serverServiceDefinition, Interceptor interceptor)
{
GrpcPreconditions.CheckNotNull(serverServiceDefinition, nameof(serverServiceDefinition));
GrpcPreconditions.CheckNotNull(interceptor, nameof(interceptor));
//構造新的ServiceBinder
var binder = new InterceptingServiceBinder(interceptor);
//將攔截器繫結到每個方法上
serverServiceDefinition.BindService(binder);
//生成並返回新的service
return binder.GetInterceptedServerServiceDefinition();
}
//新增一組攔截器
public static ServerServiceDefinition Intercept(this ServerServiceDefinition serverServiceDefinition, params Interceptor[] interceptors)
{
GrpcPreconditions.CheckNotNull(serverServiceDefinition, nameof(serverServiceDefinition));
GrpcPreconditions.CheckNotNull(interceptors, nameof(interceptors));
foreach (var interceptor in interceptors.Reverse())
{
serverServiceDefinition = Intercept(serverServiceDefinition, interceptor);
}
return serverServiceDefinition;
}
//只保留了一元呼叫的程式碼
private class InterceptingServiceBinder : ServiceBinderBase
{
//建立一個空的Builder
readonly ServerServiceDefinition.Builder builder = ServerServiceDefinition.CreateBuilder();
//當前攔截器
readonly Interceptor interceptor;
public InterceptingServiceBinder(Interceptor interceptor)
{
this.interceptor = GrpcPreconditions.CheckNotNull(interceptor, nameof(interceptor));
}
//構造新的Builder
internal ServerServiceDefinition GetInterceptedServerServiceDefinition()
{
return builder.Build();
}
//新增一元呼叫的方法,而這個就是你自定義的攔截器
public override void AddMethod<TRequest, TResponse>(
Method<TRequest, TResponse> method,
UnaryServerMethod<TRequest, TResponse> handler)
{
builder.AddMethod(method, (request, context) => interceptor.UnaryServerHandler(request, context, handler));
}
//這裡省略了一部分程式碼。。。
}
}
其實到這裡,我們們再串聯上個小部分的程式碼,應該就能看出一些端倪,上述程式碼總結:
- 這裡鏈式新增或者單次新增一組,它和客戶端攔截器呼叫順序其實是一致的。
- 我們結合目前上面Server端的所有程式碼,可以大概看出,當我們不新增任何攔截器時,ServerServiceDefinition物件裡面的方法集合列表僅僅包含使用者定義的方法委託集合。然而當我們新增攔截器時,它程式碼的執行順序則是,構建InterceptingServiceBinder->呼叫BindService方法,原來的委託集合開始執行,構造新的委託,而呼叫的AddMethod則是InterceptingServiceBinder物件裡面的AddMethod,handler則是我們寫的攔截器裡面的continuation,用於傳遞。
- 最終我們就會得到一個ServerServiceDefinition物件。當然,上述我們只看到了構造物件,而這個物件在哪裡呼叫的呢?我們繼續往下看。
DefaultServiceBinder類
internal static class ServerServiceDefinitionExtensions
{
//在寫服務端的時候,我們需要繫結服務,而在繫結服務的時候需要先呼叫靜態BindService方法(可以在proto生成的程式碼中看到這個方法),然後新增Services時,內部會呼叫GetCallHandlers方法。
internal static ReadOnlyDictionary<string, IServerCallHandler> GetCallHandlers(this ServerServiceDefinition serviceDefinition)
{
//構建預設的ServiceBinder,裡面其實是執行構造的最終handler
var binder = new DefaultServiceBinder();
//呼叫BindService方法,將執行集合委託
serviceDefinition.BindService(binder);
//返回集合列表
return binder.GetCallHandlers();
}
private class DefaultServiceBinder : ServiceBinderBase
{
readonly Dictionary<string, IServerCallHandler> callHandlers = new Dictionary<string, IServerCallHandler>();
internal ReadOnlyDictionary<string, IServerCallHandler> GetCallHandlers()
{
return new ReadOnlyDictionary<string, IServerCallHandler>(this.callHandlers);
}
public override void AddMethod<TRequest, TResponse>(
Method<TRequest, TResponse> method,
UnaryServerMethod<TRequest, TResponse> handler)
{
//每個方法名稱對應的一個handler
callHandlers.Add(method.FullName, ServerCalls.UnaryCall(method, handler));
}
}
}
上述程式碼總結:
- 在構造出ServerServiceDefinition物件時,使用者再將物件繫結到grpc的Servers時,開始執行GetCallHandlers方法,把它又重新構建一遍。
- grpc預設的會構造一個集合,key是方法全名,value則是IServerCallHandler,實際上每次請求進來會檢索方法名,然後執行IServerCallHandler內部的HandleCall方法(這個是在原始碼裡面可以看到?)。
- ServerCalls.UnaryCall想了解的可以看下原始碼,實質上內部就是執行handler,而這個handler就是使用者構建的最終ServerServiceDefinition。
Server總結
- 通過上面我們可以看出,其大致思路可Client端實現很相像,只不過最終返回的是ServerServiceDefinition物件,而這個物件從剛開始預設handler(使用者重寫的Server端方法),到新增攔截器時在上面的封裝,而這個攔截器又通過InterceptingServiceBinder類將其新增進去,它們都繼承了ServiceBinderBase,通過構造最終的Builder物件來返回最終的ServerServiceDefinition。
- 最終的ServerServiceDefinition在我們寫的服務端Demo中可以看到,它被新增到Servers中,而在這時候呼叫GetCallHandlers生成最終的以方法名為key,handler為value的集合。
- 當有請求進來時,我們只需要根據方法名找到對應的handler,然後把引數傳遞進去,再執行handler就可以把攔截器和自己定義的方法全部走一遍,這些有興趣的可以參考下原始碼。
總結
關於Grpc的攔截器,相信你看完之後會有一定的收穫,這裡我再額外說一些其他的關於閱讀Grpc原始碼時的小tips:
- 預設情況下,服務啟動時,只有4個後臺執行緒去消費請求(和計算機的CPU數量有關),但是請求的執行預設是通過新增執行緒池任務來執行的,當然也可以設定不通過執行緒池執行,直接執行時要注意防止阻塞。
- 預設情況下,Grpc支援同一時間同時處理8000個請求(也和計算機的CPU數量有關),如果有更多的請求應該就被阻塞了。這個數量是可以開發人員去調節的。
以上就是筆者對Grpc攔截器的理解,本篇文章也主要是希望給讀者提供原始碼閱讀思路,可能會有偏差,還請評論指正?。