<span id="mktg5"></span>

<i id="mktg5"><meter id="mktg5"></meter></i>

        <label id="mktg5"><meter id="mktg5"></meter></label>
        最新文章專題視頻專題問答1問答10問答100問答1000問答2000關(guān)鍵字專題1關(guān)鍵字專題50關(guān)鍵字專題500關(guān)鍵字專題1500TAG最新視頻文章推薦1 推薦3 推薦5 推薦7 推薦9 推薦11 推薦13 推薦15 推薦17 推薦19 推薦21 推薦23 推薦25 推薦27 推薦29 推薦31 推薦33 推薦35 推薦37視頻文章20視頻文章30視頻文章40視頻文章50視頻文章60 視頻文章70視頻文章80視頻文章90視頻文章100視頻文章120視頻文章140 視頻2關(guān)鍵字專題關(guān)鍵字專題tag2tag3文章專題文章專題2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章專題3
        問答文章1 問答文章501 問答文章1001 問答文章1501 問答文章2001 問答文章2501 問答文章3001 問答文章3501 問答文章4001 問答文章4501 問答文章5001 問答文章5501 問答文章6001 問答文章6501 問答文章7001 問答文章7501 問答文章8001 問答文章8501 問答文章9001 問答文章9501
        當前位置: 首頁 - 科技 - 知識百科 - 正文

        .NET Core基于Generic Host實現(xiàn)后臺任務(wù)方法教程

        來源:懂視網(wǎng) 責編:小采 時間:2020-11-27 22:34:46
        文檔

        .NET Core基于Generic Host實現(xiàn)后臺任務(wù)方法教程

        .NET Core基于Generic Host實現(xiàn)后臺任務(wù)方法教程:前言 很多時候,后臺任務(wù)對我們來說是一個利器,幫我們在后面處理了成千上萬的事情。 在.NET Framework時代,我們可能比較多的就是一個項目,會有一到多個對應(yīng)的Windows服務(wù),這些Windows服務(wù)就可以當作是我們所說的后臺任務(wù)了。 我喜歡將后臺任務(wù)分為兩大類
        推薦度:
        導讀.NET Core基于Generic Host實現(xiàn)后臺任務(wù)方法教程:前言 很多時候,后臺任務(wù)對我們來說是一個利器,幫我們在后面處理了成千上萬的事情。 在.NET Framework時代,我們可能比較多的就是一個項目,會有一到多個對應(yīng)的Windows服務(wù),這些Windows服務(wù)就可以當作是我們所說的后臺任務(wù)了。 我喜歡將后臺任務(wù)分為兩大類

        前言

        很多時候,后臺任務(wù)對我們來說是一個利器,幫我們在后面處理了成千上萬的事情。

        在.NET Framework時代,我們可能比較多的就是一個項目,會有一到多個對應(yīng)的Windows服務(wù),這些Windows服務(wù)就可以當作是我們所說的后臺任務(wù)了。

        我喜歡將后臺任務(wù)分為兩大類,一類是不停的跑,好比MQ的消費者,RPC的服務(wù)端。另一類是定時的跑,好比定時任務(wù)。

        那么在.NET Core時代是不是有一些不同的解決方案呢?答案是肯定的。

        Generic Host就是其中一種方案,也是本文的主角。

        什么是Generic Host

        Generic Host是ASP.NET Core 2.1中的新增功能,它的目的是將HTTP管道從Web Host的API中分離出來,從而啟用更多的Host方案。

        現(xiàn)在2.1版本的Asp.Net Core中,有了兩種可用的Host。

        Web Host –適用于托管Web程序的Host,就是我們所熟悉的在Asp.Net Core應(yīng)用程序的Mai函數(shù)中用CreateWebHostBuilder創(chuàng)建出來的常用的WebHost。

        Generic Host (ASP.NET Core 2.1版本才有) – 適用于托管非 Web 應(yīng)用(例如,運行后臺任務(wù)的應(yīng)用)。 在未來的版本中,通用主機將適用于托管任何類型的應(yīng)用,包括 Web 應(yīng)用。 通用主機最終將取代 Web 主機,這大概也是這種類型的主機叫做通用主機的原因。

        這樣可以讓基于Generic Host的一些特性延用一些基礎(chǔ)的功能。如:如配置、依賴關(guān)系注入和日志等。

        Generic Host更傾向于通用性,換句話就是說,我們即可以在Web項目中使用,也可以在非Web項目中使用!

        雖然有時候后臺任務(wù)混雜在Web項目中并不是一個太好的選擇,但也并不失是一個解決方案。尤其是在資源并不充足的時候。

        比較好的做法還是讓其獨立出來,讓它的職責更加單一。

        下面就先來看看如何創(chuàng)建后臺任務(wù)吧。

        后臺任務(wù)示例

        我們先來寫兩個后臺任務(wù)(一個一直跑,一個定時跑),體驗一下這些后臺任務(wù)要怎么上手,同樣也是我們后面要使用到的。

        這兩個任務(wù)統(tǒng)一繼承BackgroundService這個抽象類,而不是IHostedService這個接口。后面會說到兩者的區(qū)別。

        1、一直跑的后臺任務(wù)

        先上代碼

        public class PrinterHostedService2 : BackgroundService
        {
         private readonly ILogger _logger;
         private readonly AppSettings _settings;
        
         public PrinterHostedService2(ILoggerFactory loggerFactory, IOptionsSnapshot<AppSettings> options)
         {
         this._logger = loggerFactory.CreateLogger<PrinterHostedService2>();
         this._settings = options.Value;
         }
        
         public override Task StopAsync(CancellationToken cancellationToken)
         {
         _logger.LogInformation("Printer2 is stopped");
         return Task.CompletedTask;
         }
        
         protected override async Task ExecuteAsync(CancellationToken stoppingToken)
         {
         while (!stoppingToken.IsCancellationRequested)
         {
         _logger.LogInformation($"Printer2 is working. {_settings.PrinterDelaySecond}");
         await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond), stoppingToken);
         }
         }
        }

        來看看里面的細節(jié)。

        我們的這個服務(wù)繼承了BackgroundService,就一定要實現(xiàn)里面的ExecuteAsync,至于StartAsync和StopAsync等方法可以選擇性的override。

        我們ExecuteAsync在里面就是輸出了一下日志,然后休眠在配置文件中指定的秒數(shù)。

        這個任務(wù)可以說是最簡單的例子了,其中還用到了依賴注入,如果想在任務(wù)中注入數(shù)據(jù)倉儲之類的,應(yīng)該就不需要再多說了。

        同樣的方式再寫一個定時的。

        定時跑的后臺任務(wù)

        這里借助了Timer來完成定時跑的功能,同樣的還可以結(jié)合Quartz來完成。

        public class TimerHostedService : BackgroundService
        {
         //other ...
         
         private Timer _timer;
        
         protected override Task ExecuteAsync(CancellationToken stoppingToken)
         {
         _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_settings.TimerPeriod));
         return Task.CompletedTask;
         }
        
         private void DoWork(object state)
         {
         _logger.LogInformation("Timer is working");
         }
        
         public override Task StopAsync(CancellationToken cancellationToken)
         {
         _logger.LogInformation("Timer is stopping");
         _timer?.Change(Timeout.Infinite, 0);
         return base.StopAsync(cancellationToken);
         }
        
         public override void Dispose()
         {
         _timer?.Dispose();
         base.Dispose();
         }
        }

        和第一個后臺任務(wù)相比,沒有太大的差異。

        下面我們先來看看如何用控制臺的形式來啟動這兩個任務(wù)。

        控制臺形式

        這里會同時引入NLog來記錄任務(wù)跑的日志,方便我們觀察。

        Main函數(shù)的代碼如下:

        class Program
        {
         static async Task Main(string[] args)
         {
         var builder = new HostBuilder()
         //logging
         .ConfigureLogging(factory =>
         {
         //use nlog
         factory.AddNLog(new NLogProviderOptions { CaptureMessageTemplates = true, CaptureMessageProperties = true });
         NLog.LogManager.LoadConfiguration("nlog.config");
         })
         //host config
         .ConfigureHostConfiguration(config =>
         {
         //command line
         if (args != null)
         {
         config.AddCommandLine(args);
         }
         })
         //app config
         .ConfigureAppConfiguration((hostContext, config) =>
         {
         var env = hostContext.HostingEnvironment;
         config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
         .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
        
         config.AddEnvironmentVariables();
        
         if (args != null)
         {
         config.AddCommandLine(args);
         }
         })
         //service
         .ConfigureServices((hostContext, services) =>
         {
         services.AddOptions();
         services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));
        
         //basic usage
         services.AddHostedService<PrinterHostedService2>();
         services.AddHostedService<TimerHostedService>();
         }) ;
        
         //console 
         await builder.RunConsoleAsync();
        
         ////start and wait for shutdown
         //var host = builder.Build();
         //using (host)
         //{
         // await host.StartAsync();
        
         // await host.WaitForShutdownAsync();
         //}
         }
        }

        對于控制臺的方式,需要我們對HostBuilder有一定的了解,雖說它和WebHostBuild有相似的地方。可能大部分時候,我們是直接使用了WebHost.CreateDefaultBuilder(args)來構(gòu)造的,如果對CreateDefaultBuilder里面的內(nèi)容沒有了解,那么對上面的代碼可能就不會太清晰。

        上述代碼的大致流程如下:

      1. new一個HostBuilder對象
      2. 配置日志,主要是接入了NLog
      3. Host的配置,這里主要是引入了CommandLine,因為需要傳遞參數(shù)給程序
      4. 應(yīng)用的配置,指定了配置文件,和引入CommandLine
      5. Service的配置,這個就和我們在Startup里面寫的差不多了,最主要的是我們的后臺服務(wù)要在這里注入
      6. 啟動
      7. 其中,

        2-5的順序可以按個人習慣來寫,里面的內(nèi)容也和我們寫Startup大同小異。

        第6步,啟動的時候,有多種方式,這里列出了兩種行為等價的方式。

        a. 通過RunConsoleAsync的方式來啟動

        b. 先StartAsync然后再WaitForShutdownAsync

        RunConsoleAsync的奧秘,我覺得還是直接看下面的代碼比較容易懂。

        /// <summary>
        /// Listens for Ctrl+C or SIGTERM and calls <see cref="IApplicationLifetime.StopApplication"/> to start the shutdown process.
        /// This will unblock extensions like RunAsync and WaitForShutdownAsync.
        /// </summary>
        /// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
        /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
        public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
        {
         return hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton<IHostLifetime, ConsoleLifetime>());
        }
        
        /// <summary>
        /// Enables console support, builds and starts the host, and waits for Ctrl+C or SIGTERM to shut down.
        /// </summary>
        /// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
        {
         return hostBuilder.UseConsoleLifetime().Build().RunAsync(cancellationToken);
        }

        這里涉及到了一個比較重要的IHostLifetime,Host的生命周期,ConsoleLifeTime是默認的一個,可以理解成當接收到ctrl+c這樣的指令時,它就會觸發(fā)停止。

        接下來,寫一下nlog的配置文件

        <?xml version="1.0" encoding="utf-8" ?>
        <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         autoReload="true"
         internalLogLevel="Info" >
        
         <targets>
         <target xsi:type="File"
         name="ghost"
         fileName="logs/ghost.log"
         layout="${date}|${level:uppercase=true}|${message}" />
         </targets>
        
         <rules>
         <logger name="GHost.*" minlevel="Info" writeTo="ghost" />
         <logger name="Microsoft.*" minlevel="Info" writeTo="ghost" />
         </rules>
        </nlog>

        這個時候已經(jīng)可以通過命令啟動我們的應(yīng)用了。

        dotnet run -- --environment Staging

        這里指定了運行環(huán)境為Staging,而不是默認的Production。

        在構(gòu)造HostBuilder的時候,可以通過UseEnvironment或ConfigureHostConfiguration直接指定運行環(huán)境,但是個人更加傾向于在啟動命令中去指定,避免一些不可控因素。

        這個時候大致效果如下:

        雖然效果已經(jīng)出來了,不過大家可能會覺得這個有點小打小鬧,下面來個略微復雜一點的后臺任務(wù),用來監(jiān)聽并消費RabbitMQ的消息。

        消費MQ消息的后臺任務(wù)

        public class ComsumeRabbitMQHostedService : BackgroundService
        {
         private readonly ILogger _logger;
         private readonly AppSettings _settings;
         private IConnection _connection;
         private IModel _channel;
        
         public ComsumeRabbitMQHostedService(ILoggerFactory loggerFactory, IOptionsSnapshot<AppSettings> options)
         {
         this._logger = loggerFactory.CreateLogger<ComsumeRabbitMQHostedService>();
         this._settings = options.Value;
         InitRabbitMQ(this._settings);
         }
        
         private void InitRabbitMQ(AppSettings settings)
         {
         var factory = new ConnectionFactory { HostName = settings.HostName, };
         _connection = factory.CreateConnection();
         _channel = _connection.CreateModel();
        
         _channel.ExchangeDeclare(_settings.ExchangeName, ExchangeType.Topic);
         _channel.QueueDeclare(_settings.QueueName, false, false, false, null);
         _channel.QueueBind(_settings.QueueName, _settings.ExchangeName, _settings.RoutingKey, null);
         _channel.BasicQos(0, 1, false);
        
         _connection.ConnectionShutdown += RabbitMQ_ConnectionShutdown;
         }
        
         protected override Task ExecuteAsync(CancellationToken stoppingToken)
         {
         stoppingToken.ThrowIfCancellationRequested();
        
         var consumer = new EventingBasicConsumer(_channel);
         consumer.Received += (ch, ea) =>
         {
         var content = System.Text.Encoding.UTF8.GetString(ea.Body);
         HandleMessage(content);
         _channel.BasicAck(ea.DeliveryTag, false);
         };
        
         consumer.Shutdown += OnConsumerShutdown;
         consumer.Registered += OnConsumerRegistered;
         consumer.Unregistered += OnConsumerUnregistered;
         consumer.ConsumerCancelled += OnConsumerConsumerCancelled;
        
         _channel.BasicConsume(_settings.QueueName, false, consumer);
         return Task.CompletedTask;
         }
        
         private void HandleMessage(string content)
         {
         _logger.LogInformation($"consumer received {content}");
         }
         
         private void OnConsumerConsumerCancelled(object sender, ConsumerEventArgs e) { ... }
         private void OnConsumerUnregistered(object sender, ConsumerEventArgs e) { ... }
         private void OnConsumerRegistered(object sender, ConsumerEventArgs e) { ... }
         private void OnConsumerShutdown(object sender, ShutdownEventArgs e) { ... }
         private void RabbitMQ_ConnectionShutdown(object sender, ShutdownEventArgs e) { ... }
        
         public override void Dispose()
         {
         _channel.Close();
         _connection.Close();
         base.Dispose();
         }
        }

        代碼細節(jié)就不需要多說了,下面就啟動MQ發(fā)送程序來模擬消息的發(fā)送

        同時看我們?nèi)蝿?wù)的日志輸出

        由啟動到停止,效果都是符合我們預(yù)期的。

        下面再來看看Web形式的后臺任務(wù)是怎么處理的。

        Web形式

        這種模式下的后臺任務(wù),其實就是十分簡單的了。

        我們只要在Startup的ConfigureServices方法里面注冊我們的幾個后臺任務(wù)就可以了。

        public void ConfigureServices(IServiceCollection services)
        {
         services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
         services.AddHostedService<PrinterHostedService2>();
         services.AddHostedService<TimerHostedService>();
         services.AddHostedService<ComsumeRabbitMQHostedService>();
        }

        啟動Web站點后,我們發(fā)了20條MQ消息,再訪問了一下Web站點的首頁,最后是停止站點。

        下面是日志結(jié)果,都是符合我們的預(yù)期。

        可能大家會比較好奇,這三個后臺任務(wù)是怎么混合在Web項目里面啟動的。

        答案就在下面的兩個鏈接里。

        https://github.com/aspnet/Hosting/blob/2.1.1/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs

        https://github.com/aspnet/Hosting/blob/2.1.1/src/Microsoft.AspNetCore.Hosting/Internal/HostedServiceExecutor.cs

        上面說了那么多,都是在本地直接運行的,可能大家會比較關(guān)注這個要怎樣部署,下面我們就不看看怎么部署。

        部署

        部署的話,針對不同的情形(web和非web)都有不同的選擇。

        正常來說,如果本身就是web程序,那么平時我們怎么部署的,就和平時那樣部署即可。

        花點時間講講部署非web的情形。

        其實這里的部署等價于讓程序在后臺運行。

        在Linux下面讓程序在后臺運行方式有好多好多,Supervisor、Screen、pm2、systemctl等。

        這里主要介紹一下systemctl,同時用上面的例子來進行部署,由于個人服務(wù)器沒有MQ環(huán)境,所以沒有啟用消費MQ的后臺任務(wù)。

        先創(chuàng)建一個 service 文件

        vim /etc/systemd/system/ghostdemo.service

        內(nèi)容如下:

        [Unit]
        Description=Generic Host Demo
        
        [Service]
        WorkingDirectory=/var/www/ghost
        ExecStart=/usr/bin/dotnet /var/www/ghost/ConsoleGHost.dll --environment Staging
        KillSignal=SIGINT
        SyslogIdentifier=ghost-example
        
        [Install]
        WantedBy=multi-user.target

        其中,各項配置的含義可以自行查找,這里不作說明。

        然后可以通過下面的命令來啟動和停止這個服務(wù)

        service ghostdemo start
        service ghostdemo stop 

        測試無誤之后,就可以設(shè)為自啟動了。

        systemctl enable ghostdemo.service

        下面來看看運行的效果

        我們先啟動服務(wù),然后去查看實時日志,可以看到應(yīng)用的日志不停的輸出。

        當我們停了服務(wù),再看實時日志,就會發(fā)現(xiàn)我們的兩個后臺任務(wù)已經(jīng)停止了,也沒有日志再進來了。

        再去看看服務(wù)系統(tǒng)日志

        sudo journalctl -fu ghostdemo.service

        發(fā)現(xiàn)它確實也是停了。

        在這里,我們還可以看到服務(wù)的當前環(huán)境和根路徑。

        IHostedService和BackgroundService的區(qū)別

        前面的所有示例中,我們用的都是BackgroundService,而不是IHostedService。

        這兩者有什么區(qū)別呢?

        可以這樣簡單的理解,IHostedService是原料,BackgroundService是一個用原料加工過一部分的半成品。

        這兩個都是不能直接當成成品來用的,都需要進行加工才能做成一個可用的成品。

        同時也意味著,如果使用IHostedService可能會需要做比較多的控制。

        基于前面的打印后臺任務(wù),在這里使用IHostedService來實現(xiàn)。

        如果我們只是純綷的把實現(xiàn)代碼放到StartAsync方法中,那么可能就會有驚喜了。

        public class PrinterHostedService : IHostedService, IDisposable
        {
         //other ....
         
         public async Task StartAsync(CancellationToken cancellationToken)
         {
         while (!cancellationToken.IsCancellationRequested)
         {
         Console.WriteLine("Printer is working.");
         await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond), cancellationToken);
         }
         }
        
         public Task StopAsync(CancellationToken cancellationToken)
         {
         Console.WriteLine("Printer is stopped");
         return Task.CompletedTask;
         }
        } 

        運行之后,想用ctrl+c來停止,發(fā)現(xiàn)還是一直在跑。

        ps一看,這個進程還在,kill掉之后才不會繼續(xù)輸出。

        問題出在那里呢?原因其實還是比較明顯的,因為這個任務(wù)還沒有啟動成功,一直處于啟動中的狀態(tài)!

        換句話說,StartAsync方法還沒有執(zhí)行完。這個問題一定要小心再小心。

        要怎么處理這個問題呢?解決方法也比較簡單,可以通過引用一個變量來記錄要運行的任務(wù),將其從StartAsync方法中解放出來。

        public class PrinterHostedService3 : IHostedService, IDisposable
        {
         //others .....
         private bool _stopping;
         private Task _backgroundTask;
        
         public Task StartAsync(CancellationToken cancellationToken)
         {
         Console.WriteLine("Printer3 is starting.");
         _backgroundTask = BackgroundTask(cancellationToken);
         return Task.CompletedTask;
         }
        
         private async Task BackgroundTask(CancellationToken cancellationToken)
         {
         while (!_stopping)
         {
         await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond),cancellationToken);
         Console.WriteLine("Printer3 is doing background work.");
         }
         }
        
         public Task StopAsync(CancellationToken cancellationToken)
         {
         Console.WriteLine("Printer3 is stopping.");
         _stopping = true;
         return Task.CompletedTask;
         }
        
         public void Dispose()
         {
         Console.WriteLine("Printer3 is disposing.");
         }
        }

        這樣就能讓這個任務(wù)真正的啟動成功了!效果就不放圖了。

        相對來說,BackgroundService用起來會比較簡單,實現(xiàn)核心的ExecuteAsync這個抽象方法就差不多了,出錯的概率也會比較低。

        IHostBuilder的擴展寫法

        在注冊服務(wù)的時候,我們還可以通過編寫IHostBuilder的擴展方法來完成。

        public static class Extensions
        {
         public static IHostBuilder UseHostedService<T>(this IHostBuilder hostBuilder)
         where T : class, IHostedService, IDisposable
         {
         return hostBuilder.ConfigureServices(services =>
         services.AddHostedService<T>());
         }
        
         public static IHostBuilder UseComsumeRabbitMQ(this IHostBuilder hostBuilder)
         {
         return hostBuilder.ConfigureServices(services =>
         services.AddHostedService<ComsumeRabbitMQHostedService>());
         }
        }

        使用的時候就可以像下面一樣。

        var builder = new HostBuilder()
         //others ...
         .ConfigureServices((hostContext, services) =>
         {
         services.AddOptions();
         services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));
        
         //basic usage
         //services.AddHostedService<PrinterHostedService2>();
         //services.AddHostedService<TimerHostedService>();
         //services.AddHostedService<ComsumeRabbitMQHostedService>();
         })
         //extensions usage
         .UseComsumeRabbitMQ()
         .UseHostedService<TimerHostedService>()
         .UseHostedService<PrinterHostedService2>()
         //.UseHostedService<ComsumeRabbitMQHostedService>()
         ;

        總結(jié)

        Generic Host讓我們可以用熟悉的方式來處理后臺任務(wù),不得不說這是一個很👍的特性。

        無論是將后臺任務(wù)獨立一個項目,還是將其混搭在Web項目中,都已經(jīng)符合不少應(yīng)用的情景了。

        最后放上本文用到的示例代碼

        GenericHostDemo

        好了,

        聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com

        文檔

        .NET Core基于Generic Host實現(xiàn)后臺任務(wù)方法教程

        .NET Core基于Generic Host實現(xiàn)后臺任務(wù)方法教程:前言 很多時候,后臺任務(wù)對我們來說是一個利器,幫我們在后面處理了成千上萬的事情。 在.NET Framework時代,我們可能比較多的就是一個項目,會有一到多個對應(yīng)的Windows服務(wù),這些Windows服務(wù)就可以當作是我們所說的后臺任務(wù)了。 我喜歡將后臺任務(wù)分為兩大類
        推薦度:
        標簽: net host core
        • 熱門焦點

        最新推薦

        猜你喜歡

        熱門推薦

        專題
        Top
        主站蜘蛛池模板: 精品国产污污免费网站aⅴ| a在线观看免费视频| 免费无码黄十八禁网站在线观看| 亚洲精品中文字幕无码A片老| 91av在线免费视频| 在线观看亚洲人成网站| 99re在线视频免费观看| 亚洲欧洲日韩在线电影| 五月婷婷综合免费| 亚洲精品无码久久久久秋霞| 久久久久久国产精品免费免费 | 无码人妻一区二区三区免费| 亚洲一级毛片视频| 成人午夜性A级毛片免费| 亚洲午夜无码久久| 免费一级毛片不卡不收费| 日本高清不卡中文字幕免费| 91精品视频免费| 亚洲小说图区综合在线| 亚洲国产精品自产在线播放| 岛国精品一区免费视频在线观看| 亚洲嫩草影院久久精品| 日韩精品无码区免费专区| 亚洲精品无播放器在线播放 | 亚洲国产精品成人| 叮咚影视在线观看免费完整版| 亚洲国产综合专区在线电影 | 亚洲综合另类小说色区色噜噜| 亚洲国产成人久久| 四虎免费久久影院| 久久国产精品成人免费| 亚洲人成77777在线播放网站不卡 亚洲人成77777在线观看网 | 不卡视频免费在线观看| 亚洲国产精品人久久电影| 国产成人免费一区二区三区| aa级毛片毛片免费观看久| 亚洲国产成人久久综合一区77 | 中文字幕在线观看免费| 亚洲国产成人精品无码一区二区| 手机看片久久国产免费| 中文字幕无码一区二区免费|