Orleans 知多少 | 3. Hello Orleans

Orleans 3.0 Released

1. 引言

是的吹菱,Orleans v3.0.0 已經(jīng)發(fā)布了故河,并已經(jīng)完全支持 .NET Core 3.0吱韭。
所以,Orleans 系列是時(shí)候繼續(xù)了鱼的,抱歉理盆,讓大家久等了。
萬(wàn)丈高樓平地起凑阶,這一節(jié)我們就先來(lái)了解下Orleans的基本使用猿规。

2. 模板項(xiàng)目講解

Orleans 核心概念

在上一篇文章中,我們了解到Orleans 作為.NET 分布式框架宙橱,其主要包括三個(gè)部分:Client姨俩、Grains、Silo Host(Server)师郑。因此环葵,為了方便講解,創(chuàng)建如下的項(xiàng)目結(jié)構(gòu)進(jìn)行演示:


Hello.Orleans 項(xiàng)目結(jié)構(gòu)

這里有幾點(diǎn)需要說(shuō)明:

  1. Orleans.Grains: 類庫(kù)項(xiàng)目宝冕,用于定義Grain的接口以及實(shí)現(xiàn)积担,需要引用Microsoft.Orleans.CodeGenerator.MSBuildMicrosoft.Orleans.Core.Abstractions NuGet包。
  2. Orleans.Server:控制臺(tái)項(xiàng)目猬仁,為 Silo 宿主提供宿主環(huán)境帝璧,需要引用Microsoft.Orleans.ServerMicrosoft.Extensions.Hosting NuGet包阐虚,以及Orleans.Grains 項(xiàng)目称龙。
  3. Orleans.Client:控制臺(tái)項(xiàng)目,用于演示如何借助Orleans Client建立與Orleans Server的連接迟赃,需要引用Microsoft.Orleans.ClientMicrosoft.Extensions.Hosting NuGet包诈闺,同時(shí)添加Orleans.Grains項(xiàng)目引用渴庆。

3. 第一個(gè)Grain

Grain作為Orleans的第一公民,以及Virtual Actor的實(shí)際代言人,想吃透Orleans襟雷,那Grain就是第一道坎刃滓。
先看一個(gè)簡(jiǎn)單的Demo,我們來(lái)模擬統(tǒng)計(jì)網(wǎng)站的實(shí)時(shí)在線用戶耸弄。
Orlean s.Grains添加ISessionControl接口咧虎,主要用戶登錄狀態(tài)的管理。

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

可以看見(jiàn)Grain的定義很簡(jiǎn)單计呈,只需要指定繼承自IGrain的接口就好砰诵。這里面繼承自IGrainWithStringKey,說(shuō)明該Grain 的Identity Key(身份標(biāo)識(shí))為string類型捌显。同時(shí)需要注意的是
Grain 的方法申明茁彭,返回值必須是: Task、Task<T>扶歪、ValueTask<T>理肺。
緊接著定義SessionControlGrain來(lái)實(shí)現(xiàn)ISessionControlGrain接口。

public class SessionControlGrain : Grain, ISessionControlGrain
{
    private List<string> LoginUsers { get; set; } = new List<string>();

    public Task Login(string userId)
    {
        //獲取當(dāng)前Grain的身份標(biāo)識(shí)(因?yàn)镮SessionControlGrain身份標(biāo)識(shí)為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)
    {
        //獲取當(dāng)前Grain的身份標(biāo)識(shí)
        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);
    }
}

實(shí)現(xiàn)也很簡(jiǎn)單妹萨,Grain的實(shí)現(xiàn)要繼承自Grain基類。代碼中我們定義了一個(gè)List<string>集合用于保存登錄用戶媳禁。

4. 第一個(gè)Silo Host(Server)

定義一個(gè)Silo用于暴露Grain提供的服務(wù),在Orleans.Server.Program中添加以下代碼用于啟動(dòng)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():創(chuàng)建泛型主機(jī)提供宿主環(huán)境竣稽。
  2. UseOrleans:用來(lái)配置Oleans。
  3. UseLocalhostClustering() :用于在開(kāi)發(fā)環(huán)境下指定連接到本地集群霍弹。
  4. Configure<ClusterOptions>:用于指定連接到那個(gè)集群毫别。
  5. Configure<EndpointOptions>:用于配置silo與silo、silo與client之間的通信端點(diǎn)典格。開(kāi)發(fā)環(huán)境下可僅指定回環(huán)地址作為集群間通信的IP地址岛宦。
  6. ConfigureApplicationParts():用于指定暴露哪些Grain服務(wù)。

以上就是開(kāi)發(fā)環(huán)境下耍缴,Orleans Server的基本配置砾肺。對(duì)于詳細(xì)的配置也可以先參考Orleans Server Configuration。后續(xù)也會(huì)有專門(mén)的一篇文章來(lái)詳解防嗡。

5. 第一個(gè)Client

客戶端的定義也很簡(jiǎn)單变汪,主要是創(chuàng)建IClusterClient對(duì)象建立于Orleans Server的連接。因?yàn)?code>IClusterClient最好能在程序啟動(dòng)之時(shí)就建立連接蚁趁,所以可以通過(guò)繼承IHostedService來(lái)實(shí)現(xiàn)裙盾。
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. 構(gòu)造函數(shù)中通過(guò)借助ClientBuilder() 來(lái)初始化IClusterClient。其中UseLocalhostClustering()用于連接到開(kāi)發(fā)環(huán)境中的localhost 集群番官。并通過(guò)Configure<ClusterOptions>指定連接到哪個(gè)集群庐完。(需要注意的是,這里的ClusterId必須與Orleans.Server中配置的保持一致徘熔。
Client = new ClientBuilder()
    .UseLocalhostClustering()
    .Configure<ClusterOptions>(options =>
    {
        options.ClusterId = "Hello.Orleans";
        options.ServiceId = "Hello.Orleans";
    })
    .ConfigureLogging(builder => builder.AddProvider(loggerProvider))
    .Build();
  1. StartAsync方法中通過(guò)調(diào)用Client.Connect建立與Orleans Server的連接门躯。同時(shí)定義了一個(gè)重試機(jī)制。

緊接著我們需要將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();
}

對(duì)于ClusterClientHostedService生音,并沒(méi)有選擇直接通過(guò)services.AddHostedService<T>的方式注入,是因?yàn)槲覀冃枰⑷朐摲?wù)中提供的IClusterClient(單例)窒升,以供其他類去消費(fèi)缀遍。

緊接著,定義一個(gè)HelloOrleansClientHostedService用來(lái)消費(fèi)定義的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)
    {
        // 模擬控制臺(tái)終端用戶登錄
       await MockLogin("Hello.Orleans.Console");
       // 模擬網(wǎng)頁(yè)終端用戶登錄
       await MockLogin("Hello.Orleans.Web");
    }

    /// <summary>
    /// 模擬指定應(yīng)用的登錄
    /// </summary>
    /// <param name="appName"></param>
    /// <returns></returns>
    public async Task MockLogin(string appName)
    {
        //假設(shè)我們需要支持不同端登錄用戶域醇,則只需要將項(xiàng)目名稱作為身份標(biāo)識(shí)。
        //即可獲取一個(gè)代表用來(lái)維護(hù)當(dāng)前項(xiàng)目登錄狀態(tài)的的單例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 只是返回所有循環(huán)創(chuàng)建完畢譬挚,并不保證循環(huán)的內(nèi)部任務(wù)創(chuàng)建并執(zhí)行完畢
            //所以,此處手動(dòng)延遲5秒后再去讀取活動(dòng)用戶數(shù)酪呻。
            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; ;
    }
}

代碼講解:
這里定義了一個(gè)MockLogin用于模擬不同終端10000個(gè)用戶的并發(fā)登錄减宣。

  1. 通過(guò)構(gòu)造函數(shù)注入需要的IClusterClient
  2. 通過(guò)指定Grain接口以及身份標(biāo)識(shí)玩荠,就可以通過(guò)Client 獲取對(duì)應(yīng)的Grain漆腌,進(jìn)而消費(fèi)Grain中暴露的方法。var sessionControl = _client.GetGrain<ISessionControlGrain>(appName); 這里需要注意的是阶冈,指定的身份標(biāo)識(shí)為終端應(yīng)用的名稱闷尿,那么在整個(gè)應(yīng)用生命周期內(nèi),將有且僅有一個(gè)代表這個(gè)終端應(yīng)用的Grain女坑。
  3. 使用Parallel.For 模擬并發(fā)
  4. ParallelLoopResult.IsCompleted 只是返回所有循環(huán)任務(wù)創(chuàng)建完畢填具,并不代表循環(huán)的內(nèi)部任務(wù)執(zhí)行完畢。

6. 啟動(dòng)第一個(gè) Orleans 應(yīng)用

先啟動(dòng)Orleans.Server

Orleans Server Stared

再啟動(dòng)Orleans.Client
Orleans Client

Orleans Server log

從上面的運(yùn)行結(jié)果來(lái)看匆骗,模擬兩個(gè)終端10000個(gè)用戶的并發(fā)登錄劳景,最終輸出的活動(dòng)用戶數(shù)量均為10000個(gè)。
回顧整個(gè)實(shí)現(xiàn)碉就,并沒(méi)有用到諸如鎖枢泰、并發(fā)集合等避免并發(fā)導(dǎo)致的線程安全問(wèn)題,但卻輸出正確的期望結(jié)果铝噩,這就正好說(shuō)明了Orleans強(qiáng)大的并發(fā)控制特性衡蚂。

public class SessionControlGrain : Grain, ISessionControlGrain
{
    // 未使用并發(fā)集合
    private List<string> LoginUsers { get; set; } = new List<string>();

    public Task Login(string userId)
    {
        //獲取當(dāng)前Grain的身份標(biāo)識(shí)(因?yàn)镮SessionControlGrain身份標(biāo)識(shí)為string類型窿克,GetPrimaryKeyString());
        var appName = this.GetPrimaryKeyString();
        
        LoginUsers.Add(userId);//未加鎖

        Console.WriteLine($"Current active users count of {appName} is {LoginUsers.Count}");
        return Task.CompletedTask;
    }
    ....
}

7. 小結(jié)

通過(guò)簡(jiǎn)單的演示,想必你對(duì)Orleans的編程實(shí)現(xiàn)有了基本的認(rèn)知毛甲,并體會(huì)到其并發(fā)控制的強(qiáng)大之處年叮。
這只是簡(jiǎn)單的入門(mén)演練,Orleans很多強(qiáng)大的特性玻募,后續(xù)再結(jié)合具體場(chǎng)景進(jìn)行詳細(xì)闡述只损。
源碼已上傳至GitHub:Hello.Orleans

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市七咧,隨后出現(xiàn)的幾起案子跃惫,更是在濱河造成了極大的恐慌,老刑警劉巖艾栋,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爆存,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蝗砾,警方通過(guò)查閱死者的電腦和手機(jī)先较,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)悼粮,“玉大人闲勺,你說(shuō)我怎么就攤上這事】勖ǎ” “怎么了菜循?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)申尤。 經(jīng)常有香客問(wèn)我癌幕,道長(zhǎng),這世上最難降的妖魔是什么瀑凝? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任序芦,我火速辦了婚禮臭杰,結(jié)果婚禮上粤咪,老公的妹妹穿的比我還像新娘。我一直安慰自己渴杆,他們只是感情好寥枝,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著磁奖,像睡著了一般囊拜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上比搭,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天冠跷,我揣著相機(jī)與錄音,去河邊找鬼。 笑死蜜托,一個(gè)胖子當(dāng)著我的面吹牛抄囚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播橄务,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼幔托,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蜂挪?” 一聲冷哼從身側(cè)響起重挑,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棠涮,沒(méi)想到半個(gè)月后谬哀,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡故爵,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年玻粪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诬垂。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡劲室,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出结窘,到底是詐尸還是另有隱情很洋,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布隧枫,位于F島的核電站喉磁,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏官脓。R本人自食惡果不足惜协怒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卑笨。 院中可真熱鬧孕暇,春花似錦、人聲如沸赤兴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)桶良。三九已至座舍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間陨帆,已是汗流浹背曲秉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工采蚀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人承二。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓搏存,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親矢洲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子璧眠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容