Orleans 知多少 | 3. Hello Orleans

1. 引言

是的,Orleans v3.0.0 已經發布了,並已經徹底支持 .NET Core 3.0。因此,Orleans 系列是時候繼續了,抱歉,讓你們久等了。萬丈高樓平地起,這一節咱們就先來了解下Orleans的基本使用。編程

2. 模板項目講解

在上一篇文章中,咱們瞭解到Orleans 做爲.NET 分佈式框架,其主要包括三個部分:Client、Grains、Silo Host(Server)。所以,爲了方便講解,建立以下的項目結構進行演示:安全

這裏有幾點須要說明:微信

  1. Orleans.Grains:類庫項目,用於定義Grain的接口以及實現,須要引用 Microsoft.Orleans.CodeGenerator.MSBuild和 Microsoft.Orleans.Core.Abstractions NuGet包。session

  2. Orleans.Server:控制檯項目,爲 Silo 宿主提供宿主環境,須要引用 Microsoft.Orleans.Server 和 Microsoft.Extensions.Hosting NuGet包,以及 Orleans.Grains 項目。併發

  3. Orleans.Client:控制檯項目,用於演示如何藉助Orleans Client創建與Orleans Server的鏈接,須要引用 Microsoft.Orleans.Client 和 Microsoft.Extensions.Hosting NuGet包,同時添加 Orleans.Grains項目引用。app

3. 第一個Grain

Grain做爲Orleans的第一公民,以及Virtual Actor的實際代言人,想吃透Orleans,那Grain就是第一道坎。先看一個簡單的Demo,咱們來模擬統計網站的實時在線用戶。在 Orleans.Grains添加 ISessionControl接口,主要用戶登陸狀態的管理。框架

  
  
   
   
            
   
   
  1. async

  2. 分佈式

  3. ide

public interface ISessionControlGrain : IGrainWithStringKey{ Task Login(string userId); Task Logout(string userId); Task<int> GetActiveUserCount();}

能夠看見Grain的定義很簡單,只須要指定繼承自IGrain的接口就好。這裏面繼承自 IGrainWithStringKey,說明該Grain 的Identity Key(身份標識)爲 string類型。同時須要注意的是Grain 的方法申明,返回值必須是:Task、Task 、ValueTask 。緊接着定義 SessionControlGrain來實現 ISessionControlGrain接口。

  
  
   
   
            
   
   






public class SessionControlGrain : Grain, ISessionControlGrain{ private List<string> LoginUsers { get; set; } = new List<string>(); public Task Login(string userId) { //獲取當前Grain的身份標識(由於ISessionControlGrain身份標識爲string類型,GetPrimaryKeyString()); var appName = this.GetPrimaryKeyString(); LoginUsers.Add(userId); Console.WriteLine($"Current active users count of {appName} is {LoginUsers.Count}"); return Task.CompletedTask; } public Task Logout(string userId) { //獲取當前Grain的身份標識 var appName = this.GetPrimaryKey(); LoginUsers.Remove(userId); Console.WriteLine($"Current active users count of {appName} is {LoginUsers.Count}"); return Task.CompletedTask; } public Task<int> GetActiveUserCount() { return Task.FromResult(LoginUsers.Count); }}

實現也很簡單,Grain的實現要繼承自 Grain基類。代碼中咱們定義了一個 List<string>集合用於保存登陸用戶。

4. 第一個Silo Host(Server)

定義一個Silo用於暴露Grain提供的服務,在 Orleans.Server.Program中添加如下代碼用於啓動Silo Host。

  
  
   
   
            
   
   

static Task Main(string[] args){ Console.Title = typeof(Program).Namespace; // define the cluster configuration return Host.CreateDefaultBuilder() .UseOrleans((builder) => { builder.UseLocalhostClustering() .AddMemoryGrainStorageAsDefault() .Configure<ClusterOptions>(options => { options.ClusterId = "Hello.Orleans"; options.ServiceId = "Hello.Orleans"; }) .Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback) .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(ISessionControlGrain).Assembly).WithReferences()); } ) .ConfigureServices(services => { services.Configure<ConsoleLifetimeOptions>(options => { options.SuppressStatusMessages = true; }); }) .ConfigureLogging(builder => { builder.AddConsole(); }) .RunConsoleAsync();}
  1. Host.CreateDefaultBuilder():建立泛型主機提供宿主環境。

  2. UseOrleans:用來配置Oleans。

  3. UseLocalhostClustering() :用於在開發環境下指定鏈接到本地集羣。

  4. Configure<ClusterOptions>:用於指定鏈接到那個集羣。

  5. Configure<EndpointOptions>:用於配置silo與silo、silo與client之間的通訊端點。開發環境下可僅指定迴環地址做爲集羣間通訊的IP地址。

  6. ConfigureApplicationParts():用於指定暴露哪些Grain服務。

以上就是開發環境下,Orleans Server的基本配置。對於詳細的配置也能夠先參考Orleans Server Configuration。後續也會有專門的一篇文章來詳解。

5. 第一個Client

客戶端的定義也很簡單,主要是建立 IClusterClient對象創建於Orleans Server的鏈接。由於 IClusterClient最好能在程序啓動之時就創建鏈接,因此能夠經過繼承 IHostedService來實現。在 Orleans.Client中定義 ClusterClientHostedService繼承自 IHostedService

  
  
   
   
            
   
   








public class ClusterClientHostedService : IHostedService{ public IClusterClient Client { get; } private readonly ILogger<ClusterClientHostedService> _logger; public ClusterClientHostedService(ILogger<ClusterClientHostedService> logger, ILoggerProvider loggerProvider) { _logger = logger; Client = new ClientBuilder() .UseLocalhostClustering() .Configure<ClusterOptions>(options => { options.ClusterId = "Hello.Orleans"; options.ServiceId = "Hello.Orleans"; }) .ConfigureLogging(builder => builder.AddProvider(loggerProvider)) .Build(); } public Task StartAsync(CancellationToken cancellationToken) { var attempt = 0; var maxAttempts = 100; var delay = TimeSpan.FromSeconds(1); return Client.Connect(async error => { if (cancellationToken.IsCancellationRequested) { return false; } if (++attempt < maxAttempts) { _logger.LogWarning(error, "Failed to connect to Orleans cluster on attempt {@Attempt} of {@MaxAttempts}.", attempt, maxAttempts); try { await Task.Delay(delay, cancellationToken); } catch (OperationCanceledException) { return false; } return true; } else { _logger.LogError(error, "Failed to connect to Orleans cluster on attempt {@Attempt} of {@MaxAttempts}.", attempt, maxAttempts); return false; } }); } public async Task StopAsync(CancellationToken cancellationToken) { try { await Client.Close(); } catch (OrleansException error) { _logger.LogWarning(error, "Error while gracefully disconnecting from Orleans cluster. Will ignore and continue to shutdown."); } }}

代碼講解:

1.構造函數中經過藉助 ClientBuilder() 來初始化 IClusterClient。其中 UseLocalhostClustering()用於鏈接到開發環境中的localhost 集羣。並經過 Configure<ClusterOptions>指定鏈接到哪一個集羣。(須要注意的是,這裏的ClusterId必須與Orleans.Server中配置的保持一致。

  
  
   
   
            
   
   
Client = new ClientBuilder() .UseLocalhostClustering() .Configure<ClusterOptions>(options => { options.ClusterId = "Hello.Orleans"; options.ServiceId = "Hello.Orleans"; }) .ConfigureLogging(builder => builder.AddProvider(loggerProvider)) .Build();

2. 在 StartAsync方法中經過調用 Client.Connect創建與Orleans Server的鏈接。同時定義了一個重試機制。

緊接着咱們須要將 ClusterClientHostedService添加到Ioc容器,添加如下代碼到 Orleans.Client.Program中:

  
  
   
   
            
   
   


static Task Main(string[] args){ Console.Title = typeof(Program).Namespace; return Host.CreateDefaultBuilder() .ConfigureServices(services => { services.AddSingleton<ClusterClientHostedService>(); services.AddSingleton<IHostedService>(_ => _.GetService<ClusterClientHostedService>()); services.AddSingleton(_ => _.GetService<ClusterClientHostedService>().Client); services.AddHostedService<HelloOrleansClientHostedService>(); services.Configure<ConsoleLifetimeOptions>(options => { options.SuppressStatusMessages = true; }); }) .ConfigureLogging(builder => { builder.AddConsole(); }) .RunConsoleAsync();}

對於 ClusterClientHostedService,並無選擇直接經過 services.AddHostedService<T>的方式注入,是由於咱們須要注入該服務中提供的 IClusterClient(單例),以供其餘類去消費。

緊接着,定義一個 HelloOrleansClientHostedService用來消費定義的 ISessionControlGrain

  
  
   
   
            
   
   






public class HelloOrleansClientHostedService : IHostedService{ private readonly IClusterClient _client; private readonly ILogger<HelloOrleansClientHostedService> _logger; public HelloOrleansClientHostedService(IClusterClient client, ILogger<HelloOrleansClientHostedService> logger) { _client = client; _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { // 模擬控制檯終端用戶登陸 await MockLogin("Hello.Orleans.Console"); // 模擬網頁終端用戶登陸 await MockLogin("Hello.Orleans.Web"); } /// <summary> /// 模擬指定應用的登陸 /// </summary> /// <param name="appName"></param> /// <returns></returns> public async Task MockLogin(string appName) { //假設咱們須要支持不一樣端登陸用戶,則只須要將項目名稱做爲身份標識。 //便可獲取一個表明用來維護當前項目登陸狀態的的單例Grain。 var sessionControl = _client.GetGrain<ISessionControlGrain>(appName); ParallelLoopResult result = Parallel.For(0, 10000, (index) => { var userId = $"User-{index}"; sessionControl.Login(userId); }); if (result.IsCompleted) { //ParallelLoopResult.IsCompleted 只是返回全部循環建立完畢,並不保證循環的內部任務建立並執行完畢 //因此,此處手動延遲5秒後再去讀取活動用戶數。 await Task.Delay(TimeSpan.FromSeconds(5)); var activeUserCount = await sessionControl.GetActiveUserCount(); _logger.LogInformation($"The Active Users Count of {appName} is {activeUserCount}"); } } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Closed!"); return Task.CompletedTask; ; }}

代碼講解:這裏定義了一個 MockLogin用於模擬不一樣終端10000個用戶的併發登陸。

  1. 經過構造函數注入須要的 IClusterClient

  2. 經過指定Grain接口以及身份標識,就能夠經過Client 獲取對應的Grain,進而消費Grain中暴露的方法。 varsessionControl=_client.GetGrain<ISessionControlGrain>(appName); 這裏須要注意的是,指定的身份標識爲終端應用的名稱,那麼在整個應用生命週期內,將有且僅有一個表明這個終端應用的Grain。

  3. 使用 Parallel.For 模擬併發

  4. ParallelLoopResult.IsCompleted 只是返回全部循環任務建立完畢,並不表明循環的內部任務執行完畢。

6. 啓動第一個 Orleans 應用

先啓動 Orleans.Server再啓動 Orleans.Client

從上面的運行結果來看,模擬兩個終端10000個用戶的併發登陸,最終輸出的活動用戶數量均爲10000個。回顧整個實現,並無用到諸如鎖、併發集合等避免併發致使的線程安全問題,但卻輸出正確的指望結果,這就正好說明了Orleans強大的併發控制特性。

  
  
   
   
            
   
   



public class SessionControlGrain : Grain, ISessionControlGrain{ // 未使用併發集合 private List<string> LoginUsers { get; set; } = new List<string>(); public Task Login(string userId) { //獲取當前Grain的身份標識(由於ISessionControlGrain身份標識爲string類型,GetPrimaryKeyString()); var appName = this.GetPrimaryKeyString(); LoginUsers.Add(userId);//未加鎖 Console.WriteLine($"Current active users count of {appName} is {LoginUsers.Count}"); return Task.CompletedTask; } ....}

7. 小結

經過簡單的演示,想必你對Orleans的編程實現有了基本的認知,並體會到其併發控制的強大之處。這只是簡單的入門演練,Orleans不少強大的特性,後續再結合具體場景進行詳細闡述。源碼已上傳至GitHub:Hello.Orleans


本文分享自微信公衆號 - 微服務知多少(dotnet-microservice)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索