前言
一直以來對內部服務間使用RPC的方式呼叫都比較贊同,因為內部間沒有這麼多限制,最簡單明瞭的方式就是最合適的方式。個人比較喜歡類似Dubbo的那種使用方式,把介面層單獨出來,作為服務的契約,服務端以這套契約提供服務,客戶端使用這套契約呼叫服務,和使用本地方法的方式是一樣的。.Net平臺上類似Dubbo這種相對比較完善的RPC框架還是比較少的,GRPC確實是一款非常優秀的RPC框架,能誇語言呼叫,但是從個人的角度上來說每次還得編寫proto檔案感覺還是比較麻煩的。如今服務拆分,微服務架構比較盛行的潮流下,一個簡單實用的RPC框架確實可以提升很多開發效率。
簡介
隨著.Net Core逐漸成熟穩定,為我一直以來想實現的這個目標提供了便利的方式。於是利用閒暇時間本人手寫了一套基於Asp.Net Core的RPC框架,算是實現了一個自己的小目標。大致的實現方式,Server端依賴Asp.Net Core,採用的是中介軟體的方式攔截處理請求比較方便。Client端可以是任何可承載.Net Core的宿主程式。通訊方式是HTTP協議,使用的是HttpClientFactory。至於為什麼使用HttpClientFactory,因為HttpClientFactory可以更輕鬆的實現服務發現,而且可以很好的整合Polly,很方便的實現,超時重試,熔斷降級這些,給開發過程中提供了很多便利。由於本人能力有限,基於這些便利,站在巨人的肩膀上,簡單的實現了一個RPC框架,專案託管在GitHub上https://github.com/softlgl/DotNetCoreRpc有興趣的可以自行查閱。
開發環境
- Visual Studio 2019
- .Net Standard 2.1
- Asp.Net Core 3.1.x
使用方式
開啟Visual Studio先新建一個RPC契約介面層,這裡我起的名字叫IRpcService。然後新建一個Client層(可以是任何可承載.Net Core的宿主程式)叫ClientDemo,然後建立一個Server層(必須是Asp.Net Core專案)叫WebDemo,文末附本文Demo連線,建完這些之後專案結構如下:
Client端配置
Client端引入DotNetCoreRpc.Client包,並引入自定義的契約介面層
<PackageReference Include="DotNetCoreRpc.Client" Version="1.0.2" />
然後可以愉快的編碼了,大致編碼如下
class Program
{
static void Main(string[] args)
{
IServiceCollection services = new ServiceCollection();
//*註冊DotNetCoreRpcClient核心服務
services.AddDotNetCoreRpcClient()
//*通訊是基於HTTP的,內部使用的HttpClientFactory,自行註冊即可
.AddHttpClient("WebDemo", client => { client.BaseAddress = new Uri("http://localhost:13285/"); });
IServiceProvider serviceProvider = services.BuildServiceProvider();
//*獲取RpcClient使用這個類建立具體服務代理物件
RpcClient rpcClient = serviceProvider.GetRequiredService<RpcClient>();
//IPersonService是我引入的服務包interface,需要提供ServiceName,即AddHttpClient的名稱
IPersonService personService = rpcClient.CreateClient<IPersonService>("WebDemo");
PersonDto personDto = new PersonDto
{
Id = 1,
Name = "yi念之間",
Address = "中國",
BirthDay = new DateTime(2000,12,12),
IsMarried = true,
Tel = 88888888888
};
bool addFlag = personService.Add(personDto);
Console.WriteLine($"新增結果=[{addFlag}]");
var person = personService.Get(personDto.Id);
Console.WriteLine($"獲取person結果=[{person.ToJson()}]");
var persons = personService.GetAll();
Console.WriteLine($"獲取persons結果=[{persons.ToList().ToJson()}]");
personService.Delete(person.Id);
Console.WriteLine($"刪除完成");
Console.ReadLine();
}
}
到這裡Client端的程式碼就編寫完成了
Server端配置
Client端引入DotNetCoreRpc.Client包,並引入自定義的契約介面層
<PackageReference Include="DotNetCoreRpc.Server" Version="1.0.2" />
然後編寫契約介面實現類,比如我的叫PersonService
//實現契約介面IPersonService
public class PersonService:IPersonService
{
private readonly ConcurrentDictionary<int, PersonDto> persons = new ConcurrentDictionary<int, PersonDto>();
public bool Add(PersonDto person)
{
return persons.TryAdd(person.Id, person);
}
public void Delete(int id)
{
persons.Remove(id,out PersonDto person);
}
//自定義Filter
[CacheFilter(CacheTime = 500)]
public PersonDto Get(int id)
{
return persons.GetValueOrDefault(id);
}
//自定義Filter
[CacheFilter(CacheTime = 300)]
public IEnumerable<PersonDto> GetAll()
{
foreach (var item in persons)
{
yield return item.Value;
}
}
}
通過上面的程式碼可以看出,我自定義了Filter,這裡的Filter並非Asp.Net Core框架定義的Filter,而是DotNetCoreRpc框架定義的Filter,自定義Filter的方式如下
//*要繼承自抽象類RpcFilterAttribute
public class CacheFilterAttribute: RpcFilterAttribute
{
public int CacheTime { get; set; }
//*支援屬性注入,可以是public或者private
//*這裡的FromServices並非Asp.Net Core名稱空間下的,而是來自DotNetCoreRpc.Core名稱空間
[FromServices]
private RedisConfigOptions RedisConfig { get; set; }
[FromServices]
public ILogger<CacheFilterAttribute> Logger { get; set; }
public override async Task InvokeAsync(RpcContext context, RpcRequestDelegate next)
{
Logger.LogInformation($"CacheFilterAttribute Begin,CacheTime=[{CacheTime}],Class=[{context.TargetType.FullName}],Method=[{context.Method.Name}],Params=[{JsonConvert.SerializeObject(context.Parameters)}]");
await next(context);
Logger.LogInformation($"CacheFilterAttribute End,ReturnValue=[{JsonConvert.SerializeObject(context.ReturnValue)}]");
}
}
以上程式碼基本上完成了對服務端業務程式碼的操作,接下來我們來看如何在Asp.Net Core中配置使用DotNetCoreRpc。開啟Startup,配置如下程式碼既可
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IPersonService, PersonService>()
.AddSingleton(new RedisConfigOptions { Address = "127.0.0.1:6379", Db = 10 })
//*註冊DotNetCoreRpcServer
.AddDotNetCoreRpcServer(options => {
//*確保新增的契約服務介面事先已經被註冊到DI容器中
//新增契約介面
//options.AddService<IPersonService>();
//或新增契約介面名稱以xxx為結尾的
//options.AddService("*Service");
//或新增具體名稱為xxx的契約介面
//options.AddService("IPersonService");
//或掃描具體名稱空間下的契約介面
options.AddNameSpace("IRpcService");
//可以新增全域性過濾器,實現方式和CacheFilterAttribute一致
options.AddFilter<LoggerFilterAttribute>();
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//這一堆可以不要+1
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//新增DotNetCoreRpc中介軟體既可
app.UseDotNetCoreRpc();
//這一堆可以不要+2
app.UseRouting();
//這一堆可以不要+3
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Server Start!");
});
});
}
}
以上就是Server端簡單的使用和配置,是不是感覺非常的Easy。附上可執行的Demo地址,具體編碼可檢視Demo.
總結
能自己實現一套RPC框架是我近期以來的一個願望,現在可以說實現了。雖然看起來沒這麼高大上,但是整體還是符合RPC的思想。主要還是想自身實地的實踐一下,順便也希望能給大家提供一些簡單的思路。不是說我說得一定是對的,我講得可能很多是不對的,但是我說的東西都是我自身的體驗和思考,也許能給你帶來一秒鐘、半秒鐘的思考,亦或是哪怕你覺得我哪一句話說的有點道理,能引發你內心的感觸,這就是我做這件事的意義。最後,歡迎大家評論區或本專案GitHub下批評指導。