.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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM