原始碼解析Grpc攔截器(C#版本)

SnailZz發表於2021-09-17

前言

其實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攔截器的理解,本篇文章也主要是希望給讀者提供原始碼閱讀思路,可能會有偏差,還請評論指正?。

相關文章