.NetCore使用Grpc通信,简单的微服务+JWT认证
首先创建一个客户端和服务端,服务端选择创建GRPC服务,客户端就用WebApi就可以了,也可以用控制台、MVC等
服务端:
先安装 Grpc.AspNetCore 和 protobuf-net 两个nuget包
创建.proto文件。

syntax ="proto3"; option csharp_namespace="DataService01.protos"; package WeService01.Controllers; message users{ int32 ID=1; string name=2; string login_name=3; int32 roleid=4; bool is_man=5; } message getusers{ int32 ID=1; string name=2; } message getusersresponse{ int32 code=1; string msg=2; users usermodel =3; } message addphoto{ bytes data=1; } message get_token{ string login_name=1; string password=2; } message return_token{ string token=1; string expire_time=2; } service userservice{ rpc Getuser(getusers) returns (getusersresponse); rpc Add(stream addphoto) returns (getusers); rpc getall(getusers) returns (stream getusersresponse); rpc saveall(stream addphoto) returns (stream getusersresponse); rpc gettoken(get_token) returns (return_token); };
proto文件我个人理解就像定义接口,文件中指定了方法名、接收参数类型、返回参数类型等。
syntax="proto3" 表示proto3的版本,不写默认是proto2版本。
option csharp_namespace="DataService01.protos"; 是指c#生成代码的命名空间,message users{} 表示传输的类型,可以是请求或者返回类型,{}内的 id=1; name=2; 为自定义,同一消息类型中12345 这些编号不能重复。
service 服务名称{
rpc 服务的接口名称(接收参数类型) returns (stream 返回参数类型);//stream 表示持续传输 一般是list 或者文件传输等,没有这个关键字请求后就结束了,称为一元请求
}
proto文件创建好之后,设置文件属性为的build Action=Protobuf compiler;grpc stub classes=Server Only; 这里服务端所以选Server Only 客户端就选 Client Only
将文件复制给客户端,客户端也安装开头说的两个nuget包,并且设置文件属性。【文件属性是依赖 protobuf-net 这个nuget包】
设置之后项目生成时会生成grpc需要的文件,默认在Debug文件夹下;proto文件配置的csharp_namespace 和package 也在文件中体现。生成的文档不建议修改。
#region
proto文件需要客户端和服务端一样,可以不用复制的方式。项目“依赖项” 右键 “添加连接的服务”会显示项目内的已编写的proto文件作为服务,这样就可以写一份,服务端和客户端公用
#endregion
接下来编写服务端的业务逻辑代码;【代码中加入了JWT认证,写在最后;暂时先不介绍】
创建service文件例如ds01.cs 继承 proto定义的service;项目中proto文件中的 service 名称是 userservice,则创建的服务继承userservice.userserviceBase。在文件中override proto定义的方法

using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Grpc.AspNetCore.Server; using Grpc.AspNetCore; using DataService01.protos; using Grpc.Core; using System.IO; using Microsoft.AspNetCore.Authorization; using System.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using DataService01.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; namespace DataService01.Services { [Authorize(AuthenticationSchemes =JwtBearerDefaults.AuthenticationScheme)] public class ds01 : userservice.userserviceBase { private readonly ILogger<ds01> logger; private readonly IConfiguration configuration; private readonly IOptions<JWTDTO> jwt_Options; public ds01(ILogger<ds01> logger,IConfiguration configuration,IOptions<JWTDTO> Jwt_options) { this.logger = logger; this.configuration = configuration; jwt_Options = Jwt_options; } [AllowAnonymous] public override async Task<return_token> gettoken(get_token request, ServerCallContext context) { users _user = new users(); _user.LoginName = request.LoginName; if (request.LoginName.Equals("admin") && request.Password.Equals("123456")) { var jwttoken=await new JWTHelper().IssueJwt(_user, jwt_Options.Value); return await Task.FromResult(new return_token() { Token = jwttoken.Token, ExpireTime = new DateTimeOffset(jwttoken.ExpireTime).ToUnixTimeSeconds().ToString() }); } return await Task.FromResult(new return_token() { Token = "", ExpireTime = "" }); } public override Task<getusersresponse> Getuser(getusers request, ServerCallContext context) { var matedata_md=context.RequestHeaders; foreach (var pire in matedata_md) { logger.LogInformation($"{pire.Key}:{pire.Value}"); logger.LogInformation(pire.Key+":"+pire.Value); } users item = userdatas.userslist.SingleOrDefault(n => n.ID == request.ID); if (item != null) { return Task.FromResult(new getusersresponse() { Code = 0, Msg = "成功", Usermodel = item }); } else { return Task.FromResult(new getusersresponse() { Code = -1, Msg = "失败" }); } } public override async Task getall(getusers request, IServerStreamWriter<getusersresponse> responseStream, ServerCallContext context) { foreach (var item in userdatas.userslist) { //逐步返回数据 await responseStream.WriteAsync(new getusersresponse() { Usermodel = item } ) ; } } public override async Task<getusers> Add(IAsyncStreamReader<addphoto> requestStream, ServerCallContext context) { List<byte> bt = new List<byte>(); while (await requestStream.MoveNext())//有数据进入 { bt.AddRange(requestStream.Current.Data); } //while 执行完之后表示没有数据再进来 FileStream file = new FileStream(AppDomain.CurrentDomain.BaseDirectory+"01.png",FileMode.OpenOrCreate) ; file.Write(bt.ToArray(), 0, bt.Count); file.Flush(); file.Close(); return new getusers() { Name = "成功",ID = 0 }; } public override async Task saveall(IAsyncStreamReader<addphoto> requestStream, IServerStreamWriter<getusersresponse> responseStream, ServerCallContext context) { List<byte> bt = new List<byte>(); while (await requestStream.MoveNext())//有数据进入 { bt.AddRange(requestStream.Current.Data); } //while 执行完之后表示没有数据再进来 FileStream file = new FileStream("/01.png", FileMode.OpenOrCreate); file.Write(bt.ToArray(), 0, bt.Count); file.Flush(); file.Close(); //返回数据 foreach (var item in userdatas.userslist) { await responseStream.WriteAsync(new getusersresponse() { Msg = "成功", Code = 0, Usermodel = item }); } } } public class userdatas { public static IList<users> userslist = new List<users>() { new users(){ID=1,Name="11",LoginName="111",Roleid=1,IsMan=true}, new users(){ID=2,Name="22",LoginName="222",Roleid=2,IsMan=false}, new users(){ID=3,Name="33",LoginName="333",Roleid=3,IsMan=true} }; } }
服务端代码编写完成后需要配置Statup.cs。主要代码就一行,将写好的ds01写进Endpoints (终结点路由)
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<ds01>();
});

using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DataService01.Models; using DataService01.protos; using DataService01.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using System.Text; namespace DataService01 { public class Startup { private readonly IConfiguration configuration; public Startup(IConfiguration configuration) { this.configuration = configuration; } // 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(); services.AddAuthorization(option => option.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy => { policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.RequireClaim("sub"); })); //services.AddAuthorization(); services.AddAuthentication().AddJwtBearer(options=> { options.TokenValidationParameters = new TokenValidationParameters() { ValidIssuer = "http://localhost:5001", ValidAudience = "http://localhost:5000", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.GetSection("JWTDTO").GetSection("SecurityKey").Value))// "9e79234cd150108e5048d0e0cb4ca5e4" }; }); services.Configure<JWTDTO>(configuration.GetSection("JWTDTO")); } // 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.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { //endpoints.MapGet("/", async context => //{ // await context.Response.WriteAsync("Hello World!"); //}); endpoints.MapGrpcService<ds01>(); // endpoints.MapGrpcService<ds02>(); }); } } }
到此服务端结束;
客户端:
客户端需要和服务端一样的proto文件,可以复制过来,改属性Client Only,也可以使用 “添加连接的服务”,上面有写。项目重新生成之后也就会生成gRPC文件了。
using GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:5001");//创建服务链接 ,using 用完即销毁,可以不适用using
var service01 = new userservice.userserviceClient(channel);//连接服务
var md = new Metadata()//metadata 用于headers ,只能是数字字母字符,不能有中文,不然会报 Request headers must contain only ASCII characters 错误
{
{ "Name","deven" },
{ "ID","37" }
};
getusersresponse us = await service01.GetuserAsync(new getusers() { ID = 2 },headers:md);//一元请求+传送元数据 //调用服务的接口//Metadata 用于传输的Headers 可以是一些数据,稍后在JWT中会用到
以下是我测试使用的客户端代码,分段参考就行,整体逻辑不一定对

using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Google.Protobuf; using Grpc.Net.Client; using Grpc.AspNetCore.Server; using Grpc.AspNetCore; using DataService01.protos; using Grpc.Core; using System.IO; using Microsoft.Extensions.Logging; namespace WeService01.Controllers { [ApiController] [Route("[controller]")] public class HomeController : ControllerBase { private readonly ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { this._logger = logger; } [HttpGet(nameof(Index))] public async Task<IActionResult> Index() { // AppContext.SetSwitch("System.Net.Http.SockersHttpHandler.Http2UnencryptedSupport", true); #region 连接服务 using GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:5001"); var service01 = new userservice.userserviceClient(channel); #endregion #region 请求接口获取token,登录 return_token rt_token = service01.gettoken(new get_token() { LoginName = "admin", Password = "123456" });//获取JWTtoken var md_add_token = new Metadata() ; if (!string.IsNullOrEmpty(rt_token.Token)) { md_add_token.Add("Authorization", $"Bearer {rt_token.Token}");//将Token 添加到 Hearers里,key和Value 是固定写法,value中 Bearer 与token中间 要加一个空格 } #endregion #region 一元请求 var md = new Metadata()//metadata 用于headers ,只能是数字字母字符,不能有中文,不然会报 Request headers must contain only ASCII characters 错误 { { "Name","deven" }, { "ID","37" } }; getusersresponse us = await service01.GetuserAsync(new getusers() { ID = 2 },headers:md_add_token);//一元请求+传送元数据// header是Jwttoken _logger.LogInformation(us.Msg); #endregion #region 请求接口,发送结合数据 using var getall_response = service01.getall(new getusers());//stream 数据返回 while (await getall_response.ResponseStream.MoveNext()) { //取出每次返回的数据 _logger.LogInformation(getall_response.ResponseStream.Current.Msg); // return (IActionResult)Task.FromResult(Content(getall_response.ResponseStream.Current.Msg));//getall_response.ResponseStream.Current.Usermodel } #endregion #region 发送文件 //Bytes 数据传输 FileStream file = System.IO.File.OpenRead(AppDomain.CurrentDomain.BaseDirectory + "img/img01.png"); using var add_call = service01.Add(); var st = add_call.RequestStream; while (true) { byte[] bt = new byte[1024]; int meleng = await file.ReadAsync(bt, 0, bt.Length); if (meleng == 0)//=0表示读取完毕 { break; } if (bt.Length > meleng)//最后一次,读取可能少于1024,修改bt数组的长度 { Array.Resize(ref bt, meleng); } await st.WriteAsync(new addphoto() { Data = ByteString.CopyFrom(bt) });//传输数据 } await st.CompleteAsync();//通知服务端 数据传送完毕 getusers res = await add_call.ResponseAsync;//接受返回内容 _logger.LogInformation(res.Name);//打印日志 响应值 #endregion #region 双向Stream 数据传输 //双向Stream 数据传输 using var saveall_call= service01.saveall(); var saveall_req= saveall_call.RequestStream; var saveall_resp= saveall_call.ResponseStream; //首先定义 接受返回内容并处理的逻辑,等发送结束后再执行 var responsetask = Task.Run(async () => { while (await saveall_resp.MoveNext())//处理相应的内容 { _logger.LogInformation(saveall_resp.Current.Msg); } }); #region 发送请求的Stream 数据 while (true) { byte[] bt = new byte[1024]; int meleng = await file.ReadAsync(bt, 0, bt.Length);//将文件 分批发送 if (meleng == 0)//=0表示读取完毕 { break; } if (bt.Length > meleng)//最后一次,读取可能少于1024,修改bt数组的长度 { Array.Resize(ref bt, meleng); } await saveall_req.WriteAsync(new addphoto() { Data = ByteString.CopyFrom(bt) });//传输数据 } //先执行 发送数据的逻辑request, 再执行接受数据的逻辑,response;需要先执行 saveall_req.CompleteAsync() 通知服务端请求结束,服务端才能正确的返回 response await saveall_req.CompleteAsync();//通知服务端 数据传送完毕 await responsetask;//执行接受返回并处理的逻辑 #endregion return (IActionResult)Task.FromResult(Content(us.Msg)); } } }
客户端很简单,到这里就结束了。生产环境还需要使用到注册中心,我暂时还没了解该如何配置。
问题:微服务是多个,而且单个服务也需要分布式部署,需要在微服务前做负载均衡,降低故障率,分摊压力,使服务课横向扩展并实现热插拔,还能监控各服务的运行状态,合理分流。怎么才能达到这个效果呢?
通过了解 觉得 Consul组件 比较符合预期,另外还有ZooKeeper 等其他 服务注册中心 的组件 https://developer.aliyun.com/article/766176
JWT认证
接下来介绍一下JWT,用于客户端和服务端的身份认证。
请看另外一篇文章,主要介绍jwt
https://www.cnblogs.com/zeran/p/14481591.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示