.net8 使用grpc
服务端项目
创建Grpc服务端项目


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


可以看到项目中默认包含两个文件夹Protos、Services。
Protos文件夹用来存放 .proto格式的文件,这些文件用来定义grpc的服务和消息结构。项目可以根据该格式文件自动生成对应的类文件。
Services文件夹用来存放实现grpc服务的类,这些类继承自 .proto 文件中定义的服务基类,并实现其中的方法。

- Greeter用来定义服务,服务内包含具体的方法。可以看作是一个类。
- SayHello是定义服务的方法名,HttpRequest是该服务方法对应的传参对象,用message HelloRequest的方式表明该方法传参对象具体包括哪些参数。同理,HelloReply是该服务方法对应的返回结果对象,用message HelloReply的方式表名该方法返回结果对象具体包括哪些参数。
注意:参数类型必须是protobuf数据类型,另外如果方法不需要传参或没有返回值,切记不能为空,需要定义空类型google.protobuf.Empty。
- option csharp_namespace后面+命名空间名,package后面+包名,需注意每个proto文件的包名和命名空间不能重复。
- 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文件
- 鼠标右键Protos文件夹→添加→新建项→协议缓冲区文件,新增role.proto,order.proto两个文件。


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


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


- 编辑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 就知道这是 orderNo 字段的数据。
- 在消息定义发生变更时,字段编号有助于维持兼容性。若后续要对消息结构进行修改,比如添加新字段,只要不改动原有的字段编号,旧版本的客户端和服务器依旧能够正确处理消息。举例来说,若后续在 CreateRequest 里添加新字段 string customerName = 4;,旧版本的代码不会受到影响,因为它们会忽略不认识的字段编号。
- google.protobuf.Empty表示不需要参数,可以是请求不需要参数,也可以返回不需要参数。
添加服务引用
- 项目右键,找到 添加→服务引用,单击。

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

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

点击“关闭”。

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

添加之后如下所示。

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

添加数据库依赖包和Redis包
Services文件夹下依次创建 OrderService 和 RoleService 两个类文件。其中 RoleService 将会读取数据库表信息,而OrderService 不读取数据库表信息。
由于RoleService会读取数据库表数据,因此这里采用EFCORE连接SQL SERVER数据库,所以服务端项目添加三个相关包。

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

生成数据库表实体类文件
- 打开程序包管理器控制台,输入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文件和指定表对应的实体类文件。

- 去掉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文件夹中,然后更改生成的类的类型即可,有几种方式可以修改。
- 直接点击文件,在右下角属性面板中找到“gRPC Stub Classes” 选项,将“Server only”改为“Client Only”。

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


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

发起请求
- 打开控制器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);
}
}
运行项目


浙公网安备 33010602011771号