ASP.NET Core: Tạo các dịch vụ và phương thức gRPC
Trong bài viết này
- Tạo dịch vụ gRPC mới
- Triển khai các phương thức gRPC
- Truy cập header yêu cầu gRPC
- Đa luồng với các phương thức truyền phát gRPC
- Tài nguyên bổ sung
Tài liệu này giải thích cách tạo các dịch vụ và phương thức gRPC trong C#. Các chủ đề bao gồm:
- Cách định nghĩa các dịch vụ và phương thức trong file
.proto
. - Mã được tạo bằng công cụ gRPC C#.
- Triển khai các dịch vụ và phương pháp gRPC.
Các dịch vụ gRPC với C# đã giới thiệu cách tiếp cận hợp đồng đầu tiên của gRPC để phát triển API. Dịch vụ và tin nhắn được xác định trong file .proto
. Công cụ C# sau đó tạo mã từ các file .proto
. Đối với nội dung phía máy chủ, một loại cơ sở trừu tượng được tạo cho mỗi dịch vụ, cùng với các lớp cho bất kỳ tin nhắn nào.
File .proto
sau sẽ:
- Định nghĩa một dịch vụ
Greeter
. - Dịch vụ
Greeter
xác định một lời gọiSayHello
. SayHello
gửi tin nhắnHelloRequest
và nhận tin nhắnHelloReply
syntax = "proto3"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
Công cụ C# tạo ra kiểu cơ sở GreeterBase
C#:
public abstract partial class GreeterBase { public virtual Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { throw new RpcException(new Status(StatusCode.Unimplemented, "")); } } public class HelloRequest { public string Name { get; set; } } public class HelloReply { public string Message { get; set; } }
Theo mặc định thì GreeterBase
được tạo ra sẽ không làm gì cả. Phương thức ảo SayHello
của nó sẽ trả về lỗi UNIMPLEMENTED
cho bất kỳ máy khách nào gọi nó. Để dịch vụ trở nên hữu ích, ứng dụng phải tạo ra một triển khai cụ thể của GreeterBase
:
public class GreeterService : GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" }); } }
ServerCallContext
cung cấp ngữ cảnh cho lời gọi phía máy chủ.
Việc triển khai dịch vụ được đăng ký với ứng dụng. Nếu dịch vụ được lưu trữ bởi ASP.NET Core gRPC, thì dịch vụ đó sẽ được thêm vào quy trình định tuyến bằng phương thức MapGrpcService
.
app.MapGrpcService<GreeterService>();
Xem các dịch vụ gRPC với ASP.NET Core để biết thêm thông tin.
Một dịch vụ gRPC có thể có nhiều loại phương thức khác nhau. Cách dịch vụ gửi và nhận tin nhắn tùy thuộc vào loại phương thức được định nghĩa. Các loại phương thức gRPC là:
- Đơn nhất (Unary)
- Truyền phát máy chủ
- Truyền phát máy khách
- Truyền phát hai chiều
Lời gọi trực tuyến được chỉ định bằng từ khóa stream
trong file .proto
. stream
có thể được đặt trên tin nhắn yêu cầu, tin nhắn phản hồi hoặc cả hai.
syntax = "proto3"; service ExampleService { // Unary rpc UnaryCall (ExampleRequest) returns (ExampleResponse); // Server streaming rpc StreamingFromServer (ExampleRequest) returns (stream ExampleResponse); // Client streaming rpc StreamingFromClient (stream ExampleRequest) returns (ExampleResponse); // Bi-directional streaming rpc StreamingBothWays (stream ExampleRequest) returns (stream ExampleResponse); }
Mỗi loại lời gọi có một chữ ký phương thức khác nhau. Việc ghi đè các phương thức được tạo từ loại dịch vụ cơ sở trừu tượng trong quá trình triển khai cụ thể sẽ đảm bảo sử dụng các đối số và kiểu trả về chính xác.
Phương thức đơn nhất có thông báo yêu cầu làm tham số và trả về phản hồi. Lời gọi đơn nhất hoàn tất khi phản hồi được trả về.
public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context) { var response = new ExampleResponse(); return Task.FromResult(response); }
Lời gọi đơn nhất giống nhất với các action trên controller API web. Một điểm khác biệt quan trọng mà các phương thức gRPC có so với các action là các phương thức gRPC không thể liên kết các phần của yêu cầu với các đối số phương thức khác nhau. Các phương thức gRPC luôn có một đối số thông báo cho dữ liệu yêu cầu đến. Nhiều giá trị vẫn có thể được gửi đến dịch vụ gRPC bằng cách thêm các trường vào thông báo yêu cầu:
message ExampleRequest {
int32 pageIndex = 1;
int32 pageSize = 2;
bool isDescending = 3;
}
Phương thức phát trực tuyến của máy chủ có thông báo yêu cầu làm tham số. Bởi vì nhiều tin nhắn có thể được truyền lại cho người gọi, nên responseStream.WriteAsync
được sử dụng để gửi tin nhắn phản hồi. Lời gọi phát trực tuyến trên máy chủ hoàn tất khi phương thức trả về.
public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { for (var i = 0; i < 5; i++) { await responseStream.WriteAsync(new ExampleResponse()); await Task.Delay(TimeSpan.FromSeconds(1)); } }
Máy khách không có cách nào để gửi tin nhắn hoặc dữ liệu bổ sung sau khi phương thức phát trực tuyến của máy chủ đã bắt đầu. Một số phương thức phát trực tuyến được thiết kế để chạy vĩnh viễn. Đối với các phương thức truyền phát liên tục, máy khách có thể hủy lời gọi khi không còn cần thiết. Khi việc hủy xảy ra, máy khách sẽ gửi tín hiệu đến máy chủ và ServerCallContext.CancellationToken được kích hoạt. Mã thông báo (token) CancellationToken
phải được sử dụng trên máy chủ với các phương thức không đồng bộ để:
- Mọi công việc không đồng bộ đều bị hủy cùng với lời gọi trực tuyến.
- Phương thức thoát ra nhanh chóng.
public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { while (!context.CancellationToken.IsCancellationRequested) { await responseStream.WriteAsync(new ExampleResponse()); await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken); } }
Phương thức truyền phát ứng dụng máy khách bắt đầu mà không cần phương thức nhận tin nhắn. Tham số requestStream
được sử dụng để đọc tin nhắn từ máy khách. Lời gọi trực tuyến của máy khách hoàn tất khi có thông báo phản hồi được trả về:
public override async Task<ExampleResponse> StreamingFromClient( IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context) { await foreach (var message in requestStream.ReadAllAsync()) { // ... } return new ExampleResponse(); }
Phương thức truyền phát hai chiều bắt đầu mà không cần phương thức nhận tin nhắn. Tham số requestStream
được sử dụng để đọc tin nhắn từ máy khách. Phương thức có thể chọn gửi tin nhắn bằng responseStream.WriteAsync
. Lời gọi truyền phát hai chiều hoàn tất khi phương thức trả về:
public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { await foreach (var message in requestStream.ReadAllAsync()) { await responseStream.WriteAsync(new ExampleResponse()); } }
Đoạn code trên:
- Gửi phản hồi cho mỗi yêu cầu.
- Là cách sử dụng cơ bản của truyền phát hai chiều.
Có thể hỗ trợ các tình huống phức tạp hơn, chẳng hạn như đọc yêu cầu và gửi phản hồi đồng thời:
public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { // Read requests in a background task. var readTask = Task.Run(async () => { await foreach (var message in requestStream.ReadAllAsync()) { // Process request. } }); // Send responses until the client signals that it is complete. while (!readTask.IsCompleted) { await responseStream.WriteAsync(new ExampleResponse()); await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken); } }
Trong phương thức truyền phát hai chiều, máy khách và dịch vụ có thể gửi tin nhắn cho nhau bất kỳ lúc nào. Việc thực hiện tốt nhất phương thức hai chiều khác nhau tùy theo yêu cầu.
Tin nhắn yêu cầu không phải là cách duy nhất để máy khách gửi dữ liệu đến dịch vụ gRPC. Các giá trị tiêu đề có sẵn trong một dịch vụ sử dụng ServerCallContext.RequestHeaders
.
public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context) { var userAgent = context.RequestHeaders.GetValue("user-agent"); // ... return Task.FromResult(new ExampleResponse()); }
Có những cân nhắc quan trọng khi triển khai các phương thức phát trực tuyến gRPC sử dụng nhiều luồng.
An toàn luồng trình đọc và trình ghi
IAsyncStreamReader<TMessage>
và IServerStreamWriter<TMessage>
chỉ có thể được sử dụng bởi một luồng tại một thời điểm. Đối với phương thức gRPC phát trực tuyến, nhiều luồng không thể đọc tin nhắn mới với requestStream.MoveNext()
cùng một lúc. Và nhiều luồng không thể ghi tin nhắn mới với responseStream.WriteAsync(message)
cùng một lúc.
Một cách an toàn để cho phép nhiều luồng tương tác với phương thức gRPC là sử dụng mẫu nhà sản xuất-người tiêu dùng với System.Threading.Channels.
public override async Task DownloadResults(DataRequest request, IServerStreamWriter<DataResult> responseStream, ServerCallContext context) { var channel = Channel.CreateBounded<DataResult>(new BoundedChannelOptions(capacity: 5)); var consumerTask = Task.Run(async () => { // Consume messages from channel and write to response stream. await foreach (var message in channel.Reader.ReadAllAsync()) { await responseStream.WriteAsync(message); } }); var dataChunks = request.Value.Chunk(size: 10); // Write messages to channel from multiple threads. await Task.WhenAll(dataChunks.Select( async c => { var message = new DataResult { BytesProcessed = c.Length }; await channel.Writer.WriteAsync(message); })); // Complete writing and wait for consumer to complete. channel.Writer.Complete(); await consumerTask; }
Phương thức phát trực tuyến máy chủ gRPC trên:
- Tạo một kênh giới hạn để tạo và tiêu thụ tin nhắn
DataResult
. - Bắt đầu tác vụ đọc tin nhắn từ kênh và ghi chúng vào luồng phản hồi.
- Viết tin nhắn vào kênh từ nhiều luồng.
Lưu ý
Các phương thức truyền phát hai chiều lấy
IAsyncStreamReader<TMessage>
vàIServerStreamWriter<TMessage>
làm đối số. Sẽ an toàn khi sử dụng các loại này trên các luồng riêng biệt với nhau.
Tương tác với phương thức gRPC sau khi lời gọi kết thúc
Lời gọi gRPC kết thúc trên máy chủ sau khi phương thức gRPC thoát. Các đối số sau được truyền cho phương thức gRPC không an toàn để sử dụng sau khi lời gọi kết thúc:
ServerCallContext
IAsyncStreamReader<TMessage>
IServerStreamWriter<TMessage>
Nếu phương thức gRPC bắt đầu các tác vụ nền sử dụng các kiểu này thì nó phải hoàn thành các tác vụ trước khi phương thức gRPC thoát. Việc tiếp tục sử dụng ngữ cảnh, trình đọc luồng hoặc trình ghi luồng sau khi phương thức gRPC tồn tại sẽ gây ra lỗi và hành vi không thể đoán trước.
Trong ví dụ sau, phương thức phát trực tuyến của máy chủ có thể ghi vào luồng phản hồi sau khi lời gọi kết thúc:
public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { _ = Task.Run(async () => { for (var i = 0; i < 5; i++) { await responseStream.WriteAsync(new ExampleResponse()); await Task.Delay(TimeSpan.FromSeconds(1)); } }); await PerformLongRunningWorkAsync(); }
Đối với ví dụ trên, giải pháp là chờ tác vụ ghi trước khi thoát khỏi phương thức:
public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { var writeTask = Task.Run(async () => { for (var i = 0; i < 5; i++) { await responseStream.WriteAsync(new ExampleResponse()); await Task.Delay(TimeSpan.FromSeconds(1)); } }); await PerformLongRunningWorkAsync(); await writeTask; }