Nguon: Microsoft Learn · .NET 8.0

Gọi dịch vụ gRPC với .NET client

Nguồn: Call gRPC services with the .NET client

Thư viện .NET gRPC client có sẵn trong gói NuGet Grpc.Net.Client. Tài liệu này giải thích cách:

Cấu hình gRPC client

gRPC client là các kiểu client cụ thể được tạo ra từ file .proto. gRPC client cụ thể có các phương thức dịch sang dịch vụ gRPC trong file .proto. Ví dụ, một dịch vụ có tên Greeter tạo ra kiểu GreeterClient với các phương thức để gọi dịch vụ.

gRPC client được tạo từ một channel (kênh). Bắt đầu bằng cách sử dụng GrpcChannel.ForAddress để tạo channel, sau đó sử dụng channel để tạo gRPC client:

csharp
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greet.GreeterClient(channel);

Channel đại diện cho một kết nối dài hạn đến dịch vụ gRPC. Khi channel được tạo, nó được cấu hình với các tùy chọn liên quan đến việc gọi dịch vụ. Ví dụ, HttpClient được sử dụng để thực hiện lời gọi, kích thước message gửi và nhận tối đa và logging có thể được chỉ định trên GrpcChannelOptions và sử dụng với GrpcChannel.ForAddress. Để xem danh sách đầy đủ các tùy chọn, xem client configuration options.

csharp
var channel = GrpcChannel.ForAddress("https://localhost:5001");

var greeterClient = new Greet.GreeterClient(channel);
var counterClient = new Count.CounterClient(channel);

// Use clients to call gRPC services

Cấu hình TLS

gRPC client phải sử dụng cùng bảo mật cấp kết nối như dịch vụ được gọi. gRPC client Transport Layer Security (TLS - bảo mật tầng vận chuyển) được cấu hình khi gRPC channel được tạo. gRPC client gặp lỗi khi gọi dịch vụ và bảo mật cấp kết nối của channel và dịch vụ không khớp.

Để cấu hình gRPC channel sử dụng TLS, hãy đảm bảo địa chỉ server bắt đầu bằng https. Ví dụ, GrpcChannel.ForAddress("https://localhost:5001") sử dụng giao thức HTTPS. gRPC channel tự động thương lượng kết nối được bảo mật bởi TLS và sử dụng kết nối an toàn để thực hiện các lời gọi gRPC.

gRPC hỗ trợ xác thực chứng chỉ client qua TLS. Để biết thông tin về cách cấu hình chứng chỉ client với gRPC channel, xem Authentication and authorization in gRPC for ASP.NET Core.

Để gọi dịch vụ gRPC không bảo mật, hãy đảm bảo địa chỉ server bắt đầu bằng http. Ví dụ, GrpcChannel.ForAddress("http://localhost:5000") sử dụng giao thức HTTP. Trong .NET Core 3.1, cần cấu hình bổ sung để gọi dịch vụ gRPC không bảo mật với .NET client.

Hiệu suất của client

Hiệu suất và cách sử dụng channel và client:

GrpcChannel.ForAddress không phải là tùy chọn duy nhất để tạo gRPC client. Nếu gọi dịch vụ gRPC từ ứng dụng ASP.NET Core, hãy xem xét gRPC client factory integration. gRPC tích hợp với HttpClientFactory cung cấp một giải pháp tập trung để tạo gRPC client.

Khi gọi các phương thức gRPC, ưu tiên sử dụng lập trình bất đồng bộ với async và await. Thực hiện các lời gọi gRPC theo cách blocking, chẳng hạn sử dụng Task.Result hoặc Task.Wait(), ngăn các task khác sử dụng thread. Điều này có thể dẫn đến thread pool starvation (kiệt sức thread pool) và hiệu suất kém. Nó có thể gây ra ứng dụng bị treo với deadlock (bế tắc).

Thực hiện các lời gọi gRPC

gRPC call được khởi tạo bằng cách gọi một phương thức trên client. gRPC client sẽ xử lý việc serialize message (mã hóa hóa message) và địa chỉ của lời gọi gRPC đến đúng dịch vụ.

gRPC có các kiểu phương thức khác nhau. Cách client được sử dụng để thực hiện lời gọi gRPC phụ thuộc vào kiểu phương thức được gọi. Các kiểu phương thức gRPC là:

Lời gọi Unary

Lời gọi unary bắt đầu bằng việc client gửi request message. Response message được trả về khi dịch vụ hoàn thành.

csharp
var client = new Greet.GreeterClient(channel);
var response = await client.SayHelloAsync(new HelloRequest { Name = "World" });

Console.WriteLine("Greeting: " + response.Message);
// Greeting: Hello World

Mỗi phương thức dịch vụ unary trong file .proto sẽ tạo ra hai phương thức .NET trên kiểu gRPC client cụ thể để gọi phương thức: phương thức bất đồng bộ và phương thức blocking. Ví dụ, trên GreeterClient có hai cách gọi SayHello:

Lời gọi Server streaming

Lời gọi server streaming bắt đầu bằng việc client gửi request message. ResponseStream.MoveNext() đọc các message được stream từ dịch vụ. Lời gọi server streaming hoàn thành khi ResponseStream.MoveNext() trả về false.

csharp
var client = new Greet.GreeterClient(channel);
using var call = client.SayHellos(new HelloRequest { Name = "World" });

while (await call.ResponseStream.MoveNext())
{
    Console.WriteLine("Greeting: " + call.ResponseStream.Current.Message);
    // "Greeting: Hello World" is written multiple times
}

Khi sử dụng C# 8 hoặc mới hơn, cú pháp await foreach có thể được dùng để đọc message. Extension method IAsyncStreamReader<T>.ReadAllAsync() đọc tất cả message từ response stream:

csharp
var client = new Greet.GreeterClient(channel);
using var call = client.SayHellos(new HelloRequest { Name = "World" });

await foreach (var response in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine("Greeting: " + response.Message);
    // "Greeting: Hello World" is written multiple times
}

Kiểu được trả về từ việc bắt đầu lời gọi server streaming triển khai IDisposable. Luôn dispose (giải phóng) lời gọi streaming để đảm bảo nó được dừng lại và tất cả tài nguyên được dọn sạch.

Lời gọi Client streaming

Lời gọi client streaming bắt đầu mà không có client gửi message. Client có thể chọn gửi message bằng RequestStream.WriteAsync. Khi client đã gửi xong message, RequestStream.CompleteAsync() nên được gọi để thông báo cho dịch vụ. Lời gọi hoàn thành khi dịch vụ trả về response message.

csharp
var client = new Counter.CounterClient(channel);
using var call = client.AccumulateCount();

for (var i = 0; i < 3; i++)
{
    await call.RequestStream.WriteAsync(new CounterRequest { Count = 1 });
}
await call.RequestStream.CompleteAsync();

var response = await call;
Console.WriteLine($"Count: {response.Count}");
// Count: 3

Kiểu được trả về từ việc bắt đầu lời gọi client streaming triển khai IDisposable. Luôn dispose lời gọi streaming để đảm bảo nó được dừng lại và tất cả tài nguyên được dọn sạch.

Lời gọi Bi-directional streaming

Lời gọi bi-directional streaming bắt đầu mà không có client gửi message. Client có thể chọn gửi message bằng RequestStream.WriteAsync. Các message được stream từ dịch vụ có thể truy cập bằng ResponseStream.MoveNext() hoặc ResponseStream.ReadAllAsync(). Lời gọi bi-directional streaming hoàn thành khi ResponseStream không còn message.

csharp
var client = new Echo.EchoClient(channel);
using var call = client.Echo();

Console.WriteLine("Starting background task to receive messages");
var readTask = Task.Run(async () =>
{
    await foreach (var response in call.ResponseStream.ReadAllAsync())
    {
        Console.WriteLine(response.Message);
        // Echo messages sent to the service
    }
});

Console.WriteLine("Starting to send messages");
Console.WriteLine("Type a message to echo then press enter.");
while (true)
{
    var result = Console.ReadLine();
    if (string.IsNullOrEmpty(result))
    {
        break;
    }

    await call.RequestStream.WriteAsync(new EchoMessage { Message = result });
}

Console.WriteLine("Disconnecting");
await call.RequestStream.CompleteAsync();
await readTask;

Để có hiệu suất tốt nhất và tránh lỗi không cần thiết trong client và dịch vụ, hãy cố gắng hoàn thành các lời gọi bi-directional streaming một cách duyên dáng (gracefully). Lời gọi bi-directional kết thúc gracefully khi server đã đọc xong request stream và client đã đọc xong response stream. Lời gọi mẫu trước là một ví dụ về lời gọi bi-directional kết thúc gracefully. Trong lời gọi, client:

  1. Bắt đầu lời gọi bi-directional streaming mới bằng cách gọi EchoClient.Echo.
  2. Tạo một background task để đọc message từ dịch vụ sử dụng ResponseStream.ReadAllAsync().
  3. Gửi message đến server bằng RequestStream.WriteAsync.
  4. Thông báo cho server rằng đã hoàn thành gửi message bằng RequestStream.CompleteAsync().
  5. Chờ cho đến khi background task đã đọc tất cả message đến.

Trong suốt lời gọi bi-directional streaming, client và dịch vụ có thể gửi message cho nhau bất cứ lúc nào. Logic client tốt nhất để tương tác với lời gọi bi-directional thay đổi tùy thuộc vào logic dịch vụ.

Kiểu được trả về từ việc bắt đầu lời gọi bi-directional streaming triển khai IDisposable. Luôn dispose lời gọi streaming để đảm bảo nó được dừng lại và tất cả tài nguyên được dọn sạch.

Truy cập gRPC headers (tiêu đề)

Lời gọi gRPC trả về response headers. HTTP response headers truyền metadata tên/giá trị về một lời gọi không liên quan đến message được trả về.

Headers có thể truy cập bằng ResponseHeadersAsync, trả về một collection metadata. Headers thường được trả về cùng với response message; do đó, bạn phải await chúng.

csharp
var client = new Greet.GreeterClient(channel);
using var call = client.SayHelloAsync(new HelloRequest { Name = "World" });

var headers = await call.ResponseHeadersAsync;
var myValue = headers.GetValue("my-trailer-name");

var response = await call.ResponseAsync;

Cách sử dụng ResponseHeadersAsync:

Truy cập gRPC trailers

Lời gọi gRPC có thể trả về response trailers. Trailers được dùng để cung cấp metadata tên/giá trị về một lời gọi. Trailers cung cấp chức năng tương tự HTTP headers, nhưng được nhận ở cuối lời gọi.

Trailers có thể truy cập bằng GetTrailers(), trả về một collection metadata. Trailers được trả về sau khi response hoàn thành. Do đó, bạn phải await tất cả response message trước khi truy cập trailers.

Lời gọi unary và client streaming phải await ResponseAsync trước khi gọi GetTrailers():

csharp
var client = new Greet.GreeterClient(channel);
using var call = client.SayHelloAsync(new HelloRequest { Name = "World" });
var response = await call.ResponseAsync;

Console.WriteLine("Greeting: " + response.Message);
// Greeting: Hello World

var trailers = call.GetTrailers();
var myValue = trailers.GetValue("my-trailer-name");

Lời gọi server và bidirectional streaming phải hoàn thành await response stream trước khi gọi GetTrailers():

csharp
var client = new Greet.GreeterClient(channel);
using var call = client.SayHellos(new HelloRequest { Name = "World" });

await foreach (var response in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine("Greeting: " + response.Message);
    // "Greeting: Hello World" is written multiple times
}

var trailers = call.GetTrailers();
var myValue = trailers.GetValue("my-trailer-name");

Trailers cũng có thể truy cập từ RpcException. Dịch vụ có thể trả về trailers cùng với trạng thái gRPC không phải OK. Trong tình huống này, trailers được lấy từ exception được ném bởi gRPC client:

csharp
var client = new Greet.GreeterClient(channel);
string myValue = null;

try
{
    using var call = client.SayHelloAsync(new HelloRequest { Name = "World" });
    var response = await call.ResponseAsync;

    Console.WriteLine("Greeting: " + response.Message);
    // Greeting: Hello World

    var trailers = call.GetTrailers();
    myValue = trailers.GetValue("my-trailer-name");
}
catch (RpcException ex)
{
    var trailers = ex.Trailers;
    myValue = trailers.GetValue("my-trailer-name");
}

Cấu hình deadline (thời hạn)

Cấu hình deadline (thời hạn) cho lời gọi gRPC được khuyến nghị vì nó cung cấp giới hạn trên về thời gian lời gọi có thể chạy. Nó ngăn các dịch vụ hoạt động sai chạy mãi mãi và làm cạn kiệt tài nguyên server. Deadlines là công cụ hữu ích để xây dựng ứng dụng đáng tin cậy.

Cấu hình CallOptions.Deadline để thiết lập deadline cho lời gọi gRPC:

csharp
var client = new Greet.GreeterClient(channel);

try
{
    var response = await client.SayHelloAsync(
        new HelloRequest { Name = "World" },
        deadline: DateTime.UtcNow.AddSeconds(5));
    
    // Greeting: Hello World
    Console.WriteLine("Greeting: " + response.Message);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
    Console.WriteLine("Greeting timeout.");
}

Để biết thêm thông tin, xem Reliable gRPC services with deadlines and cancellation.