首先創建一個客戶端和服務端,服務端選擇創建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