.Net Core gRpc调用
简介
-
高性能、开源的通用 RPC 框架
-
实现不同语言相互调用
创建gRPC
创建服务端
- vs2022直接搜索grpc默认下一步创建
创建控制台测试
- 创建控制台
- 引入以下dll
<PackageReference Include="Google.Protobuf" Version="3.23.4" />
<PackageReference Include="Grpc.Net.Client" Version="2.55.0" />
<PackageReference Include="Grpc.Tools" Version="2.56.2">
- 打开服务端.csproj文件,复制以下内容粘贴到客户端的.csproj文件中并修改GrpcServices=Client,客户端.csproj文件出现 None Update="Protos\greet.proto" 这组ItemGroup是可以删除的
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
4.将服务端Protos文件夹及内容全部拷贝到客户端项目下
5.在客户端创建gRpcRequest.cs文件并增加下列代码,端口号填写服务端端口
using Grpc.Net.Client;
using GrpcService;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static GrpcService.Greeter;
namespace gRpcConsoleTest
{
public static class gRpcRequest
{
public static async Task SayHello()
{
using(var channel = GrpcChannel.ForAddress("https://localhost:7166"))
{
GreeterClient client = new GreeterClient(channel);
HelloReply reply = await client.SayHelloAsync(new HelloRequest() {Name = "jjjjj" });
Console.WriteLine(reply.Message);
}
}
}
}
6.客户端Program.cs文件中加入测试代码 await gRpcRequest.SayHello();
7.服务端和客户端在想要调试的位置打上断点
8.运行服务端
9.选中客户端项目右键 -> 调试 -> 启动新实例 即可两个项目全部命中断点进行调试测试
创建自定义服务
-
服务端
- Protos文件夹添加 custom.proto文件并添加下列代码,重新生成项目打开项目所在文件夹,打开路径:obj\Debug\net6.0\Protos 查看CustomGrpc.cs是否存在,如果没有存在则在.csproj文件中添加:
syntax = "proto3"; option csharp_namespace = "Custom.Service"; package custom; service CustomGreeter { rpc Plus(Number) returns (NumberResult) ; } message Number { int32 leftNumber = 1; int32 rightNumber = 2; } message NumberResult{ int32 result = 1; }
- Services文件夹下添加CustomGreeterService.cs文件,namespace 与 .protos中的csharp_namespace对应
using Grpc.Core; namespace Custom.Service { public class CustomGreeterService : CustomGreeter.CustomGreeterBase { public override async Task<NumberResult> Plus(Number request, ServerCallContext context) => await Task.FromResult<NumberResult>(new NumberResult() { Result = request.LeftNumber + request.RightNumber, }); } }
3.在Program.cs 中注册新创建的服务,加入下列代码:
app.MapGrpcService(); - Protos文件夹添加 custom.proto文件并添加下列代码,重新生成项目打开项目所在文件夹,打开路径:obj\Debug\net6.0\Protos 查看CustomGrpc.cs是否存在,如果没有存在则在.csproj文件中添加:
-
客户端
- 将服务端的custom.proto文件拷贝到Protos文件夹内并在.csproj文件中添加(注意这里GrpcServices="Client"):
- 重新生成项目并检查路径: obj\Debug\net6.0\Protos 是否生成对应的CustomGrpc.cs文件
- 加入测试代码:
public static async Task Plus(int leftNumber,int rightNumber) { using (var channel = GrpcChannel.ForAddress("https://localhost:7166")) { CustomGreeterClient client = new CustomGreeterClient(channel); NumberResult number = await client.PlusAsync(new Custom.Service.Number() { LeftNumber = leftNumber,RightNumber = rightNumber}); Console.WriteLine(number.Result); } }
- 测试步骤与上面控制台测试一样,调试方式也一样
- 将服务端的custom.proto文件拷贝到Protos文件夹内并在.csproj文件中添加(注意这里GrpcServices="Client"):
服务器流式处理方法
custom.proto
syntax = "proto3";
option csharp_namespace = "Custom.Service";
package custom;
service CustomGreeter {
rpc SelfIncreaseServer(IntArrayModel) returns (stream BathTheCatResp); //服务端流
}
message BathTheCatResp{
string message = 1;
}
message IntArrayModel{
repeated int32 number = 1;
}
CustomGreeterService.cs
using Grpc.Core;
namespace Custom.Service
{
public class CustomGreeterService : CustomGreeter.CustomGreeterBase
{
public override async Task SelfIncreaseServer(IntArrayModel request, IServerStreamWriter<BathTheCatResp> responseStream, ServerCallContext context)
{
foreach (var item in request.Number)
{
Console.WriteLine($"客户端传入参数: {item}");
await responseStream.WriteAsync(new BathTheCatResp() { Message = item.ToString()});
await Task.Delay(1000);
}
}
}
}
gRpcRequest.cs
using Custom.Service;
using Grpc.Core;
using Grpc.Net.Client;
using GrpcService;
using static Custom.Service.CustomGreeter;
namespace gRpcConsoleTest
{
public static class gRpcRequest
{
public static async Task SelfIncreaseServe()
{
using (var channel = GrpcChannel.ForAddress("https://localhost:7166"))
{
CustomGreeterClient client = new CustomGreeterClient(channel);
IntArrayModel intArray = new IntArrayModel();
for (int i = 0; i < 10; i++)
{
intArray.Number.Add(i);
}
var batch = client.SelfIncreaseServer(intArray);
Task batchTask = Task.Run(async ()=>
{
await foreach (var item in batch.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"服务端相应数据: {item.Message}");
}
});
await batchTask;
}
}
}
}
客户端流式处理方法
custom.proto
syntax = "proto3";
option csharp_namespace = "Custom.Service";
package custom;
service CustomGreeter {
rpc SelfIncreaseClient(stream BathTheCatReq) returns (IntArrayModel); //客户端流
}
message BathTheCatResp{
string message = 1;
}
message IntArrayModel{
repeated int32 number = 1;
}
CustomGreeterService.cs
using Grpc.Core;
namespace Custom.Service
{
public class CustomGreeterService : CustomGreeter.CustomGreeterBase
{
public override async Task<IntArrayModel> SelfIncreaseClient(IAsyncStreamReader<BathTheCatReq> requestStream, ServerCallContext context)
{
IntArrayModel result = new IntArrayModel();
while (await requestStream.MoveNext())
{
var message = requestStream.Current;
Console.WriteLine($"客户端流传入消息: {message}");
result.Number.Add(message.Id + 1);
}
return result;
}
}
}
gRpcRequest.cs
using Custom.Service;
using Grpc.Core;
using Grpc.Net.Client;
using GrpcService;
using static Custom.Service.CustomGreeter;
namespace gRpcConsoleTest
{
public static class gRpcRequest
{
public static async Task SelfIncreaseClient()
{
using (var channel = GrpcChannel.ForAddress("https://localhost:7166"))
{
CustomGreeterClient client = new CustomGreeterClient(channel);
var batch = client.SelfIncreaseClient();
for (int i = 0; i < 10; i++)
{
await batch.RequestStream.WriteAsync(new BathTheCatReq() { Id = i });
await Task.Delay(1000);
}
await batch.RequestStream.CompleteAsync();
foreach (var item in batch.ResponseAsync.Result.Number)
{
Console.WriteLine($"响应数据: {item}");
}
}
}
}
}
双向流式处理方法
custom.proto
syntax = "proto3";
option csharp_namespace = "Custom.Service";
package custom;
service CustomGreeter {
rpc SelfIncreaseDouble(stream BathTheCatReq) returns (stream BathTheCatResp);//双端流
}
message BathTheCatReq{
int32 id = 1;
}
message BathTheCatResp{
string message = 1;
}
CustomGreeterService.cs
using Grpc.Core;
namespace Custom.Service
{
public class CustomGreeterService : CustomGreeter.CustomGreeterBase
{
public override async Task SelfIncreaseDouble(IAsyncStreamReader<BathTheCatReq> requestStream, IServerStreamWriter<BathTheCatResp> responseStream, ServerCallContext context)
{
while (await requestStream.MoveNext())
{
var message = requestStream.Current.Id;
Console.WriteLine($"客户端流传入消息: {message}");
await responseStream.WriteAsync(new BathTheCatResp() { Message=(message+1).ToString()});
}
}
}
}
gRpcRequest.cs
using Custom.Service;
using Grpc.Core;
using Grpc.Net.Client;
using GrpcService;
using static Custom.Service.CustomGreeter;
using static GrpcService.Greeter;
namespace gRpcConsoleTest
{
public static class gRpcRequest
{
public static async Task SelfIncreaseDouble()
{
using (var channel = GrpcChannel.ForAddress("https://localhost:7166"))
{
CustomGreeterClient client = new CustomGreeterClient(channel);
var batch = client.SelfIncreaseDouble();
Task batchTask = Task.Run(async () =>
{
await foreach (var item in batch.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"服务端相应数据: {item.Message}");
}
});
for (int i = 0; i < 10; i++)
{
await batch.RequestStream.WriteAsync(new BathTheCatReq() { Id = i });
await Task.Delay(1000);
}
await batchTask;
}
}
}
}
.Net Core 调用gRpc
项目引用
<PackageReference Include="Google.Protobuf" Version="3.24.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.55.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.55.0" />
<PackageReference Include="Grpc.Tools" Version="2.56.2">
<ItemGroup>
<Protobuf Include="Protos\custom.proto" GrpcServices="Client" />
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
Program.cs
//CustomGreeterClient grpc连接类
builder.Services.AddGrpcClient<CustomGreeterClient>(options =>
{
options.Address = new Uri("https://localhost:7166"); //grpc 服务地址
});
gRpcController.cs
using Custom.Service;
using Microsoft.AspNetCore.Mvc;
using static Custom.Service.CustomGreeter;
namespace gRpcWebAPI.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class gRpcController : ControllerBase
{
CustomGreeterClient _client; //使用构造函数注入
public gRpcController(CustomGreeterClient client)
{
_client = client;
}
[HttpGet]
public async Task<IActionResult> Plus(int leftNumber, int rightNumber)
{
NumberResult number = await _client.PlusAsync(new Custom.Service.Number() { LeftNumber = leftNumber, RightNumber = rightNumber });
return new JsonResult(number);
}
}
}
支持Aop
- 服务端, 客户端 都需要继承 Interceptor
- 重新需要实现Aop的方法,如服务端: UnaryServerHandler,客户端: AsyncUnaryCall 针对一元调用的Aop
- 我这里使用的是NLog写的日志,NLog可以使用可以翻阅我先前的博客
.Net Core NLog+oracel
服务端 Program.cs
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LogInterceptor>();
});
服务端 LogInterceptor.cs
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace GrpcService.Interceptors
{
public class LogInterceptor : Interceptor
{
ILogger<LogInterceptor> _logger;
public LogInterceptor(ILogger<LogInterceptor> logger)
{
_logger = logger;
}
public override Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation("===========UnaryServerHandler==========");
return continuation(request, context);
}
}
}
客户端 Program.cs
builder.Services.AddGrpcClient<CustomGreeterClient>(options =>
{
options.Address = new Uri("https://localhost:7166"); //服务端地址
}).AddInterceptor<LogInterceptor>();
客户端 LogInterceptor.cs
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace GrpcService.Interceptors
{
public class LogInterceptor : Interceptor
{
ILogger<LogInterceptor> _logger;
public LogInterceptor(ILogger<LogInterceptor> logger)
{
_logger = logger;
}
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
_logger.LogInformation("===========AsyncUnaryCall===========");
return continuation(request, context);
}
}
}
jwt+gRPC验证
- 准备单独的一个网站发布jwt Token (授权中心)
- 然后在gRPC项目中jwt鉴权 和 获取到角色信息之后授权
- webapi测试gRPC调用, 先在授权中心获取token然后在 ConfigureChannel 方法中设置gRPC全局的jwt token
- 返回401: jwt鉴权不通过
- 返回403: jwt授权不通过
准备Jwt Token发布中心
Program.cs
//读取Jwt配置
builder.Services.Configure<JwtConfig>(builder.Configuration.GetSection("JwtTokenOptions"));
JwtConfig.cs
namespace AuthenorizationCenter.Tools.Model
{
public class JwtConfig
{
public string? Audience { get; set; }
public string? Issuer { get; set; }
public string? SecurityKey { get; set; }
public int ExpiresMinutes { get; set; }
}
}
AuthenorizationController.cs
using AuthenorizationCenter.Tools;
using AuthenorizationCenter.Tools.Model;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace AuthenorizationCenter.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthenorizationController : ControllerBase
{
JwtConfig _jwtconfig;
public AuthenorizationController(IOptions<JwtConfig> jwtconfig)
{
_jwtconfig = jwtconfig.Value;
}
[HttpGet]
public async Task<string> GetToken(string userName, string passWord)
{
string token = JwtHeleper.GetToken(new()
{
UserName = userName,
Extended1 = "无信息",
Role = new List<RoleInfo>()
{
new RoleInfo() { Id = "1",Role="系统管理员"} ,
new RoleInfo() { Id = "2",Role="用户管理员"} ,
}
}, new()
{
Audience = _jwtconfig.Audience,
Issuer = _jwtconfig.Issuer,
SecurityKey = _jwtconfig.SecurityKey,
ExpiresMinutes = 5,
});
await Task.CompletedTask;
return token;
}
}
}
JwtHeleper.cs
using AuthenorizationCenter.Tools.Model;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace AuthenorizationCenter.Tools
{
public class JwtHeleper
{
public static string GetToken(UserInfo user, JwtConfig jwtConfig)
{
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName ?? ""),
new Claim("Extended1", user.Extended1 ?? ""),
new Claim("Extended2", user.Extended2 ?? ""),
new Claim("Extended3", user.Extended3 ?? ""),
new Claim("Extended4", user.Extended4 ?? ""),
new Claim("Extended5", user.Extended5 ?? ""),
};
if (user.Role is not null)
{
foreach (var item in user.Role)
{
claims.Add(new Claim(item.Id.ToString(), item.Role));
}
}
if (jwtConfig.SecurityKey == null)
{
throw new Exception("JwtConfig.SecurityKey 不能为空");
}
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.SecurityKey));
SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
JwtSecurityToken token = new JwtSecurityToken(
issuer: jwtConfig.Issuer,
audience: jwtConfig.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(jwtConfig.ExpiresMinutes),
signingCredentials: creds
);
string resultToken = new JwtSecurityTokenHandler().WriteToken(token);
return resultToken;
}
}
}
RoleInfo.cs
namespace AuthenorizationCenter.Tools.Model
{
public class RoleInfo
{
public string Id { get; set; }
public string Role { get; set; }
}
}
UserInfo.cs
namespace AuthenorizationCenter.Tools.Model
{
public class UserInfo
{
public string? UserName { get; set; }
public List<RoleInfo>? Role { get; set; }
public string? Extended1 { get; set; }
public string? Extended2 { get; set; }
public string? Extended3 { get; set; }
public string? Extended4 { get; set; }
public string? Extended5 { get; set; }
}
}
appsetting.json
{
"JwtTokenOptions": {
"Issuer": "https://localhost:7117",
"Audience": "https://localhost:7117",
"SecurityKey": "kq4DY5N1eFJhscOkI7Zp4Nd0WNy9d9AEsN6Yjgdv9OxLyol66tzGBKT_7vwolN7GZ8EDwqJBwccjDJfb81ws5s3sbbP5wUzQ3-PcTSsD-Rueiu2rsOUZwg_NR3RBCwmtouV-832YV2trCjNTawLB1z0LMukWGFNaAJVZ8WdQcrYn6a0ko5oVhZqaHBgsCLEGiqPtoFsiCcrJTz1IvXHk9_cDSr2hwEmSl18GlkOtgCHFH8aidYth3aQHRHuClTi6Y9mYRJtqqK-FNQYq4ZP23DSGZGFejJFTnM9YMpppuTMLklhSGySwX8rfjZ_0L5ac18nHaykTaiC2fvH00W42qQ"
}
}
gRPC准备
Program.cs
JwtConfig jwtConfig = new JwtConfig();
builder.Configuration.Bind("JwtTokenOptions", jwtConfig);
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = jwtConfig.Issuer, //发行人
ValidateAudience = true,
ValidAudience = jwtConfig.Audience,//订阅人
ValidateIssuerSigningKey = true,
//对称加密密钥
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.SecurityKey!)),
ValidateLifetime = true, //验证失效时间
ClockSkew = TimeSpan.FromSeconds(30), //过期时间容错值
RequireExpirationTime = true,
AudienceValidator = (audiences, securityToken, validationParameters) =>
{
return true;
},
LifetimeValidator = (notBefore, expires, securityToken, validationParameters) =>
{
return true;
}
};
});
builder.Services.AddTransient<IUserServices, UserServices>();
builder.Services.AddTransient<IAuthorizationHandler, JwtAuthorization>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("JwtPolicy", policy =>
{
//jwt 授权
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
//这里为自定义授权指定一下类
.AddRequirements(new UserRoleRequirement(JwtBearerDefaults.AuthenticationScheme));
});
});
CustomGreeterService.cs
using Grpc.Core;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
namespace Custom.Service
{
public class CustomGreeterService : CustomGreeter.CustomGreeterBase
{
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme,Policy = "JwtPolicy")]
public override async Task<NumberResult> Plus(Number request, ServerCallContext context) =>
await Task.FromResult<NumberResult>(new NumberResult()
{
Result = request.LeftNumber + request.RightNumber,
});
}
}
JwtAuthorization.cs
using Cnpc.Com.Ioc.IBll;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
namespace GrpcService.Authorization
{
public class UserRoleRequirement : IAuthorizationRequirement
{
public string AuthenticateScheme;
public UserRoleRequirement(string authenticateScheme)
{
AuthenticateScheme = authenticateScheme;
}
}
public class JwtAuthorization : AuthorizationHandler<UserRoleRequirement>
{
IUserServices userSercices;
public JwtAuthorization(IUserServices userSercices)
{
this.userSercices = userSercices;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserRoleRequirement requirement)
{
string? userName = context.User.FindFirst(it => it.Type == ClaimTypes.Name)?.Value;
if (userSercices.IsAdmin(userName!))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
}
客户端调用
Program.cs
builder.Services.AddGrpcClient<CustomGreeterClient>(options =>
{
options.Address = new Uri("https://localhost:7166");
}).AddInterceptor<LogInterceptor>().ConfigureChannel(async config =>
{
//所有调用自动添加 Authorization
CallCredentials credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
{
string token = await HttpClientHelper.HttpGetAsync("https://localhost:7117/api/Authenorization?userName=admin&passWord=666");
metadata.Add("Authorization", $"Bearer {token}");
});
config.Credentials = ChannelCredentials.Create(new SslCredentials(), credentials);
});
HttpClientHelper.cs
using Newtonsoft.Json;
using System.Text;
namespace gRpcWebAPI.Utility
{
public static class HttpClientHelper
{
public static async Task<string> HttpGetAsync(string url, string contentType = "application/json", Dictionary<string, string> headers = null)
{
using (System.Net.Http.HttpClient client = new System.Net.Http.HttpClient())
{
if (contentType != null)
client.DefaultRequestHeaders.Add("ContentType", contentType);
if (headers != null)
{
foreach (var header in headers)
client.DefaultRequestHeaders.Add(header.Key, header.Value);
}
HttpResponseMessage response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
}
}
}
gRpcController.cs
using Custom.Service;
using Grpc.Core;
using gRpcWebAPI.Utility;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Security.Cryptography.X509Certificates;
using static Custom.Service.CustomGreeter;
namespace gRpcWebAPI.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class gRpcController : ControllerBase
{
CustomGreeterClient _client;
public gRpcController(CustomGreeterClient client)
{
_client = client;
}
[HttpGet]
public async Task<IActionResult> AsyncPlus(int leftNumber, int rightNumber)
{
try
{
//string token = await HttpClientHelper.HttpGetAsync("https://localhost:7117/api/Authenorization?userName=admin&passWord=666");
//Metadata jwtCode = new Metadata { { "Authorization", $"Bearer {token}" } };
NumberResult number = await _client.PlusAsync(new Custom.Service.Number() { LeftNumber = leftNumber, RightNumber = rightNumber });
return new JsonResult(number);
}
catch (Exception ex)
{
return new JsonResult(ex.Message);
}
}
[HttpGet]
public IActionResult Plus(int leftNumber, int rightNumber)
{
try
{
string token = HttpClientHelper.HttpGet("https://localhost:7117/api/Authenorization?userName=admin&passWord=666");
Metadata jwtCode = new Metadata { { "Authorization",$"Bearer {token}"} };
NumberResult number = _client.Plus(new Custom.Service.Number() { LeftNumber = leftNumber, RightNumber = rightNumber },headers: jwtCode);
return new JsonResult(number);
}
catch (Exception ex)
{
return new JsonResult(ex.Message);
}
}
}
}
Rpc 与 Restful 区别
-
RPC是以一种调用本地方法的思路来调用远程方法,通过各种RPC框架隐藏调用远程方法的细节,让用户以为调用的就是本地方法。RPC隐藏了底层网络通信的复杂度,让我们更专注于业务逻辑的开发。
-
REST通过HTTP实现,把用户的需求抽象成对资源的操作,用户必须通过HTTP协议的GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS七种基本操作去和服务器交互。
-
RPC通常是服务器和服务器之间的通信,比如和中间件的通信,MQ、分布式缓存、分布式数据库等等。
-
而REST通常是面向客户端的(一般是浏览器),他们的使用场景也是不一样的。