gRPC представляет фреймворк, который использует протокол RPC (Remote Procedure Call) для обмена сообщениями между клиентом и сервером. Цель фреймворка состоит в том, чтобы обеспечить высокую производительность в тех условиях, где это особенно критично, например, при интенсивном обмене информацией в режиме реального времени. gRPC не является частью ASP.NET Core или .NET, более того gRPC представляет технологию, которая не привязана к конкретному языку, поэтому данную технологию можно использовать и в рамках приложения на C#.
gRPC имеет следующие преимущества:
Легковесность и высокая производительность
Независимость от конкретного языка программирования
Доступные инструменты для работы со многими распространнеными языками программирования
Поддержка клиентских, серверных и двунаправленных потоковых вызовов
Уменьшение нагрузки сети за счет бинарной сериализации
Если говорить непосредственно о gRPC в .NET, сервисы ASP.NET Core gRPC можно развертывать на всех трех основных операционных системах: Windows, Linux, MacOS. Кроме того, сервисы gRPC поддерживаются всеми стандартными веб-серверами ASP.NET Core: Kestrel, IIS, HTTP.sys.
Для создания проектов на C# для gRPC .NET CLI предоставляет специальный шаблон grpc. Итак, для создания проекта для gRPC сначала определим каталог. Допустим, он будет называться GreeterServiceApp. Перейдем к этой папке в консоли с помощью команды cd и затем введем команду dotnet new grpc:
C:\Users\eugen>cd C:\dotnet\aspnet\GreeterServiceApp C:\dotnet\aspnet\GreeterServiceApp>dotnet new grpc Шаблон "Служба gRPC ASP.NET Core" успешно создан. Идет обработка действий после создания... Восстановление C:\dotnet\aspnet\GreeterServiceApp\GreeterServiceApp.csproj: Определение проектов для восстановления... Восстановлен C:\dotnet\aspnet\GreeterServiceApp\GreeterServiceApp.csproj (за 17,09 sec). Восстановление выполнено. C:\dotnet\aspnet\GreeterServiceApp>
В итоге .NET CLI сгенерирует следующий проект:
Рассмотрим вкратце его структуру:
Папка Properties содержит файл launchSettings.json, который определяет параметры запуска сервиса.
Папка Protos содержит файлы с определением сервисов и сообщений, используемых для взаимодействия по сети
По умолчанию в этой папке определен один файл greet.proto.
Папка Services содержит файлы с реализацией сервисов
По умолчанию в этой папке определен один файл GreeterService.cs.
Файл appsettings.json - стандартный файл конфигурации приложения ASP.NET Core.
Файл appsettings.Development.json - файл конфигурации приложения для стадии разработки.
Файл Program.cs содержит стандартный класс Program, с которого начинается выполнение приложения ASP.NET Core.
Файл GreeterServiceApp.csproj - стандартный файл конфигурации проекта C#.
Если мы откроем файл конфигурации проекта, который в моем случае называется GreeterServiceApp.csproj, то увидим, то это веб-проект:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Server" /> </ItemGroup> <ItemGroup> <PackageReference Include="Grpc.AspNetCore" Version="2.57.0" /> </ItemGroup> </Project>
В частности, проект использует тот же SDK, что и стандартные проекты ASP.NET Core - "Microsoft.NET.Sdk.Web". Но в отличие от других типов проектов в данном случае по умолчанию установлен nuget-пакет Grpc.AspNetCore.
Другой важный момент конфигурации - указание пути к файлу greet.proto
внутри проекта с помощью элемента Protobuf
:
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
Теперь рассмотрим, как собственно определяется сервис.
gRPC использует подход "contract-first", то есть вначале определяется контракт - общее определение сервиса, которое определяет механизм взаимодействия. Так, по умолчанию в папке Protos есть файл greet.proto со следующим кодом:
syntax = "proto3"; option csharp_namespace = "GreeterServiceApp"; package greet; // определение сервиса service Greeter { // отправка сообщения rpc SayHello (HelloRequest) returns (HelloReply); } // сообщение от клиента содержит name message HelloRequest { string name = 1; } // сообщение клиенту содержит message message HelloReply { string message = 1; }
Определение этого файла может напоминать синтаксис C#, но в реальности это синтаксис proto, который используется для описания сервиса gPRC. Хотя в целом это си-подобный синтаксис, поэтому ориентироваться в нем не так сложно.
Самая первая строка определяет тип используемого синтаксиса:
syntax = "proto3";
В данном случаем применяется синтаксис "proto3".
Далее указывается пространство имен, которое будет использоваться с этим сервисом:
option csharp_namespace = "GreeterServiceApp";
По умолчанию это название проекта. И соответственно генерируемые классы будут помещаться в даное пространство имен.
Следующая строка с помощью оператора package определяет название пакета:
package greet;
В данном случае пакет называется "greet". Установка имени пакета позволяет разрешить конфликты имен при наличие сущностей с одинаковыми именами.
Далее собственно определяется сервис:
service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }
Сервис определяется с помощью ключевого слова service, после которого указывается название сервиса. То есть в данном случае сервис называется "Greeter".
С помощью ключевого слова rpc в сервисе определяется метод SayHello
. Данный метод отправляет
сообщение HelloRequest и сообщение HelloReply.
Далее идет определение используемых сообщений:
message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
Сообщение представляет специальную сущность, которая содержит пересылаемые данные. Сообщение определяет набор полей, для которых определен тип. Каждое поле представляет некоторый кусочек информации, посылаемой в сообщении. Так, в обоих сообщениях определены два поля типа string, то есть в каждом сообщении будут отправляться некоторая строка.
Каждому полю в сообщении присваивается уникальное число, например, в примере выше полям обоих сообщений присваивается единица
(в string name = 1
или в string message = 1
). Эти значения позволяют идентифицировать непосредственные значения полей в бинарном формате при кодировании и получении
сообщений.
В папке Services по умолчанию в файле GreeterService.cs определен класс GreeterService, который представляет реализацию сервиса gPRC на языке C#:
using Grpc.Core; using GreeterServiceApp; namespace GreeterServiceApp.Services; public class GreeterService : Greeter.GreeterBase { private readonly ILogger<GreeterService> _logger; public GreeterService(ILogger<GreeterService> logger) { _logger = logger; } public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } }
Класс сервиса (в данном случае GreeterService) наследуется от класса Greeter.GreeterBase. Greeter.GreeterBase - абстрактный класс, который автоматически генерируется по определению сервиса greeter в файле greeter.proto
.
Класс GreeterService по сути представляет обычный класс C#. Так, по умолчанию он имеет конструктор, который посредством dependency injection получает объект логгера и может его использовать для логирования.
Но основной момент класса сервиса - это реализация метода SayHello, который по сути отвечает за обмен сообщениями. Его первый параметр представляет класс HelloRequest, который соответствует определению входящего сообщения в файле proto. То есть это те данные, которые мы получаем от клиента:
message HelloRequest { string name = 1; }
Поскольку сообщение содержит одно поле name, которое представляет строку, то в коде C# мы можем получить эти данные через свойство Name.
Второй параметр метода - объект ServerCallContext, хранит информацию, связанную с контекстом, в котором работает сервер.
Возвращаемое значение метода - объект класса HelloReply, который представляет ответное сообщение:
message HelloReply { string message = 1; }
Это те данные, которые мы посылаем клиенту в ответ. Поскольку сообщение содержит строковое поле message, то в соответствующем классе C# мы можем обращаться к нему через свойство Message:
new HelloReply { Message = "Hello " + request.Name }
Класс HelloReply, как и все классы, которые представляют сообщения или сервисы из файла proto, также генерируется автоматически.
Точкой входа в программу по умолчанию является класс Program, который определен неявно в файле Program.cs. И имеено здесь и происходит подключение всей инфраструктуры gRPC:
using GreeterServiceApp.Services; var builder = WebApplication.CreateBuilder(args); // добавляем сервисы для работы с gRPC builder.Services.AddGrpc(); var app = builder.Build(); // настраиваем обработку HTTP-запросов app.MapGrpcService<GreeterService>(); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client..."); app.Run();
Для того, чтобы задействовать сервис gRPC, во-первых, добавляются необходимые сервисы:
builder.Services.AddGrpc();
Во-вторых, далее сервис gRPC встраивается в систему маршрутизации для обработки запроса:
app.MapGrpcService<GreeterService>();
Для встраивания сервиса применяется метод MapGrpcService(), который типизируется типом встраиваемого сервиса.
Это был примитивная стандартная инфрастуктура сервиса gRPC. Теперь рассмотрим, как создать для него клиентское приложение и банально отправить сервису сообщение и получить от него ответ.
Создадим клиентское приложение для вышеопределенного сервиса gRPC. Для этого создадим новый проект консольного приложения с помощью .NET CLI. Определим для него папку с именем GreeterClientApp. Перейдем к этой папке в консоли с помощью команды cd и для создания проекта выполним команду dotnet new console:
C:\Users\eugen>cd C:\dotnet\aspnet\GreeterClientApp C:\dotnet\aspnet\GreeterClientApp>dotnet new console Шаблон "Консольное приложение" успешно создан. Идет обработка действий после создания... Восстановление C:\dotnet\aspnet\GreeterClientApp\GreeterClientApp.csproj: Определение проектов для восстановления... Восстановлен C:\dotnet\aspnet\GreeterClientApp\GreeterClientApp.csproj (за 2,79 sec). Восстановление выполнено. C:\dotnet\aspnet\GreeterClientApp>
Для создания клиентского приложения для gRPC необходимо через Nuget установить следующие пакеты:
Grpc.Net.Client: содержит функционал клиента .NET
Google.Protobuf: содержит API для сообщений protobuf для языка C#.
Grpc.Tools: содержит инструменты для поддержки protobuf-файлов в C#
Поэтому добавим в проект консольного клиента эти пакета, последовательно выполнив следующие команды:
dotnet add package Grpc.Net.Client dotnet add package Google.Protobuf dotnet add package Grpc.Tools
Далее создадим в проекте консольного приложения новую папку Protos, а в нее скопируем из проекта сервиса файл greet.proto, который содержит определение используемого сервиса. Но после копирования изменим в этом файле строку
option csharp_namespace = "GreeterServiceApp";
на строку
option csharp_namespace = "GreeterClientApp";
То есть мы поменяли пространство имен с "GreeterServiceApp" (имени проекта сервиса) на "GreeterClientApp" (имя проекта клиента).
Далее нам надо отредактировать файл проекта с расширением csproj, который называется по имени проекта. После добавления пакетов он выглядит примерно следующим образом:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Google.Protobuf" Version="3.25.1" /> <PackageReference Include="Grpc.Net.Client" Version="2.59.0" /> <PackageReference Include="Grpc.Tools" Version="2.59.0"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> </Project>
В узел <Project>
добавим следующий элемент:
<ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Client" /> </ItemGroup>
То есть после изменения файл проекта будет выглядеть следующим образом:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Client" /> </ItemGroup> <ItemGroup> <PackageReference Include="Google.Protobuf" Version="3.25.1" /> <PackageReference Include="Grpc.Net.Client" Version="2.59.0" /> <PackageReference Include="Grpc.Tools" Version="2.59.0"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> </Project>
Далее в проекте консольного клиента изменим код файла Program.cs следующим образом:
using Grpc.Net.Client; using GreeterClientApp; // создаем канал для обмена сообщениями с сервером // параметр - адрес сервера gRPC using var channel = GrpcChannel.ForAddress("http://localhost:5134"); // создаем клиент var client = new Greeter.GreeterClient(channel); Console.Write("Введите имя: "); string? name = Console.ReadLine(); // обмениваемся сообщениями с сервером var reply = await client.SayHelloAsync(new HelloRequest { Name = name }); Console.WriteLine($"Ответ сервера: {reply.Message}"); Console.ReadKey();
Допустим, программа будет спрашивать у пользователя имя, отправлять его сервису gRPC и отображать ответ сервиса.
В файле greet.proto определено, что генерируемые классы будут помещаться в пространство имен GreeterClientApp:
option csharp_namespace = "GreeterClientApp";
Поэтому в коде клиентского приложения для подключения всех необходимых классов, связанных с сервисом grpc и автосгенерированных при построении проекта, применяется соответствующая директива using:
using GreeterClientApp;
В каждом конкретном случае пространство имен может отличаться (по умолчанию оно равно названию проекта сервиса).
Для обмена сообщениями с сервером с помощью метода GrpcChannel.ForAddress()
создается канал - объект GrpcChannel:
using var channel = GrpcChannel.ForAddress("http://localhost:5134");
В метод передается адрес сервиса. В данном случае поскольку сервис будет запускаться по адресу "http://localhost:5134", то
соответственно передается данный адрес. Адрес сервиса можно посмотреть в проекте сервиса в файле Properties/launchSettings.json
Для обращения к серверу необходимо создать объект клиента:
var client = new Greeter.GreeterClient(channel);
В конструктор клиента передается объект GrpcChannel.
Название конкретного класса клиента зависит от определения сервиса и устанавливается по шаблону [имя_сервиса].[имя_сервиса]Client
.
То есть в данном случае сервис (согласно определению в файле proto) называется Greeter, поэтому класс клиента для этого сервиса называется
Greeter.GreeterClient.
Далее собственно выполняется взаимодействие с сервисом. Для этого вызывается метод client.SayHelloAsync()
, определение которого в целом
совпадает с определением метода SayHello в сервисе Greeter за тем исключением, что метод клиента асинхронный:
var reply = await client.SayHelloAsync(new HelloRequest { Name = name });
В метод передается объект HelloRequest, который представляет отправляемую сервису информацию. Определение класса HelloRequest совпадает с определением сообщения HelloRequest из файла proto.
Возвращает метод ответ сервера в виде объекта HelloReply, обернутого в объект Grpc.Core.AsyncUnaryCall. Класс HelloReply совпадает с определением сообщения HelloReply из файла proto, поэтому через свойство Message мы можем собственно получить ответ от сервера.
Console.WriteLine($"Ответ сервера: {reply.Message}");
Протестируем приложения. Сначала запустим приложение сервиса gRPC командой:
dotnet run
Затем запустим приложение консольного клиента той же командой:
dotnet run
и в консоли введем некоторое строковое значение: