Введение в gRPC

Первый проект с .NET CLI

Последнее обновление: 26.11.2023

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 сгенерирует следующий проект:

Первый проект сервиса gRPC на языке C# с помощью .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, также генерируется автоматически.

Подключение gRPC

Точкой входа в программу по умолчанию является класс 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-сервиса

Создадим клиентское приложение для вышеопределенного сервиса 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>

Добавление Nuget-пакетов

Для создания клиентского приложения для 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

Далее создадим в проекте консольного приложения новую папку 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

Протестируем приложения. Сначала запустим приложение сервиса gRPC командой:

dotnet run
Запуск сервиса gRPC на C#

Затем запустим приложение консольного клиента той же командой:

dotnet run

и в консоли введем некоторое строковое значение:

Взаимодействие клиента и сервера gRPC в приложении на C# и .NET
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850