.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);
};
user.proto

 

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}
        };
    }
}
ds01.cs

 服务端代码编写完成后需要配置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>();
            });
        }
    }
}
Statup

 到此服务端结束;

客户端:

客户端需要和服务端一样的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));
        }
    }
}
HomeController

 客户端很简单,到这里就结束了。生产环境还需要使用到注册中心,我暂时还没了解该如何配置。

问题:微服务是多个,而且单个服务也需要分布式部署,需要在微服务前做负载均衡,降低故障率,分摊压力,使服务课横向扩展并实现热插拔,还能监控各服务的运行状态,合理分流。怎么才能达到这个效果呢?

通过了解 觉得 Consul组件 比较符合预期,另外还有ZooKeeper 等其他 服务注册中心 的组件 https://developer.aliyun.com/article/766176

JWT认证

接下来介绍一下JWT,用于客户端和服务端的身份认证。

请看另外一篇文章,主要介绍jwt
https://www.cnblogs.com/zeran/p/14481591.html

posted @ 2021-03-04 17:05  zeran  阅读(1087)  评论(1编辑  收藏  举报