.NET 6 创建 gRPC 服务(简单实现)

.NET 6 创建 gRPC 服务

gRPC(https://grpc.io)是一个由Google开发的高性能、开源、跨多种编程语言和通用的远程过程调用协议(RPC)框架,用于客户端和服务端之间的通信,使用HTTP/2协议并将ProtoBuf(https://developers.google.com/protocol-buffers)作为序列工具.

参考资料:

https://www.cnblogs.com/dennisdong/p/17120990.html#5149719

https://learn.microsoft.com/zh-cn/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-7.0&tabs=visual-studio

.NET 上的 gRPC 概述 | Microsoft Learn

项目源码地址:https://github.com/DarkRogerZz/simplegRPC

一、新建服务端

新建项目 -- 新建 ASP.NET Core gRPC 服务

默认文件结构

  • Protos/greet.proto:定义 Greeter gRPC,并用于生成 gRPC 服务器资产。

  • Services 文件夹:包含 Greeter 服务的实现。

  • appSettings.json:包含配置数据,如 Kestrel 使用的协议。

  • Program.cs,其中包含:

    • gRPC 服务的入口点。

    • 配置应用行为的代码。

右键项目生成,项目obj/Debug/net6.0/Protos 就会生成对应proto文件的代码

新建proto文件

下载google protobuf

protobuf是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据

比如以下代码块

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;
}

在gRPC通信过程中,会将以下信息自动序列成json对象

引入google protobuf

在项目Protos文件夹下创建Google文件夹,将下载的google protobuf文件中\include\google\protobuf全部放入Google文件夹

创建proto

新建完成 查看下proto文件属性,如果生成操作是无,将 生成操作改为Protobuf complier

proto具体写法可以参考

https://protobuf.dev/programming-guides/proto3/

syntax = "proto3";

//命名空间
option csharp_namespace = "GrpcService.Protos";

//包名称
package ticket;

//引入google struct
import "Protos/Google/struct.proto";

//定义服务
service TicketServer{
	//简单模式(Simple RPC)
	rpc SayHello(HelloRequest) returns (HelloResponse);
	//服务端数据流模式(Server-side streaming RPC)
	rpc QueryTicket(TicketRequest) returns(stream TicketResponse);
	//客户端数据流模式(Client-side streaming RPC)
	rpc BuyTicket(stream TicketRequest) returns(TicketResponse);
	//双向数据流模式 (Bidirectional streaming RPC)
	rpc StramingBothWay(stream TicketRequest) returns (stream TicketResponse);
}

//定义请求和接收内容
message HelloRequest{
	string name = 1;
}

message HelloResponse{
	string message = 1;
}


//google.protobuf.Struct 作用就是不确定字段,但是可以将一个json发送过去,只需要服务端和客户端保持字段一致即可
message TicketRequest{
	google.protobuf.Struct ticketInfo = 1;
}


message TicketResponse{
	string result = 1;
}
新建服务
using Grpc.Core;
using GrpcService.Protos;
using Newtonsoft.Json;

namespace GrpcService.Services
{
    public class TicketService : TicketServer.TicketServerBase
    {
        private readonly ILogger<TicketService> _logger;

        public TicketService(ILogger<TicketService> logger)
        {
            _logger = logger;
        }

        //简单说个hello
        public override Task<HelloResponse> SayHello(HelloRequest request, ServerCallContext context)
        {
            _logger.LogInformation($"Saying hello to {request.Name}");
            return Task.FromResult(new HelloResponse
            {
                Message = $"Hello{request.Name}"
            });
        }

        //查询票价
        public override async Task QueryTicket(TicketRequest request, IServerStreamWriter<TicketResponse> responseStream, ServerCallContext context)
        {
            int price = 10;
            while (!context.CancellationToken.IsCancellationRequested)
            {
                price += 10;
                await responseStream.WriteAsync(new TicketResponse
                {
                    Result = $"票价为{price}"
                });

                await Task.Delay(TimeSpan.FromSeconds(3), context.CancellationToken);
            }
        }

        //买票
        public override async Task<TicketResponse> BuyTicket(IAsyncStreamReader<TicketRequest> requestStream, ServerCallContext context)
        {
            int num = 100000;
            string name = "";
            //处理请求
            await foreach (var req in requestStream.ReadAllAsync())
            {
                Console.WriteLine($"剩余{num}");
                num--;
                var user = JsonConvert.DeserializeObject<TicketInfo>(req.TicketInfo.ToString());
                name = user.Name;
                Console.WriteLine(name);

            }

            return new TicketResponse { Result = $"成功,最后用户:{name},剩余:{num}" };
        }

        public override async Task StramingBothWay(IAsyncStreamReader<TicketRequest> requestStream, IServerStreamWriter<TicketResponse> responseStream, ServerCallContext context)
        {
            // 服务器响应客户端一次
            // 处理请求
            //await foreach (var req in requestStream.ReadAllAsync())
            //{
            //    Console.WriteLine(req);
            //}

            // 请求处理完成之后只响应一次
            //await responseStream.WriteAsync(new TicketResponse
            //{
            //    Code = 200,
            //    Result = true,
            //    Message = $"StramingBothWay 单次响应: {Guid.NewGuid()}"
            //});
            //await Task.Delay(TimeSpan.FromSeconds(3), context.CancellationToken);

            // 服务器响应客户端多次
            // 处理请求
            var readTask = Task.Run(async () =>
            {
                await foreach (var req in requestStream.ReadAllAsync())
                {
                    Console.WriteLine(req);
                }
            });

            // 请求未处理完之前一直响应
            while (!readTask.IsCompleted)
            {
                await responseStream.WriteAsync(new TicketResponse
                {
                   
                    Result = $"StramingBothWay 请求处理完之前的响应: {Guid.NewGuid()}"
                });
                await Task.Delay(TimeSpan.FromSeconds(3), context.CancellationToken);
            }

            // 也可以无限响应客户端
            //while (!context.CancellationToken.IsCancellationRequested)
            //{
            //    await responseStream.WriteAsync(new TicketResponse
            //    {
            //        Code = 200,
            //        Result = true,
            //        Message = $"StreamingFromServer 无限响应: {Guid.NewGuid()}"
            //    });
            //    await Task.Delay(TimeSpan.FromSeconds(3), context.CancellationToken);
            //}
        }
    }

    public class TicketInfo 
    {
        public string Name { get; set; }

        public bool Sex { get; set; }
    }
}

添加证书(可选)
builder.WebHost.ConfigureKestrel(opt =>
{
    var httpPort = builder.Configuration.GetValue<int>("port:http");

    var httpsPort = builder.Configuration.GetValue<int>("port:https");

    opt.Listen(IPAddress.Any, httpPort, listenOpt => listenOpt.UseConnectionLogging());
    opt.Listen(IPAddress.Any, httpsPort, listenOpt =>
    {
        var enableSsl = builder.Configuration.GetValue<bool>("enableSsl");
        if (enableSsl)
        {
            listenOpt.UseHttps("Certs\\cert.pfx","1234.com");
        }
        else
        {
            listenOpt.UseHttps();
        }
        listenOpt.UseConnectionLogging();
    });
});

二、新建客户端

1、新建一个控制台程序,添加所需依赖

2、新建Protos文件夹

3、放入服务端配置好的proto文件

4、添加项目文件引用

编辑项目文件,添加以下内容

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

5、生成项目,proto会自动生成对应类

建立连接

如果启用证书,需要将服务端证书拷贝到客户端/bin/debug/net6.0下

// 建立连接
    private static TicketServer.TicketServerClient CreateClient(bool enableSsl = false)
    {
        GrpcChannel channel;
        if (enableSsl)
        {
            string url = "https://localhost:7000";

            var handle = new HttpClientHandler();
            // 添加证书
            handle.ClientCertificates.Add(new X509Certificate2("Certs\\cert.pfx", "1234.com"));

            // 忽略证书
            handle.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
            channel = GrpcChannel.ForAddress(url, new GrpcChannelOptions
            {
                HttpClient = new HttpClient(handle)
            });
        }
        else
        {
            string url = "http://localhost:5000";
            channel = GrpcChannel.ForAddress(url);
        }

        return new TicketServer.TicketServerClient(channel);
    }

客户端代码

// See https://aka.ms/new-console-template for more information
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using GrpcService.Protos;
using System;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;



//GrpcTest.SayHello();
GrpcTest.QueryTicket();
//GrpcTest.BuyTicket();
//GrpcTest.StreamingBothWay();

Console.ReadLine();

public static class GrpcTest
{


    // 建立连接
    private static TicketServer.TicketServerClient CreateClient(bool enableSsl = false)
    {
        GrpcChannel channel;
        if (enableSsl)
        {
            string url = "https://localhost:7000";

            var handle = new HttpClientHandler();
            // 添加证书
            handle.ClientCertificates.Add(new X509Certificate2("Certs\\cert.pfx", "1234.com"));

            // 忽略证书
            handle.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
            channel = GrpcChannel.ForAddress(url, new GrpcChannelOptions
            {
                HttpClient = new HttpClient(handle)
            });
        }
        else
        {
            string url = "http://localhost:5000";
            channel = GrpcChannel.ForAddress(url);
        }

        return new TicketServer.TicketServerClient(channel);
    }

    //调用就说hello
    public static async void SayHello()
    {

        Console.WriteLine("单次调用模式,参数:Dennis");
        var client = CreateClient(true);

        var result = await client.SayHelloAsync(new HelloRequest { Name = "Dennis" });

        Console.WriteLine($"服务器响应Message={result.Message}");
    }

    //服务端流模式
    public static async void QueryTicket()
    {
        Console.WriteLine("服务端流模式");
        var client = CreateClient(true);
        var result = client.QueryTicket(new TicketRequest
        {
            TicketInfo = new Struct
            {
                Fields =
                {
                    ["Name"] = Value.ForString("Den"),
                    ["Sex"] = Value.ForBool(true)
                }
            }
        }); ;

        await foreach (var resp in result.ResponseStream.ReadAllAsync())
        {

            Console.WriteLine($"服务器响应结果:{resp.Result}");
        }

    }

    //客户端流模式
    public static async void BuyTicket()
    {
        Console.WriteLine("客户端流模式");
        var client = CreateClient(true);
        var result = client.BuyTicket();

        for (int i = 0; i < 10; i++)
        {
            await result.RequestStream.WriteAsync(new TicketRequest
            {
                TicketInfo = new Struct
                {
                    Fields =
                {
                    ["Name"] = Value.ForString("Den"+i),
                    ["Sex"] = Value.ForBool(true)
                }
                }
            });
            await Task.Delay(TimeSpan.FromSeconds(2));
        }

        await result.RequestStream.CompleteAsync();

        var resp = result.ResponseAsync.Result;
        Console.WriteLine($"服务器响应结果:{resp.Result}");
    }

    //双向流模式
    public static async void StreamingBothWay()
    {
        Console.WriteLine("双向流模式");
        var client = CreateClient();
        var result = client.StramingBothWay();

        // 发送请求
        for (var i = 0; i < 5; i++)
        {
            await result.RequestStream.WriteAsync(new TicketRequest
            {
                TicketInfo = new Struct
                {
                    Fields =
                {
                    ["Name"] = Value.ForString("Den"+i),
                    ["Sex"] = Value.ForBool(true)
                }
                }
            });
            await Task.Delay(TimeSpan.FromSeconds(1));
        }

        // 处理响应
        var respTask = Task.Run(async () =>
        {
            await foreach (var resp in result.ResponseStream.ReadAllAsync())
            {
                Console.WriteLine($"Result={resp.Result}");
            }
        });

        // 等待请求发送完毕
        await result.RequestStream.CompleteAsync();

        // 等待响应处理
        await respTask;
    }
}

三、效果

单次响应:

服务端流模式:

客户端流模式:

双向流模式:

posted @ 2023-02-20 15:04  Dark华  阅读(156)  评论(0编辑  收藏  举报