[Translation] two-way streaming and ASP.NET Core 3.0 gRPC introduction

Original related

 原文作者:Eduard Los
 原文地址:https://medium.com/@eddyf1xxxer/bi-directional-streaming-and-introduction-to-grpc-on-asp-net-core-3-0-part-2-d9127a58dcdb
 Demo地址:https://github.com/f1xxxer/CustomerGrpc

Now, let's take a look at the code. You can easily create a gRPC service project using the Visual Studio UI or using command line commands: dotnet new grpc -n YourGrpcService

In my solution, both the gRPC server and client code are in C #. The gRPC server is managing client connections and processing messages, and broadcasting messages to all connected clients. The client receives the input from the client, sends it to the server, and also accepts messages sent by other clients from the server.

We first look at the server code in the CustomerGrpc project, but before that, I want to point out something in the standard Startup.cs and Program.cs files.

public class Startup {
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices (IServiceCollection services) {
        services.AddGrpc (opts => {
            opts.EnableDetailedErrors = true;
            opts.MaxReceiveMessageSize = 4096;
            opts.MaxSendMessageSize = 4096;
        });
        services.AddTransient<IChatRoomService, ChatRoomService> ();
        services.AddSingleton<IChatRoomProvider, ChatRoomProvider> ();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure (IApplicationBuilder app, IWebHostEnvironment env) {
        if (env.IsDevelopment ()) {
            app.UseDeveloperExceptionPage ();
        }

        app.UseRouting ();

        app.UseEndpoints (endpoints => {
            endpoints.MapGrpcService<Services.CustomerService> ();

            endpoints.MapGet ("/",
                async context => {
                    await context.Response.WriteAsync (
                        "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
                });
        });
    }
}

The services.AdddGrpc () ; method enables gRPC. This method adds different services and middleware that are used to build pipes for handling gRPC calls. The Undercover method uses the GrpcMarkerService class to ensure that all necessary gRPC services are added, and some middleware that operates on basic HTTP / 2 messages is also added. You can also provide service configuration through the GrpcServiceOptions type, which will be used later in the pipeline. For example, the maximum incoming message size can be configured like this:


services.AddGrpc(opts =>
{
    opts.EnableDetailedErrors = true;
    opts.MaxReceiveMessageSize = 4096;
    opts.MaxSendMessageSize = 4096;
});

For us, another interesting method is in endpoint routing: endpoints.MapGrpcService <Services.CustomerService> (); This method adds the gRPC service to the routing pipeline. In ASP.NET Core, the routing pipeline is shared between middleware and functions, which means we can have other request handlers in it, such as MVC controllers. Other request handlers work in parallel with the configured gRPC service. as follows:


app.UseEndpoints(endpoints =>
{
	endpoints.MapGrpcService<Services.CustomerService>();

	endpoints.MapGet("/",	async context =>
		{
			await context.Response.WriteAsync(
				"Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: 						https://go.microsoft.com/fwlink/?linkid=2086909");
		});
});

Now we need to configure the Kestrel server.

Kestrel gRPC endpoint:

  • HTTP / 2 is required.
  • HTTPS should be used for protection.

HTTP / 2
gRPC requires HTTP / 2, and ASP.NET Core's gRPC verifies that HttpRequest.Protocol . Is HTTP / 2.

Kestrel supports Http / 2 on most modern operating systems . By default, Kestrel endpoints are configured to support HTTP / 1.1 and HTTP / 2 connections.

HTTPS
Kestrel endpoints for gRPC should be protected using HTTPS. In development, https: // localhost: 5001 will automatically create an HTTPS endpoint when there is an ASP.NET Core development certificate. No configuration is required.
In production, HTTPS must be explicitly configured. In our case, we have the following:


public class Program
{
	public static void Main(string[] args)
	{
		CreateHostBuilder(args).Build().Run();
	}

	// Additional configuration is required to successfully run gRPC on macOS.
	// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
	public static IHostBuilder CreateHostBuilder(string[] args) =>
		Host.CreateDefaultBuilder(args)
			.ConfigureWebHostDefaults(webBuilder =>
			{
				webBuilder.ConfigureKestrel(options =>
				{
					// Setup a HTTP/2 endpoint without TLS for OSX.
					options.ListenLocalhost(5001, o => o.Protocols = HttpProtocols.Http2);
				});
				webBuilder.UseStartup<Startup>();
			});
}

Please note that there is a known issue that macOS does not support ASP.NET Core gRPC with Transport Layer Security ( Http / 2. ). To successfully run the gRPC service on macOS, additional configuration is required. This is exactly our situation, which is why we have to disable TLS using HTTP / 2 lines without TLS in options.ListenLocalhost (5001, o => o.Protocols = HttpProtocols.Http2) Used during application development. Production applications should always use transmission security. For more information, see Security Considerations in ASP.NET Core's gRPC .

Now, let's take a look at our gRPC customer service. At first we see is our CustomerService class from CustomerGrpc.CustomerService.CustomerServiceBase class inheritance.

This class is generated based on our .proto file. If you press the F12 key on the file, you will see a bunch of automatically generated codes that describe our services, messages, and communications.
In our class, we override two custom methods defined in the parent class: JoinCustomerChat and SendMessageToChatRoom This is basically all of our code. JoinCustomerChat demonstrates a relatively simple request / response model, in which we send JoinCustomerRequest customer information and receive it in the RoomId of the chat room where the customer joins JoinCustomerReply .


public override async Task<JoinCustomerReply> JoinCustomerChat (JoinCustomerRequest request, ServerCallContext context) 
{
    return new JoinCustomerReply { RoomId = await _chatRoomService.AddCustomerToChatRoomAsync (request.Customer) };
}

Then, we have a more interesting SendMessageToChatRoom method, which is a demonstration of bidirectional streaming.


public override async Task SendMessageToChatRoom (IAsyncStreamReader<ChatMessage> requestStream, IServerStreamWriter<ChatMessage> responseStream, ServerCallContext context)
 {
    var httpContext = context.GetHttpContext ();
    _logger.LogInformation ($"Connection id: {httpContext.Connection.Id}");

    if (!await requestStream.MoveNext ()) {
        return;
    }

    _chatRoomService.ConnectCustomerToChatRoom (requestStream.Current.RoomId, Guid.Parse (requestStream.Current.CustomerId), responseStream);
    var user = requestStream.Current.CustomerName;
    _logger.LogInformation ($"{user} connected");

    try {
        while (await requestStream.MoveNext ())
        {
            if (!string.IsNullOrEmpty (requestStream.Current.Message)) 
            {
                if (string.Equals (requestStream.Current.Message, "qw!", StringComparison.OrdinalIgnoreCase)) {
                    break;
                }
                await _chatRoomService.BroadcastMessageAsync (requestStream.Current);
            }
        }
    } 
    catch (IOException)
    {
        _chatRoomService.DisconnectCustomer (requestStream.Current.RoomId, Guid.Parse (requestStream.Current.CustomerId));
        _logger.LogInformation ($"Connection for {user} was aborted.");
    }
}

Let's take a closer look at the parameters of the method. IAsyncStreamReader requestStream Represents the message flow from the client, we can read and iterate requestStream.MoveNext () . If there are messages available, the method returns true; if there are no other messages (that is, the stream is closed), the method returns false.

IServerStreamWriter responseStream It is also a message flow, this time it is writable and used to send the message back to the client.

The ServerCallContext context provides some convenient attributes, such as the HttpContext and HostName of the call.
The logic in this method is not complicated. When a call is received on the server, first we store IServerStreamWriter responseStream Used to broadcast messages to clients,


_chatRoomService.ConnectCustomerToChatRoom(requestStream.Current.RoomId, Guid.Parse(requestStream.Current.CustomerId), responseStream);

Then in a while (await requestStream.MoveNext ()) loop, we browse the messages received from the client and broadcast them to another client that is currently connected.


await _chatRoomService.BroadcastMessageAsync(requestStream.Current);

The code in the BroadcastMessageAsync () method will traverse the response stream of all connected clients and write messages to the stream.


foreach (var customer in chatRoom.CustomersInRoom)
{
   await customer.Stream.WriteAsync(message);
   Console.WriteLine($"Sent message from {message.CustomerName} to   {customer.Name}");
}

We also have a try / catch block to handle the connection failure and delete the corresponding stream from the chat room.


catch (IOException)
{
   _chatRoomService.DisconnectCustomer(requestStream.Current.RoomId,     Guid.Parse(requestStream.Current.CustomerId));
   _logger.LogInformation($"Connection for {user} was aborted.");
}

This is basically all server logic directly related to gRPC. I have created some additional features in services and wrappers, which you can find here.

Now, we can briefly view the client code. First, we create a GrpcChannel and provide it to the gRPC client.


var channel = GrpcChannel.ForAddress("http://localhost:5001", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });
var client = new CustomerService.CustomerServiceClient(channel);

Then, the client returns JoinCustomerChatAsync to the RoomId of the chat room. After that, we open or two-way flow by executing SendMessageToChatRoom call and starting message exchange.


using (var streaming = client.SendMessageToChatRoom (new Metadata { new Metadata.Entry ("CustomerName", customer.Name) })) 
{
    var response = Task.Run (async () => 
    {
        while (await streaming.ResponseStream.MoveNext ()) 
        {
            Console.ForegroundColor = Enum.Parse<ConsoleColor> (streaming.ResponseStream.Current.Color);
            Console.WriteLine ($"{streaming.ResponseStream.Current.CustomerName}: {streaming.ResponseStream.Current.Message}");
            Console.ForegroundColor = Enum.Parse<ConsoleColor> (customer.ColorInConsole);
        }
    });

    ...
    
    while (!string.Equals (line, "qw!", StringComparison.OrdinalIgnoreCase)) {
        await streaming.RequestStream.WriteAsync (new ChatMessage {
            Color = customer.ColorInConsole,
                CustomerId = customer.Id,
                CustomerName = customer.Name,
                Message = line,
                RoomId = joinCustomerReply.RoomId
        });
        line = Console.ReadLine ();
        DeletePrevConsoleLine ();
    }
    await streaming.RequestStream.CompleteAsync ();
}

After opening the connection, we start a loop in the background task, which reads the message from the response stream and then displays it in the console. The second loop simply takes input from the keyboard and sends the input to the server.
Overall, the code is not that complicated, but it indicates where you need to use or design choices to use .net core and c # to implement your own gRPC service.

Thank you very much to the author Eduard Los . Share it with us.
If you like it, you might as well like it.

Guess you like

Origin www.cnblogs.com/ancold/p/12693924.html