5

ASP.Net Core 3.1 使用gRPC入门指南

 3 years ago
source link: http://www.cnblogs.com/hudean/p/14028957.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

主要参考文章微软官方文档: https://docs.microsoft.com/zh-cn/aspnet/core/grpc/client?view=aspnetcore-3.1

此外还参考了文章 https://www.cnblogs.com/stulzq/p/11581967.html并写了一个demo: https://files.cnblogs.com/files/hudean/GrpcDemo.zip

一、简介

gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架。

gRPC 的主要优点是:

  • 现代高性能轻量级 RPC 框架。
  • 协定优先 API 开发,默认使用协议缓冲区,允许与语言无关的实现。
  • 可用于多种语言的工具,以生成强类型服务器和客户端。
  • 支持客户端、服务器和双向流式处理调用。
  • 使用 Protobuf 二进制序列化减少对网络的使用。

这些优点使 gRPC 适用于:

  • 效率至关重要的轻量级微服务。
  • 需要多种语言用于开发的 Polyglot 系统。
  • 需要处理流式处理请求或响应的点对点实时服务。

二、创建 gRPC 服务

  • 启动 Visual Studio 并选择“创建新项目”。  或者,从 Visual Studio“文件”菜单中选择“新建” > “项目” 。

  • 在“创建新项目”对话框中,选择“gRPC 服务”,然后选择“下一步” :

    QnUFRz.png!mobile

  • 将项目命名为 GrpcGreeter。  将项目命名为“GrpcGreeter”非常重要,这样在复制和粘贴代码时命名空间就会匹配。

  • 选择“创建”。

  • 在“创建新 gRPC 服务”对话框中:

    • 选择“gRPC 服务”模板。
    • 选择“创建”。

运行服务

  • 按 Ctrl+F5 以在不使用调试程序的情况下运行。

    Visual Studio 会显示以下对话框:

    JnqAnyn.png!mobile

    如果信任 IIS Express SSL 证书,请选择“是” 。

    将显示以下对话框:

    2YvQBb6.png!mobile

    如果你同意信任开发证书,请选择“是”。

日志显示该服务正在侦听  https://localhost:5001

控制台显示如下:

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development

备注

gRPC 模板配置为使用 传输层安全性 (TLS) 。  gRPC 客户端需要使用 HTTPS 调用服务器。

macOS 不支持 ASP.NET Core gRPC 及 TLS。  在 macOS 上成功运行 gRPC 服务需要其他配置。 

检查项目文件

GrpcGreeter 项目文件:

  • greet.proto  :  Protos/greet.proto  文件定义  Greeter  gRPC,且用于生成 gRPC 服务器资产。 
  • Services 文件夹:包含  Greeter  服务的实现。
  • appSettings.json  :包含配置数据,例如 Kestrel 使用的协议。 
  • Program.cs:包含 gRPC 服务的入口点。 

Startup.cs :包含配置应用行为的代码。
上述准备工作完成,开始写gRPC服务端代码!


uUBjMba.png!mobile

example.proto文件内容如下


syntax = "proto3";

option csharp_namespace = "GrpcGreeter";

package example;

service exampler {
  // Unarys
  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);
}
message ExampleRequest {
    int32 id = 1;
    string name = 2;
}

message ExampleResponse {
    string msg = 1;
}

example.proto

其中:

syntax = "proto3";是使用 proto3 语法,protocol buffer 编译器默认使用的是 proto2 。 这必须是文件的非空、非注释的第一行。

对于 C#语言,编译器会为每一个.proto 文件创建一个.cs 文件,为每一个消息类型都创建一个类来操作。

option csharp_namespace = "GrpcGreeter";是c#代码的命名空间

package example;包的命名空间

service exampler 是服务的名字

rpc UnaryCall (ExampleRequest) returns (ExampleResponse); 意思是rpc调用方法 UnaryCall 方法参数是ExampleRequest类型 返回值是ExampleResponse 类型

message ExampleRequest {
    int32 id = 1;
    string name = 2;
}

指定字段类型

在上面的例子中,所有字段都是标量类型:一个整型(id),一个string类型(name)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。
分配标识号

我们可以看到在上面定义的消息中,给每个字段都定义了唯一的数字值。这些数字是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 
[1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。类似地,你不能使用之前保留的任何标识符。

指定字段规则

消息的字段可以是一下情况之一:

singular(默认):一个格式良好的消息可以包含该段可以出现 0 或 1 次(不能大于 1 次)。
repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。
默认情况下,标量数值类型的repeated字段使用packed的编码方式。

e2IfqiB.png!mobile

在GrpcGreeter.csproj文件添加:

<ItemGroup>

<Protobuf Include="Protos\example.proto" GrpcServices="Server" />

</ItemGroup>

点击保存

在Services文件夹下添加ExampleService类,代码如下:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Core;

namespace GrpcGreeter
{
    public class ExampleService :exampler.examplerBase
    {
        /// <summary>
        /// 一元方法以参数的形式获取请求消息,并返回响应。 返回响应时,一元调用完成。
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context)
        {
            // return base.UnaryCall(request, context);
            return Task.FromResult(new ExampleResponse
            {
                Msg = "id :" + request.Id + "name : " + request.Name + " hello"

            }) ;
        }

        /// <summary>
        /// 服务器流式处理方法
        /// 服务器流式处理方法以参数的形式获取请求消息。 由于可以将多个消息流式传输回调用方,因此可使用 responseStream.WriteAsync 发送响应
        /// 消息。 当方法返回时,服务器流式处理调用完成。
        /// </summary>
        /// <param name="request"></param>
        /// <param name="responseStream"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
        {

            /**
             * 服务器流式处理方法启动后,客户端无法发送其他消息或数据。 某些流式处理方法设计为永久运行。 对于连续流式处理方法,客户端可以在*再需要调用时将其取消。 当发生取消时,客户端会将信号发送到服务器,并引发 ServerCallContext.CancellationToken。 应在服务器上通过异步方法使用 CancellationToken 标记,以实现以下目的:
               所有异步工作都与流式处理调用一起取消。
               该方法快速退出。
             **/
            //return base.StreamingFromServer(request, responseStream, context);

            //for (int i = 0; i < 5; i++)
            //{
            //    await responseStream.WriteAsync(new ExampleResponse { Msg = "我是服务端流for:" + i });
            //    await Task.Delay(TimeSpan.FromSeconds(1));
            //}
            int index = 0;
            while (!context.CancellationToken.IsCancellationRequested)
            {
                index++;
                await responseStream.WriteAsync(new ExampleResponse { Msg = "我是服务端流while" + index+" "+request.Id+" "+request.Name });
                await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken);

            }

        }


        /// <summary>
        /// 客户端流式处理方法
        /// 客户端流式处理方法在该方法没有接收消息的情况下启动。 requestStream 参数用于从客户端读取消息。 返回响应消息时,客户端流式处理调用
        /// 完成:
        /// </summary>
        /// <param name="requestStream"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override async Task<ExampleResponse> StreamingFromClient(IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context)
        {
            // return base.StreamingFromClient(requestStream, context);
            List<string> list = new List<string>();
            while (await requestStream.MoveNext())
            {
                //var message = requestStream.Current;
                var id = requestStream.Current.Id;
                var name = requestStream.Current.Name;

                list.Add($"{id}-{name}");
                // ...
            }
            return new ExampleResponse() { Msg = "我是客户端流while"+string.Join(',',list) };


            //await foreach (var message in requestStream.ReadAllAsync())
            //{
            //    // ...
            //}
            // return new ExampleResponse() { Msg= "我是客户端流foreach" };
        }

        /// <summary>
        /// 双向流式处理方法
        /// 双向流式处理方法在该方法没有接收到消息的情况下启动。 requestStream 参数用于从客户端读取消息。 
        /// 该方法可选择使用 responseStream.WriteAsync 发送消息。 当方法返回时,双向流式处理调用完成:
        /// </summary>
        /// <param name="requestStream"></param>
        /// <param name="responseStream"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
        {
            //return base.StreamingBothWays(requestStream, responseStream, context);
            await foreach (var message in requestStream.ReadAllAsync())
            {
               string str= message.Id + " " + message.Name;
                await responseStream.WriteAsync(new ExampleResponse() { Msg="我是双向流:"+ str });
                
            }


            //// Read requests in a background task.
            //var readTask = Task.Run(async () =>
            //{
            //    await foreach (var message in requestStream.ReadAllAsync())
            //    {
            //        // Process request.
            //        string str = message.Id + " " + message.Name;
            //    }
            //});

            //// 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);
            //}
        }

    }
}

ExampleService

在Startup类里Configure中加入一个这个 endpoints.MapGrpcService<ExampleService>();


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace GrpcGreeter
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>();
                endpoints.MapGrpcService<ExampleService>();

                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("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");
                });
            });
        }
    }
}

Startup

就此 gRPC服务端代码完成了

在 .NET 控制台应用中创建 gRPC 客户端

  • 打开 Visual Studio 的第二个实例并选择“创建新项目”。
  • 在“创建新项目”对话框中,选择“控制台应用(.NET Core)”,然后选择“下一步” 。
  • 在“项目名称”文本框中,输入“GrpcGreeterClient”,然后选择“创建” 。

添加所需的包

gRPC 客户端项目需要以下包:

  • Grpc.Net.Client ,其中包含 .NET Core 客户端。
  • Google.Protobuf  包含适用于 C# 的 Protobuf 消息。
  • Grpc.Tools  包含适用于 Protobuf 文件的 C# 工具支持。  运行时不需要工具包,因此依赖项标记为  PrivateAssets="All"
  •  

通过包管理器控制台 (PMC) 或管理 NuGet 包来安装包。

用于安装包的 PMC 选项

  • 从 Visual Studio 中,依次选择“工具” > “NuGet 包管理器” > “包管理器控制台”

  • 从“包管理器控制台”窗口中,运行  cd GrpcGreeterClient 以将目录更改为包含 GrpcGreeterClient.csproj 文件的文件夹。

运行以下命令:
PowerShell

Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools

管理 NuGet 包选项以安装包

  • 右键单击“解决方案资源管理器” > “管理 NuGet 包”中的项目 。
  • 选择“浏览”选项卡。
  • 在搜索框中输入 Grpc.Net.Client。
  • 从“浏览”选项卡中选择“Grpc.Net.Client”包,然后选择“安装” 。
  • 为  Google.Protobuf  和  Grpc.Tools  重复这些步骤。

添加 greet.proto

  • 在 gRPC 客户端项目中创建 Protos 文件夹。

  • 从 gRPC Greeter 服务将 Protos\greet.proto 文件复制到 gRPC 客户端项目。

  • 将  greet.proto 文件中的命名空间更新为项目的命名空间:

    option csharp_namespace = "GrpcGreeterClient";
  • 编辑 GrpcGreeterClient.csproj 项目文件:

    右键单击项目,并选择“编辑项目文件”。

添加具有引用 greet.proto 文件的  <Protobuf> 元素的项组:

XML

<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

创建 Greeter 客户端

构建客户端项目,以在  GrpcGreeter 命名空间中创建类型。  GrpcGreeter  类型是由生成进程自动生成的。

使用以下代码更新 gRPC 客户端的 Program.cs 文件:

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Grpc.Net.Client;

namespace GrpcGreeterClient
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // The port number(5001) must match the port of the gRPC server.
            using var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client =  new Greeter.GreeterClient(channel);
            var reply = await client.SayHelloAsync(
                              new HelloRequest { Name = "GreeterClient" });
            Console.WriteLine("Greeting: " + reply.Message);
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }
}

UrqIZbb.png!mobile

添加内容如下:

<ItemGroup>
    <Protobuf Include="Protos\example.proto" GrpcServices="Server" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="Protos\example.proto" GrpcServices="Client" />
  </ItemGroup>

在gRPC客户端写调用服务端代码,代码如下:


using Grpc.Core;
using Grpc.Net.Client;
using GrpcGreeter;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace GrpcGreeterClient
{
    class Program
    {
        //static void Main(string[] args)
        //{
        //    Console.WriteLine("Hello World!");
        //}
        //static async Task Main(string[] args)
        //{
        //    // The port number(5001) must match the port of the gRPC server.
        //    using var channel = GrpcChannel.ForAddress("https://localhost:5001");
        //    var client = new Greeter.GreeterClient(channel);
        //    var reply = await client.SayHelloAsync(
        //                      new HelloRequest { Name = "GreeterClient" });
        //    Console.WriteLine("Greeting: " + reply.Message);
        //    Console.WriteLine("Press any key to exit...");
        //    Console.ReadKey();
        //}


        static async Task Main(string[] args)
        {
            // The port number(5001) must match the port of the gRPC server.
            using var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = new exampler.examplerClient(channel);

            #region 一元调用

            //var reply = await client.UnaryCallAsync(new ExampleRequest { Id = 1, Name = "hda" });
            //Console.WriteLine("Greeting: " + reply.Msg);

            #endregion 一元调用

            #region  服务器流式处理调用

            //using var call = client.StreamingFromServer(new ExampleRequest { Id = 1, Name = "hda" });

            //while (await call.ResponseStream.MoveNext(CancellationToken.None))
            //{
            //    Console.WriteLine("Greeting: " + call.ResponseStream.Current.Msg);

            //}
            //如果使用 C# 8 或更高版本,则可使用 await foreach 语法来读取消息。 IAsyncStreamReader<T>.ReadAllAsync() 扩展方法读取响应数据流中的所有消息:
            //await foreach (var response in call.ResponseStream.ReadAllAsync())
            //{
            //    Console.WriteLine("Greeting: " + response.Msg);
            //    // "Greeting: Hello World" is written multiple times
            //}

            #endregion  服务器流式处理调用

            #region  客户端流式处理调用
            //using var call = client.StreamingFromClient();
            //for (int i = 0; i < 5; i++)
            //{
            //    await call.RequestStream.WriteAsync(new ExampleRequest { Id = i, Name = "hda" + i });
            //}
            //await call.RequestStream.CompleteAsync();
            //var response = await call;
            //Console.WriteLine($"Count: {response.Msg}");
            #endregion 客户端流式处理调用

            #region  双向流式处理调用

            //通过调用 EchoClient.Echo 启动新的双向流式调用。
            //使用 ResponseStream.ReadAllAsync() 创建用于从服务中读取消息的后台任务。
            //使用 RequestStream.WriteAsync 将消息发送到服务器。
            //使用 RequestStream.CompleteAsync() 通知服务器它已发送消息。
            //等待直到后台任务已读取所有传入消息。
            //双向流式处理调用期间,客户端和服务可在任何时间互相发送消息。 与双向调用交互的最佳客户端逻辑因服务逻辑而异。
            using var call = client.StreamingBothWays();
            Console.WriteLine("Starting background task to receive messages");
            var readTask = Task.Run(async () =>
            {
                await foreach (var response in call.ResponseStream.ReadAllAsync())
                {
                    Console.WriteLine(response.Msg);
                    // 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 ExampleRequest { Id=1,Name= result });
            }

            Console.WriteLine("Disconnecting");
            await call.RequestStream.CompleteAsync();
            await readTask;
            #endregion 双向流式处理调用



            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }
}

View Code

代码链接地址: https://files.cnblogs.com/files/hudean/GrpcGreeter.zip


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK