.net8 使用grpc

服务端项目
创建Grpc服务端项目

注意:项目名称这里最好不要用 -,不符合规范,若使用会导致后续生成的类文件命名空间会出问题报错,必须手动修改带有 - 的命名空间,这里由于已经创建了就不手动更改了。

可以看到项目中默认包含两个文件夹Protos、Services。

Protos文件夹用来存放 .proto格式的文件,这些文件用来定义grpc的服务和消息结构。项目可以根据该格式文件自动生成对应的类文件。

Services文件夹用来存放实现grpc服务的类,这些类继承自 .proto 文件中定义的服务基类,并实现其中的方法。

  1. Greeter用来定义服务,服务内包含具体的方法。可以看作是一个类。
  2. SayHello是定义服务的方法名,HttpRequest是该服务方法对应的传参对象,用message HelloRequest的方式表明该方法传参对象具体包括哪些参数。同理,HelloReply是该服务方法对应的返回结果对象,用message HelloReply的方式表名该方法返回结果对象具体包括哪些参数。

注意:参数类型必须是protobuf数据类型,另外如果方法不需要传参或没有返回值,切记不能为空,需要定义空类型google.protobuf.Empty

  1. option csharp_namespace后面+命名空间名,package后面+包名,需注意每个proto文件的包名和命名空间不能重复。
  2. option csharp_namespace 对应的值默认是项目名称,注意命名不要使用 -(连字符)拼接,这是因为在 C# 语言里, 并非合法的标识符字符,而命名空间在 C# 中属于标识符,需要遵循 C# 的标识符命名规则。如果使用不合法的标识符字符,例如 连字符 -,后续proto生成的代码文件将会出现红色波浪线提示。

C# 标识符的命名规则如下:

  • 标识符只能由字母、数字和下划线组成。
  • 标识符不能以数字开头。
  • 标识符不能是 C# 的关键字。

GreeterService服务类文件是greet.proto文件中定义的服务方法具体的实现代码。其中Greeter.GreeterBase来自于greet.proto文件自动生成的类,位置在obj\Debug目录下的GreetGrpc.cs文件。

appsettings.json 配置文件中默认包含Kestrel服务器的配置信息,具体如下

  • "Kestrel":这是 Kestrel 服务器的配置根节点。
  • "EndpointDefaults":此为 Kestrel 端点的默认配置。
  • "Protocols": "Http2":这表明 Kestrel 服务器默认使用 HTTP/2 协议。HTTP/2 具备性能提升、二进制分帧、多路复用等优势,适合于 gRPC 这类对性能和实时性要求较高的应用。

启动类文件默认包含以下代码配置信息

// grpc服务注入到DI容器中
builder.Services.AddGrpc();
// 将GreeterService服务注册到管道中
app.MapGrpcService<GreeterService>();
创建并配置自定义proto文件
  1. 鼠标右键Protos文件夹→添加→新建项→协议缓冲区文件,新增role.proto,order.proto两个文件。

  1. 鼠标分别单击role.proto,order.proto两个文件,"生成操作"选项改为选择 Protobuf compiler

  1. 鼠标分别单击role.proto,order.proto两个文件,将"gRPC Stub Classes"选项的值改为Server only(默认是Client and Server)

  1. 编辑role.proto,order.proto两个文件,配置服务的方法及对应的请求参数和返回参数,具体配置内容如下所示:
syntax = "proto3";

option csharp_namespace = "GrpcService_Order_Server";

package order;

// 订单服务定义
service Order {
  // 创建订单
rpc CreateOrder (CreateRequest) returns (CreateResult);
  // 查询订单
rpc QueryOrder (QueryRequest) returns (QueryResult);
}

// 创建订单请求参数
message CreateRequest {
  string orderNo = 1;
  string goodName = 2;
  int32 goodCount = 3;
  double goodPrice = 4;
  double goodTotalPrice = 5;
}
// 创建订单返回结果
message CreateResult {
  bool result = 1;
  string message = 2;
  CreateResultData data = 3;
}
//创建订单返回结果数据
message CreateResultData {
	string orderId = 1;
}


// 查询订单请求参数
message QueryRequest{
  string orderId = 1;
}
// 查询订单返回结果
message QueryResult{
  bool result = 1;
  string message = 2;
  QueryResultData data = 3;
}
// 查询订单返回结果数据
message QueryResultData {
  string orderNo = 1;
  string goodName = 2;
  int32 goodCount = 3;
  double goodPrice = 4;
  double goodTotalPrice = 5;
}

syntax = "proto3";

option csharp_namespace = "GrpcService_Role_Server";

import "google/protobuf/empty.proto";

package role;

// 角色服务定义
service RoleManager {
 // 根据ID获取角色
 rpc GetRoleByID (RoleRequest) returns (RoleResult);
 // 获取所有角色,google.protobuf.Empty表示不需要参数
 rpc GetRoleAll (google.protobuf.Empty) returns (RoleListResult);
}

message RoleRequest {
   int32 roleId = 1;
}
message RoleResult {
  bool result = 1;
  string message = 2;
  RoleInfo data = 3;
}
message RoleInfo {
   int32 Id = 1;
   string RoleName = 2;
   string RoleKey = 3;
   string CreateTime = 4;
}

message RoleListResult{
   bool result = 1;
   string message = 2;
   //repeated表示可以存储多个相同类型的值,通常用来表示集合类型 
   repeated RoleInfo data = 3;
}

注意:

  1. 配置文件中的数字并不是代表对应字段的值,而是字段的唯一标识,可以理解成“字段编号”

字段编号的作用

  • 在序列化过程中,字段编号能够协助编码器明确每个字段在字节流中的位置。在反序列化时,解码器凭借这些编号可以快速定位并解析出对应字段的数据。例如,当接收到一个字节流时,解码器看到编号 1 就知道这是 orderNo 字段的数据。
  • 在消息定义发生变更时,字段编号有助于维持兼容性。若后续要对消息结构进行修改,比如添加新字段,只要不改动原有的字段编号,旧版本的客户端和服务器依旧能够正确处理消息。举例来说,若后续在 CreateRequest 里添加新字段 string customerName = 4;,旧版本的代码不会受到影响,因为它们会忽略不认识的字段编号。
  1. google.protobuf.Empty表示不需要参数,可以是请求不需要参数,也可以返回不需要参数
添加服务引用
  1. 项目右键,找到 添加→服务引用,单击。

  1. 选择gRPC,点击“下一步”。

找到对应的proto文件。点击“完成”。

点击“关闭”。

点击“+”图标,把role.proto文件也添加进来。

添加之后如下所示。

点击每个服务引用右边的“...”,选择“查看生成的代码”,便可看见生成的代码信息。

添加数据库依赖包和Redis包

Services文件夹下依次创建 OrderService 和 RoleService 两个类文件。其中 RoleService 将会读取数据库表信息,而OrderService 不读取数据库表信息。

由于RoleService会读取数据库表数据,因此这里采用EFCORE连接SQL SERVER数据库,所以服务端项目添加三个相关包。

引入第三方缓存Redis,因此需要再项目中引入StackExchange.Redis包,这里主要是在OrderService中使用,将用户创建的订单数据缓存起来,然后客户端调用获取订单数据服务的时候再将订单数据返回。

生成数据库表实体类文件
  1. 打开程序包管理器控制台,输入Scaffold-DbContext命令,实现从数据库表生成模型类。(注意:执行命令前需要在项目中新建Models文件夹

格式如下

如果要生成数据库下所有表的类文件。

Scaffold-DbContext '数据库连接字符串' Microsoft.EntityFrameworkCore.SqlServer -OutputDir 输出文件夹名称 -Context 数据上下文 -DataAnnotations

如果要生成数据库下指定表的类文件,则在原命令后面加上-Tables指令,指定对面的表名即可。

Scaffold-DbContext '数据库连接字符串' Microsoft.EntityFrameworkCore.SqlServer -OutputDir 输出文件夹名称 -Context 数据上下文 -DataAnnotations -Tables Table1,Table2

这里只生成角色表。

Scaffold-DbContext 'Server=.;DataBase=ming_test;User ID=sa;Password=123456;Integrated Security=false;Encrypt=False' Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Context GrpcDbContext -DataAnnotations -Tables grpc_role

此时Models文件夹便会生成DbContext文件和指定表对应的实体类文件。

  1. 去掉DbContext类文件中的数据库连接字符串。

添加连接字符串配置

appsettings.json 配置文件添加数据库连接字符串配置和Redis连接字符串配置。

"ConnectionStrings": {
  "GrpcDb": "Server=.;DataBase=ming_test;User ID=sa;Password=123456;Integrated Security=false;Encrypt=False"
},
"Redis": {
  "ConnectionString": "localhost:6379,password=123456"
}
配置数据库服务和Redis服务

打开启动类文件Program.cs,注册数据库服务和Redis服务。

var Configuration = builder.Configuration;

builder.Services.AddDbContext<GrpcDbContext>(a =>
{
a.UseSqlServer(Configuration.GetConnectionString("GrpcDb"));
});

var redisConnectionString = builder.Configuration["Redis:ConnectionString"];
var redis = ConnectionMultiplexer.Connect(redisConnectionString);
builder.Services.AddSingleton<IConnectionMultiplexer>(redis);
注册Service服务

还是打开启动类文件Program.cs,注册OrderService 和 RoleService 服务。

 app.MapGrpcService<OrderService>();
 app.MapGrpcService<RoleService>();
实现Service业务

分别编写代码实现OrderService和RoleService业务。

  • 创建订单场景:接收客户端传过来的订单参数写入到缓存中,并随机生成一个GUID字符串的订单ID返回给客户端。
  • 查询订单场景:接收客户端传过来的订单ID去缓存中查询订单数据。
using Grpc.Core;
using GrpcService_Order_Server;
using StackExchange.Redis;
using System.Text.Json;

namespace GrpcService_Server.Services
{
    /// <summary>
    /// 订单服务
    /// </summary>
    public class OrderService : GrpcService_Order_Server.Order.OrderBase
    {

        private readonly IConnectionMultiplexer _redis;
        public OrderService(IConnectionMultiplexer redis)
        {
            _redis = redis;
        }

        /// <summary>
        /// 创建订单
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task<CreateResult> CreateOrder(CreateRequest request, ServerCallContext context)
        {
            string cacheKey = Guid.NewGuid().ToString("N");
            var db = _redis.GetDatabase();
            var cacheValue = new
            {
                orderNo = request.OrderNo,
                goodName = request.GoodName,
                goodCount = request.GoodCount,
                goodPrice = request.GoodPrice,
                goodTotalPrice = request.GoodTotalPrice
            };
            var serializedValue = JsonSerializer.Serialize(cacheValue);
            db.StringSet(cacheKey, serializedValue, TimeSpan.FromHours(1));

            return Task.FromResult(new CreateResult
            {
                Result = true,
                Message = "订单创建成功",
                Data = new CreateResultData
                {
                    OrderId = cacheKey
                }
            });
        }

        /// <summary>
        /// 查询订单
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task<QueryResult> QueryOrder(QueryRequest request, ServerCallContext context)
        {
            var db = _redis.GetDatabase();
            var cacheKey = request.OrderId;
            var cachedValue = db.StringGet(cacheKey);
            if (!cachedValue.IsNull)
            {
                string valueAsString = cachedValue.ToString();
                var orderInfo = JsonSerializer.Deserialize<JsonElement>(valueAsString);
                return Task.FromResult(new QueryResult
                {
                    Result = true,
                    Message = "查询数据成功",
                    Data = new QueryResultData
                    {
                        OrderNo = orderInfo.GetProperty("orderNo").GetString(),
                        GoodCount = orderInfo.GetProperty("goodCount").GetInt32(),
                        GoodName = orderInfo.GetProperty("goodName").GetString(),
                        GoodPrice = orderInfo.GetProperty("goodPrice").GetDouble(),
                        GoodTotalPrice = orderInfo.GetProperty("goodTotalPrice").GetDouble(),
                    }
                });
            }

            return Task.FromResult(new QueryResult
            {
                Result = false,
                Message = "查询失败,未找到相关订单数据"
            });
        }
    }
}
  • 获取角色列表场景:查询所有角色数据返回给客户端
  • 获取角色信息场景:接收客户端传过来的角色ID获取对应的角色信息返回给客户端。
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using GrpcService_Order_Server;
using GrpcService_Role_Server;
using GrpcService_Server.Models;
using StackExchange.Redis;
using System.Data;

namespace GrpcService_Server.Services
{
    /// <summary>
    /// 角色服务
    /// </summary>
    public class RoleService : RoleManager.RoleManagerBase
    {
        GrpcDbContext _db;

        public RoleService(GrpcDbContext db)
        {
            _db = db;
        }

        /// <summary>
        /// 根据角色ID获取角色信息
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task<RoleResult> GetRoleByID(RoleRequest request, ServerCallContext context)
        {
            int roleId = request.RoleId;
            var roleModel = _db.GrpcRoles.Find(roleId);

            if (roleModel != null)
            {
                RoleInfo role = new RoleInfo();
                role.Id = roleModel.Id;
                role.RoleName = roleModel.RoleName;
                role.RoleKey = roleModel.RoleKey;
                role.CreateTime = roleModel.CreateTime.ToString();

                return Task.FromResult(new RoleResult
                {
                    Result = true,
                    Message = "查询数据成功",
                    Data = role
                });
            }

            return Task.FromResult(new RoleResult
            {
                Result = false,
                Message = "查询失败,未找到相关角色数据"
            });
        }

        /// <summary>
        /// 获取所有角色信息
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task<RoleListResult> GetRoleAll(Empty request, ServerCallContext context)
        {
            var roleList = _db.GrpcRoles.ToList();

            var result = new RoleListResult
            {
                Result = true,
                Message = "查询数据成功"
            };

            List<RoleInfo> list = new List<RoleInfo>();
            if (roleList != null && roleList.Count > 0)
            {
                foreach (var item in roleList)
                {
                    RoleInfo role = new RoleInfo();
                    role.Id = item.Id;
                    role.RoleName = item.RoleName;
                    role.RoleKey = item.RoleKey;
                    role.CreateTime = item.CreateTime.ToString();

                    result.Data.Add(role);
                }
            }

            return Task.FromResult(result);
        }
    }
}
启动服务端项目

浏览器访问http://localhost:5200/,界面显示如下信息。

该信息表明你尝试使用 HTTP/1.x 协议向仅支持 HTTP/2 协议的端点发送请求,这是不被允许的,从而导致了错误。可以不用管。

下面将演示客户端如何调用,客户端可以是任何类型项目,例如Web程序、窗体程序、控制台程序。

Web客户端
创建Web客户端项目

创建Asp.Net Core Web应用(模型-视图-控制器)。

拷贝服务端Proto协议文件

创建Protos文件夹,并将服务端Protos文件夹及其下面的2个ptoto文件移植到客户端项目中。

添加服务引用

参考服务端的方式,项目右键,找到 添加→服务引用,单击,在弹窗中选择“gRPC”,点击“下一步”。

选择对应的proto文件,然后生成的类的类型选择“客户端”。点击“完成”按钮。

第一次添加添加服务引用的时候,会自动往项目中添加Grpc.AspNetCore包,后续添加新proto文件或者需要更换原proto文件时就不需要再通过添加服务引用的方式添加。

点击“+”号按钮后再按之前步骤添加role.proto文件。

注:后续若要替换order.proto、role.proto 或者 需要添加新的proto文件,只需要把对应的文件复制粘贴到客户端Protos文件夹中,然后更改生成的类的类型即可,有几种方式可以修改。

  1. 直接点击文件,在右下角属性面板中找到“gRPC Stub Classes” 选项,将“Server only”改为“Client Only”。

  1. 在服务引用列表找到对应文件右边的...,点击找到“编辑”选项,在弹框中修改。

  1. 直接双击项目,在项目文件中找到对应的Protobug标签,将GrpcServices选项的值修改为“Client”即可。

发起请求
  1. 打开控制器HomeController,添加如下代码。
using Grpc.Net.Client;
using GrpcService_Client.Models;
using GrpcService_Order_Server;
using GrpcService_Role_Server;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

namespace GrpcService_Client.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        RoleManager.RoleManagerClient clientRole = null;
        Order.OrderClient clientOrder = null;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;

            //获取grpc客户端对象,地址填写grpc服务端的地址
            var channel = GrpcChannel.ForAddress("http://localhost:5200");

            clientOrder = new Order.OrderClient(channel);
            clientRole = new RoleManager.RoleManagerClient(channel);
        }

        public IActionResult Index()
        {
            #region 订单
            var orderReply = clientOrder.CreateOrder(new CreateRequest()
            {
                OrderNo = "Grpc_OrderNo_" + DateTime.Now.ToString("yyyMMddHHmmss"),
                GoodName = "Iphone16",
                GoodPrice = 5999,
                GoodCount = 1,
                GoodTotalPrice = 5999
            });
            ViewBag.OrderCreateMsg = orderReply.Result == true ? orderReply.Message : "创建订单失败";

            if (!string.IsNullOrEmpty(orderReply.Data.OrderId))
            {
                var orderQuery = new QueryRequest() { OrderId = orderReply.Data.OrderId };
                QueryResult orderInfo = clientOrder.QueryOrder(orderQuery);
                if (orderInfo.Result == true && orderInfo.Data != null)
                {
                    ViewBag.OrderInfo = orderInfo.Data;
                }
            }
            #endregion


            #region 角色
            int queryRoleId = 0;
            //获取所有角色数据
            RoleListResult roleListRes = clientRole.GetRoleAll(new Google.Protobuf.WellKnownTypes.Empty());
            if (roleListRes.Result == true && roleListRes.Data != null && roleListRes.Data.Count > 0)
            {
                ViewBag.RoleList = roleListRes.Data;
                queryRoleId = roleListRes.Data.FirstOrDefault().Id;
            }

            //获取单条角色数据
            if (queryRoleId > 0)
            {
                RoleRequest roleInfoQueryParam = new RoleRequest() { RoleId = queryRoleId };
                RoleResult roleResult = clientRole.GetRoleByID(roleInfoQueryParam);
                if (roleResult.Result == true && roleResult.Data != null)
                {
                    ViewBag.RoleInfo = roleResult.Data;
                }
            }
            #endregion

            return View();
        }
    }
}

对应视图配置如下内容:

@{
    ViewData["Title"] = "Grpc";
}

@model IEnumerable<GrpcService_Role_Server.RoleInfo>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            padding: 20px;
        }

        h2 {
            margin-top: 30px;
            border-bottom: 1px solid #ccc;
            padding-bottom: 10px;
        }

        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 15px;
        }

        table th,
        table td {
            border: 1px solid #ddd;
            padding: 10px;
            text-align: center;
        }

        table thead {
            background-color: #f2f2f2;
        }
    </style>
</head>
<body>
    <h2>创建订单是否成功:@ViewBag.OrderCreateMsg</h2>
    <h2>查询刚创建的订单信息</h2>
    <table class="table">
        <thead>
            <tr>
                <th>订单编号</th>
                <th>商品名称</th>
                <th>商品数量</th>
                <th>商品价格</th>
                <th>商品总价</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>@ViewBag.OrderInfo.OrderNo</td>
                <td>@ViewBag.OrderInfo.GoodName</td>
                <td>@ViewBag.OrderInfo.GoodCount</td>
                <td>@ViewBag.OrderInfo.GoodPrice</td>
                <td>@ViewBag.OrderInfo.GoodTotalPrice</td>
            </tr>
        </tbody>
    </table>

    <h2>查询角色ID = 1的角色信息</h2>
    <h3>角色信息</h3>
    <table class="table">
        <thead>
            <tr>
                <th>角色ID</th>
                <th>角色名称</th>
                <th>角色Key</th>
                <th>创建时间</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>@ViewBag.RoleInfo.Id</td>
                <td>@ViewBag.RoleInfo.RoleName</td>
                <td>@ViewBag.RoleInfo.RoleKey</td>
                <td>@ViewBag.RoleInfo.CreateTime</td>
            </tr>
        </tbody>
    </table>

    <h2>查询所有角色数据</h2>
    <h3>角色列表</h3>
    <table class="table">
        <thead>
            <tr>
                <th>角色ID</th>
                <th>角色名称</th>
                <th>角色Key</th>
                <th>创建时间</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var role in ViewBag.RoleList)
            {
                <tr>
                    <td>@role.Id</td>
                    <td>@role.RoleName</td>
                    <td>@role.RoleKey</td>
                    <td>@role.CreateTime</td>
                </tr>
            }
        </tbody>
    </table>
</body>
</html>
请求结果

将客户端和服务端同时设置为启动项,运行程序。

控制台客户端
创建控制台项目

拷贝服务端Proto协议文件

创建Protos文件夹,并将服务端Protos文件夹及其下面的greet.proto文件移植到客户端项目中。

添加服务引用

参考服务端的方式,项目右键,找到 添加→服务引用,单击,在弹窗中选择“gRPC”,点击“下一步”。

选择对应的proto文件,然后生成的类的类型选择“客户端”。点击“完成”按钮。

第一次添加添加服务引用的时候,会自动往项目中添加三个Grpc相关的包,后续添加新proto文件或者需要更换原proto文件时就不需要再通过添加服务引用的方式添加。

调用服务端

创建请求类文件gRPCRequest,调用服务端方法。

public class gRPCRequest
{
    public async Task TestgRPCService()
    {
        using (var channel = GrpcChannel.ForAddress("http://localhost:5200"))
        {
            Console.WriteLine("-----------------控制台客户端调用-----------------");
            var client = new Greeter.GreeterClient(channel);
            var reply = await client.SayHelloAsync(new HelloRequest { Name = "World" });
            Console.WriteLine("Greeting: " + reply.Message);
        }
    }
}
修改启动类文件
 static void Main(string[] args)
 {
     new gRPCRequest().TestgRPCService().Wait();
 }
运行项目

WebApi客户端
创建WebApi客户端项目

导包

导入Grpc.Core.Api 、 Grpc.Tools、Grpc.Net.ClientFactory、Google.Protobuf 包。

拷贝服务端Proto协议文件

创建Protos文件夹,并将服务端Protos文件夹及其下面的greet.proto文件移植到客户端项目中。

点击文件,在右下角属性面板中找到“gRPC Stub Classes” 选项,将“Server only”改为“Client Only”。

注册配置

打开Program.cs,添加配置,向依赖注入容器中注册gRPC客户端。

泛型参数 Greeter.GreeterClient 指明了要注册的客户端类型。这个类型通常是通过 gRPC 工具根据 .proto 文件生成的。

 builder.Services.AddGrpcClient<Greeter.GreeterClient>(opt =>
 {
     //指定客户端要连接的 gRPC 服务地址
     opt.Address = new Uri("http://localhost:5200");
 });
添加控制器

创建Api控制器gRPCController,通过构造函数注入。

 /// <summary>
 /// grpc controller
 /// </summary>
 [Route("api/[controller]")]
 [ApiController]
 public class gRPCController : ControllerBase
 {
     private readonly Greeter.GreeterClient _greeterClient;

     public gRPCController(Greeter.GreeterClient greeterClient)
     {
         this._greeterClient = greeterClient;
     }

     [HttpGet]
     public async Task<IActionResult> SayHello()
     {
         var result = await _greeterClient.SayHelloAsync(new HelloRequest() { Name = "Jack" });
         return Ok(result.Message);
     }
 }
运行项目

posted @ 2025-03-25 17:58  相遇就是有缘  阅读(71)  评论(0)    收藏  举报