Пишем, конфигурируем и доставляем сервисы gRPC¶
Созданные в прошлом разделе заглушки gRPC представляют собой абстрактные классы. Более того, если посмотреть в содержимое файла CountryGrpc.cs, можно увидеть, что методы, реализующие функции сервиса, возвращают gRPC-статус Unimplemented. Чтобы всё заработало, нужно написать код, переопределяющий выполнение этих методов.
Создадим класс под названием CountryGrpcService в папке Services. Сменим объявление пространства имён на file-scoped и добавим статический юзинг using static CountryService.Web.gRPC.CountryService; для удобства. Унаследуем наш класс от CountryServiceBase. Должно получиться примерно следующее:
using static CountryService.Web.gRPC.CountryService;
namespace CountryService.Web.Services;
public class CountryGrpcService : CountryServiceBase
{
}
Теперь переопределим методы нашего сервиса. Уберём указания на пространства имён типа
global::%что-то%, при необходимости добавляя их в юзинги в шапке файла.В итоге мы должны получить такой файл:
using CountryService.Web.gRPC;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using static CountryService.Web.gRPC.CountryService;
namespace CountryService.Web.Services;
public class CountryGrpcService : CountryServiceBase
{
public override Task GetAll(Empty request, IServerStreamWriter<CountryReply> responseStream, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, ""));
}
public override Task<CountryReply> Get(CountryIdRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, ""));
}
public override Task<Empty> Delete(IAsyncStreamReader<CountryIdRequest> requestStream, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, ""));
}
public override Task<Empty> Update(CountryUpdateRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, ""));
}
public override Task Create(IAsyncStreamReader<CountryCreationRequest> requestStream, IServerStreamWriter<CountryCreationReply> responseStream, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, ""));
}
}
Обратим внимание, что в случае, когда функция gRPC получает или возвращает сообщение не через поток - метод получает объект нужного типа (или
Empty) в качестве параметра, а возвращает в качестве Task<T>, как и полагается для асинхронных методов. В случае же с потоками, клиентский поток передаётся в виде IAsyncStreamReader<T>, а серверный в виде IServerStreamWriter<T>. Параметр ServerCallContext — контекст запроса, аналог HttpContext. %% добавить ссылку на главу 14 %%. Чтобы наш сервис был доступен извне, нужно сделать кое-то ещё. Добавим конечную точку в файле Program.cs
using CountryService.Web.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc(); //Добавляем сервисы в контейнер зависимостей
var app = builder.Build();
app.MapGrpcService<CountryGrpcService>(); //Добавляем наш сервис как конечную точку.
app.MapGet("/", () => "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");
app.Run();
Теперь нам надо реализовать собственно CRUD операции. Для этой главы реализуем простейший сервис, хранящий данные в памяти. %% Добавить ссылку на главу 9, в которой показано как сделать приложение с доступом к настоящей БД %%
Назовём его
CountryManagementService.using CountryService.Web.gRPC;
using Google.Protobuf.WellKnownTypes;
namespace CountryService.Web
{
public class CountryManagementService
{
private readonly List<CountryReply> _countries = new();
public CountryManagementService()
{
_countries.Add(new CountryReply
{
Id = 1,
Name = "Canada",
Description = "Canada has at least 32 000 lakes",
CreateDate = Timestamp.FromDateTime(DateTime.SpecifyKind(new DateTime(2021, 1, 2), DateTimeKind.Utc))
});
_countries.Add(new CountryReply
{
Id = 2,
Name = "USA",
Description = "Yellowstone has 300 to 500 geysers",
CreateDate = Timestamp.FromDateTime(DateTime.SpecifyKind(new DateTime(2021, 1, 2), DateTimeKind.Utc))
});
_countries.Add(new CountryReply
{
Id = 3,
Name = "Mexico",
Description = "Mexico is crossed by Sierra Madre Oriental and Sierra Madre Occidental mountains",
CreateDate = Timestamp.FromDateTime(DateTime.SpecifyKind(new DateTime(2021, 1, 2), DateTimeKind.Utc))
});
}
public Task<IEnumerable<CountryReply>> GetAllAsync()
{
return Task.FromResult(_countries.AsEnumerable());
}
public Task<CountryReply?> GetAsync(CountryIdRequest idRequest)
{
return Task.FromResult(_countries.FirstOrDefault(c => c.Id == idRequest.Id));
}
public Task DeleteAsync(IEnumerable<CountryIdRequest> idRequests)
{
var ids = idRequests.Select(r => r.Id).ToHashSet();
_countries.RemoveAll(c => ids.Contains(c.Id));
return Task.CompletedTask;
}
public Task UpdateAsync(CountryUpdateRequest updateRequest)
{
var countryToUpdate = _countries.FirstOrDefault(c => c.Id == updateRequest.Id);
if (countryToUpdate != null)
{
countryToUpdate.Description = updateRequest.Description;
countryToUpdate.UpdateDate = updateRequest.UpdateDate;
}
return Task.CompletedTask;
}
public Task<IEnumerable<CountryCreationReply>> CreateAsync(IEnumerable<CountryCreationRequest> creationRequests)
{
var countriesCount = _countries.Count;
var newCountries = creationRequests
.Where(cr => _countries.All(c => c.Name != cr.Name))
.Select(cr => new CountryReply
{
Id = ++countriesCount,
Name = cr.Name,
Description = cr.Description,
Flag = cr.Flag,
CreateDate = Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.Today, DateTimeKind.Utc))
}).ToList();
_countries.AddRange(newCountries);
var result = newCountries.Select(nc => new CountryCreationReply
{
Id = nc.Id,
Name = nc.Name
});
return Task.FromResult(result);
}
}
}
Зарегистрируем класс в контейнере зависимостей в файле Program.cs:
Теперь можно внедрить
CountryManagementService в конструкторе CountryGrpcService и набросать реализацию CRUD-операцийusing CountryService.Web.gRPC;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using static CountryService.Web.gRPC.CountryService;
namespace CountryService.Web.Services;
public class CountryGrpcService : CountryServiceBase
{
private readonly CountryManagementService _countryManagementService;
public CountryGrpcService(CountryManagementService countryManagementService)
{
_countryManagementService = countryManagementService;
}
public override async Task GetAll(Empty request, IServerStreamWriter<CountryReply> responseStream, ServerCallContext context)
{
//Стримим все найденные страны клиенту
var replies = await _countryManagementService.GetAllAsync();
foreach (var countryReply in replies)
{
await responseStream.WriteAsync(countryReply);
}
}
public override async Task<CountryReply> Get(CountryIdRequest request, ServerCallContext context)
{
//Нам может вернуться null, если передан несуществующий Id, вернем NotFound в этом случае
var result = await _countryManagementService.GetAsync(request);
return result ?? throw new RpcException(new Status(StatusCode.NotFound, $"No country with id {request.Id}"));
}
public override async Task<Empty> Delete(IAsyncStreamReader<CountryIdRequest> requestStream, ServerCallContext context)
{
//Сперва загрузим все запросы на удаление
var requestsList = new List<CountryIdRequest>();
await foreach (var idRequest in requestStream.ReadAllAsync())
{
requestsList.Add(idRequest);
}
//Теперь удалим всё разом
await _countryManagementService.DeleteAsync(requestsList);
return new Empty();
}
public override async Task<Empty> Update(CountryUpdateRequest request, ServerCallContext context)
{
await _countryManagementService.UpdateAsync(request);
return new Empty();
}
public override async Task Create(IAsyncStreamReader<CountryCreationRequest> requestStream, IServerStreamWriter<CountryCreationReply> responseStream, ServerCallContext context)
{
//Сперва загрузим все запросы на создание
var requestsList = new List<CountryCreationRequest>();
await foreach (var createRequest in requestStream.ReadAllAsync())
{
requestsList.Add(createRequest);
}
//сохраним всё сразу
var createdCountries = await _countryManagementService.CreateAsync(requestsList);
foreach (var createdCountry in createdCountries)
{
await responseStream.WriteAsync(createdCountry);
}
}
}
Помимо этого можно добавить несколько опций и тем самым изменить или улучшить поведение сервиса gRPC. В главе 3 было показано, как добавить опции к каналу, используемому в клиенте. Рассмотрим, как сделать то же самое на стороне сервера. Вот какие настройки можно изменить глобально или для каждого сервиса отдельно.
MaxSendMessageSize— максимальный размер сообщения, отправляемого с сервера. Если значение не задано, лимит отсутствует. Если значение задано, и размер сообщения превысит его — будет выброшено исключениеRpcException;MaxReceivedMessageSize— максимальный размер сообщения, отправляемого на сервер. По умолчанию равно 4 Мб. Чтобы снять лимит, нужно присвоитьnull. При превышении лимита выбрасывается исключениеRpcException;CompressionProviders— коллекция поставщиков (провайдеров) сжатия. Если ничего не было задано, провайдером по умолчанию будет Gzip. Можно настроить сжатие Gzip и/или добавить свой провайдер;ResponseCompressionAlgorithm— строковое обозначение алгоритма, использованного при сжатии. Если не задано, будет использован первый подходящий под заголовокgrpc-accept-encodingиз текущей коллекции. Алгоритмом по умолчанию является Gzip. Его не нужно добавлять вComperssionProviders.ResponseCompressionLevel— уровень сжатия, передаваемый провайдеру сжатия. Провайдер использует уровень сжатия по умолчанию в случае, если уровень не задан этим свойством.
Добавим провайдер сжатия¶
Совет
Несмотря на то, что MaxReceiveMessageSize и MaxSendMessageSize необязательны, настоятельно рекомендуется использовать их для ограничения потребляемых ресурсов.
Кроме этого, есть ещё настройки EnableDetailedErrors, Interceptors и IgnoreUnknownServices, но их мы разберем позже.
Дата создания : 5 апреля 2023 г.