ASP.NET Core gRPC Source Code Analysis

2024年4月3日 80点热度 2人点赞 0条评论
内容目录

Overall Logic

Using ASP.NET Core gRPC is very straightforward; service registration and middleware require only two lines of code.

        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddGrpc();

        var app = builder.Build();

        // Configure the HTTP request pipeline.
        app.MapGrpcService<GreeterService>();

Registering services:

    public static IGrpcServerBuilder AddGrpc(this IServiceCollection services)
    {
        ArgumentNullThrowHelper.ThrowIfNull(services);

#if NET8_0_OR_GREATER
        // Prefer AddRoutingCore when available.
        // AddRoutingCore doesn't register a regex constraint and produces smaller result from trimming.
        services.AddRoutingCore();
        services.Configure<RouteOptions>(ConfigureRouting);
#else
        services.AddRouting(ConfigureRouting);
#endif
        services.AddOptions();
        services.TryAddSingleton<GrpcMarkerService>();
        services.TryAddSingleton(typeof(ServerCallHandlerFactory<>));
        services.TryAddSingleton(typeof(IGrpcServiceActivator<>), typeof(DefaultGrpcServiceActivator<>));
        services.TryAddSingleton(typeof(IGrpcInterceptorActivator<>), typeof(DefaultGrpcInterceptorActivator<>));
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<GrpcServiceOptions>, GrpcServiceOptionsSetup>());

        // Model
        services.TryAddSingleton<ServiceMethodsRegistry>();
        services.TryAddSingleton(typeof(ServiceRouteBuilder<>));
        services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IServiceMethodProvider<>), typeof(BinderServiceMethodProvider<>)));

        return new GrpcServerBuilder(services);
    }

GrpcMarkerService is an empty service meant to determine whether developers have injected gRPC services via the container. If services are registered using .AddGrpc(), GrpcMarkerService can be accessed; otherwise, during middleware initialization, a direct check can be performed to throw an exception.

Each proto file generates a type, such as GreeterBase, through the sg technology, with each generated type having two BindService methods and proto custom methods.

    [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
    public static grpc::ServerServiceDefinition BindService(GreeterBase serviceImpl)
    {
      return grpc::ServerServiceDefinition.CreateBuilder()
          .AddMethod(__Method_SayHello, serviceImpl.SayHello).Build();
    }

    [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
    public static void BindService(grpc::ServiceBinderBase serviceBinder, GreeterBase serviceImpl)
    {
      serviceBinder.AddMethod(__Method_SayHello, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::GrpcService1.HelloRequest, global::GrpcService1.HelloReply>(serviceImpl.SayHello));
    }

These two methods are dynamically generated and will create .AddMethod() code for the method names generated in the proto file. This way, when the middleware registers gRPC services, it does not need to reflectively scan service classes; instead, it directly calls the BindService method to configure each route with its corresponding method.

Below are the details for each method:

internal class MethodModel
{
    public MethodModel(IMethod method, RoutePattern pattern, IList<object> metadata, RequestDelegate requestDelegate)
    {
        Method = method;
        Pattern = pattern;
        Metadata = metadata;
        RequestDelegate = requestDelegate;
    }

    public IMethod Method { get; }
    public RoutePattern Pattern { get; }
    public IList<object> Metadata { get; }
    public RequestDelegate RequestDelegate { get; }
}

The following shows the configuration process for registering a gRPC service within the middleware:

file
file

Next, for each method of each service, routes and request delegates are bound.

file

When a request matches the route and method, it directs to the corresponding RequestDelegate.

file

The generation of methods is first handled in BindService.

    [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
    public static void BindService(grpc::ServiceBinderBase serviceBinder, GreeterBase serviceImpl)
    {
      serviceBinder.AddMethod(__Method_SayHello, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::GrpcService1.HelloRequest, global::GrpcService1.HelloReply>(serviceImpl.SayHello));
    }
// ProviderServiceBinder.cs 
    public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, UnaryServerMethod<TRequest, TResponse>? handler)
    {
        var (invoker, metadata) = CreateModelCore<UnaryServerMethod<TService, TRequest, TResponse>>(
            method.Name,
            new[] { typeof(TRequest), typeof(ServerCallContext) });

        _context.AddUnaryMethod<TRequest, TResponse>(method, metadata, invoker);
    }
    public void AddUnaryMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, IList<object> metadata, UnaryServerMethod<TService, TRequest, TResponse> invoker)
        where TRequest : class
        where TResponse : class
    {
	// ServerCallHandlerFactory.cs
        var callHandler = _serverCallHandlerFactory.CreateUnary<TRequest, TResponse>(method, invoker);
        AddMethod(method, RoutePatternFactory.Parse(method.FullName), metadata, callHandler.HandleCallAsync);
    }

As seen, after several layers of encapsulation, the specified gRPC service methods are packaged into a RequestDelegate.

    public delegate Task RequestDelegate(HttpContext context);

Once the RequestDelegate is built, the logic for handling requests can be found in ServerCallHandlerBase.cs. The HandleCallAsync method in ServerCallHandlerBase aligns with the definition of RequestDelegate, allowing direct utilization by the middleware.

file

It is important to note that the ServerCallHandlerBase instance is created at the point of scanning the gRPC service and binding methods, meaning that for each request hitting the same route, it corresponds to the same ServerCallHandlerBase instance, with only the HandleCallAsync method receiving a new HttpContext on each call.

痴者工良

高级程序员劝退师

文章评论