• 自己动手实现一个简化版的Host
  • 发布于 2个月前
  • 224 热度
    0 评论
  • 旧巷
  • 0 粉丝 23 篇博客
  •   
前言
现在的 Host 变得逐渐复杂,添加新功能的同时还要保证兼容性导致复杂度的增加,而且一些新的功能没有办法在低版本中使用,所以想动手自己写一个 Host,名字叫做 AppHost 避免和 .NET 框架里的 Host 冲突。

例子
后台服务使用示例:
var builder = AppHost.CreateBuilder();
builder.Configuration.AddJsonFile("appsettings.json");
builder.Logging.AddRelaxedJsonConsole(options =>
{
    options.TimestampFormat = "yyyy-MM-dd HH:mm:ss";
});
builder.AddHostedService<TimerService>();
var cts = new CancellationTokenSource(10_000);
var app = builder.Build();
await app.RunAsync(cts.Token);
TimerService 实现如下:
file sealed class TimerService : TimerBaseBackgroundService
{
    protected override TimeSpan Period => TimeSpan.FromSeconds(1);
    protected override Task TimedTask(CancellationToken cancellationToken)
    {
        Console.WriteLine(DateTimeOffset.Now);
        return Task.CompletedTask;
    }
}
TimerService 继承于 TimerBaseBackgroundService 实现如下,它是一个 BackgroundService,可以认为就是 .NET 框架里的 BackgroundService
public abstract class TimerBaseBackgroundService : BackgroundService
{
    protected abstract TimeSpan Period { get; }
    protected abstract Task TimedTask(CancellationToken cancellationToken);
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(Period);
        while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
        {
            await TimedTask(stoppingToken).ConfigureAwait(false);
        }
    }
}
跑下上面的示例,输出结果如下:

WebServer例子
ASP.NET Core 从 3.0 开始也是一个 background service,我们也可以模拟实现一个 web server,我们基于 HttpListener 实现一个 Hello world 版本
实现如下:
file interface IWebServer
{
    Task StartAsync(CancellationToken cancellationToken);

    Task StopAsync(CancellationToken cancellationToken);
}

file sealed class HttpListenerWebServer : IWebServer
{
    private readonly IServiceProvider _serviceProvider;
    private readonly HttpListener _listener = new();

    public HttpListenerWebServer(IServiceProvider serviceProvider)
    {
      // 堆代码 duidaima.com 
        _serviceProvider = serviceProvider;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _listener.Prefixes.Add("http://localhost:5100/");
        _listener.Start();
        var logger = _serviceProvider.GetRequiredService<ILogger<HttpListenerWebServer>>();
        logger.LogInformation("WebServer started");
        
        while (!cancellationToken.IsCancellationRequested)
        {
            var listenerContext = await _listener.GetContextAsync();
            try
            {
                await listenerContext.Response.OutputStream.WriteAsync("Hello World"u8.ToArray(), cancellationToken);
            }
            catch (Exception) when (!cancellationToken.IsCancellationRequested)
            {
                throw;
            }
            finally
            {
                listenerContext.Response.Close();
            }
        }

        _listener.Stop();
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _listener.Stop();
        return Task.CompletedTask;
    }
}

出于示例目的,这里的 web server 做了简化始终返回一个 Hello World,并且端口号写死了 5100,感兴趣的小伙伴可以结合之前我们实现的 Minimal。


 AspNetCore 的逻辑实现 web server 和 http request 的处理

有了 WebServer 我们在添加一个 BackgroundService 来启动我们的 WebServer,实现如下:
file sealed class WebServerHostedService : BackgroundService
{
    private readonly IWebServer _server;

    public WebServerHostedService(IWebServer server)
    {
        _server = server;
    }
    
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        await _server.StopAsync(cancellationToken);
        await base.StopAsync(cancellationToken);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _server.StartAsync(stoppingToken);
    }
}
最后我们再和我们的 host 组合起来:
var builder = AppHost.CreateBuilder();
builder.Configuration.AddJsonFile("appsettings.json");
builder.Logging.AddRelaxedJsonConsole(options =>
{
    options.TimestampFormat = "yyyy-MM-dd HH:mm:ss";
});
builder.Services.AddSingleton<IWebServer, HttpListenerWebServer>();
builder.AddHostedService<WebServerHostedService>();
var cts = new CancellationTokenSource(10_000);
var app = builder.Build();
await app.RunAsync(cts.Token);
接着跑起来我们的 server,访问一下试一下:


完整代码
示例看完了,我们来看看代码实现吧。
用来构建 AppHost 的 AppHostBuilder
public interface IAppHostBuilder
{
    /// <summary>
    /// Gets the set of key/value configuration properties.
    /// </summary>
    ConfigurationManager Configuration { get; }

    /// <summary>
    /// Gets a collection of logging providers for the application to compose. This is useful for adding new logging providers.
    /// </summary>
    ILoggingBuilder Logging { get; }

    /// <summary>
    /// Gets a collection of services for the application to compose. This is useful for adding user provided or framework provided services.
    /// </summary>
    IServiceCollection Services { get; }
}

public sealed class AppHostBuilder : IAppHostBuilder
{
    private bool _hostBuilt;
    private readonly ServiceCollection _serviceCollection;
    internal AppHostBuilder(AppHostBuilderSettings? settings)
    {
        settings ??= new();
        _serviceCollection = new ServiceCollection();
        Configuration = settings.Configuration ?? new ConfigurationManager();

        Logging = new LoggingBuilder(Services);

        _serviceCollection.AddSingleton<IConfiguration>(Configuration);
        _serviceCollection.AddLogging();
    }

    public ConfigurationManager Configuration { get; }
    public ILoggingBuilder Logging { get; }
    public IServiceCollection Services => _serviceCollection;

    public AppHost Build()
    {
        if (_hostBuilt)
        {
            throw new InvalidOperationException("The AppHost had been created");
        }
        _hostBuilt = true;
#if NET7_0_OR_GREATER
        _serviceCollection.MakeReadOnly();
#endif
        var services = Services.BuildServiceProvider();
        return new AppHost(services, Configuration);
    }

    private sealed class LoggingBuilder : ILoggingBuilder
    {
        public LoggingBuilder(IServiceCollection services)
        {
            Services = services;
        }
        public IServiceCollection Services { get; }
    }
}
AppHost 的实现代码较多,仅贴出一部分代码,具体代码可以参考 Github。
public interface IAppHost
{
    IConfiguration Configuration { get; }
    IServiceProvider Services { get; }
    Task RunAsync(CancellationToken cancellationToken = default);
}

public sealed class AppHost : IAppHost
{
    private const string
        AppHostStartingMessage = "AppHost starting",
        AppHostStartedMessage = "AppHost started. Press Ctrl+C to shut down",
        AppHostStoppingMessage = "AppHost stopping",
        AppHostStoppedMessage = "AppHost stopped"
        ;

    private readonly ILogger _logger;
    private readonly AppHostOptions _appHostOptions;

    private readonly IHostedService[] _hostedServices;
    private readonly IHostedLifecycleService[] _hostedLifecycleServices;

    public AppHost(IServiceProvider services, IConfiguration configuration)
    {
        Services = services;
        Configuration = configuration;
        _logger = services.GetRequiredService<ILogger<AppHost>>();
        _appHostOptions = services.GetRequiredService<IOptions<AppHostOptions>>().Value;

        _hostedServices = services.GetServices<IHostedService>().ToArray();
        _hostedLifecycleServices = _hostedServices.Select(x => x as IHostedLifecycleService)
            .WhereNotNull().ToArray();
    }

    public IConfiguration Configuration { get; }
    public ILogger Logger => _logger;
    public IServiceProvider Services { get; }

    public async Task RunAsync(CancellationToken cancellationToken = default)
    {
        Debug.WriteLine(AppHostStartingMessage);
        _logger.LogInformation(AppHostStartingMessage);
        using var hostStopTokenSource = CancellationTokenSource.CreateLinkedTokenSource(InvokeHelper.GetExitToken(), cancellationToken);
#if NET6_0_OR_GREATER
        var waitForStopTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        hostStopTokenSource.Token.Register(() => waitForStopTask.TrySetResult());
#else
        var waitForStopTask = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
        hostStopTokenSource.Token.Register(() => waitForStopTask.TrySetResult(null));
#endif
        var exceptions = new List<Exception>();

        var startTimeoutCts = new CancellationTokenSource(_appHostOptions.StartupTimeout);
        var hostStartCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, InvokeHelper.GetExitToken(), startTimeoutCts.Token);
        var hostStartCancellationToken = hostStartCancellationTokenSource.Token;
        await ForeachService(_hostedLifecycleServices, hostStartCancellationToken, _appHostOptions.ServicesStartConcurrently,
            !_appHostOptions.ServicesStartConcurrently, exceptions, async (service, cancelToken) =>
            {
                await service.StartingAsync(cancelToken);
            }).ConfigureAwait(false);
        LogAndRethrow();
        await ForeachService(_hostedServices, hostStartCancellationToken, _appHostOptions.ServicesStartConcurrently,
                            !_appHostOptions.ServicesStartConcurrently, exceptions, async (service, cancelToken) =>
                            {
                                await service.StartAsync(cancelToken);
                            }).ConfigureAwait(false);
        LogAndRethrow();
        await ForeachService(_hostedLifecycleServices, hostStartCancellationToken, _appHostOptions.ServicesStartConcurrently,
                    !_appHostOptions.ServicesStartConcurrently, exceptions, async (service, cancelToken) =>
                    {
                        await service.StartedAsync(cancelToken);
                    }).ConfigureAwait(false);
        LogAndRethrow();
        startTimeoutCts.Dispose();
        Debug.WriteLine(AppHostStartedMessage);
        _logger.LogInformation(AppHostStartedMessage);

        await waitForStopTask.Task.ConfigureAwait(false);
        Debug.WriteLine(AppHostStoppingMessage);
        _logger.LogInformation(AppHostStoppingMessage);
        // reverse to keep first startup last stop when not in concurrent
        Array.Reverse(_hostedServices);
        Array.Reverse(_hostedLifecycleServices);
        var stopTimeoutCts = new CancellationTokenSource(_appHostOptions.ShutdownTimeout);
        var hostStopCancellationToken = stopTimeoutCts.Token;
        await ForeachService(_hostedLifecycleServices, hostStopCancellationToken, _appHostOptions.ServicesStopConcurrently,
            false, exceptions, async (service, cancelToken) =>
            {
                await service.StoppingAsync(cancelToken);
            }).ConfigureAwait(false);
        await ForeachService(_hostedServices, hostStopCancellationToken, _appHostOptions.ServicesStopConcurrently,
            false, exceptions, async (service, cancelToken) =>
            {
                await service.StopAsync(cancelToken);
            }).ConfigureAwait(false);
        await ForeachService(_hostedLifecycleServices, hostStopCancellationToken, _appHostOptions.ServicesStopConcurrently,
            false, exceptions, async (service, cancelToken) =>
            {
                await service.StoppedAsync(cancelToken);
            }).ConfigureAwait(false);

        Debug.WriteLine(AppHostStoppedMessage);
        _logger.LogInformation(AppHostStoppedMessage);
    }

    public static AppHostBuilder CreateBuilder(AppHostBuilderSettings? settings = null)
    {
        return new AppHostBuilder(settings);
    }
}
为了方便后台任务,重新定义了 IHostedService/BackgroundService 实现基本和 .NET 框架里的一致,为了避免扩展方法冲突,这里的扩展方法是直接基于 IAppHostBuilder 而非 IServiceCollection
public static IAppHostBuilder AddHostedService<TService>(this IAppHostBuilder appHostBuilder)
        where TService : class, IHostedService
{
    Guard.NotNull(appHostBuilder);
    appHostBuilder.Services.TryAddEnumerable(
        ServiceDescriptor.Describe(typeof(IHostedService), typeof(TService), ServiceLifetime.Singleton)
        );
    return appHostBuilder;
}
总结
总体来说使用起来和 .NET 6 Minimal API 的使用风格是一样的,借助了 .NET 6 的 ConfigurationManager 来处理配置。做了一些简化,去掉了 lifetime,IHost 直接使用了一个 RunAsync 方法,没有 StartAsync/StopAsync 。把 .NET 8 里的一些新特性比如并行启动停止,startup timeout 以及 IHostedLifecycleService 也引入了进来,支持了 .NET Standard 2.0。

参考
https://github.com/WeihanLi/WeihanLi.Common/blob/dev/samples/DotNetCoreSample/AppHostTest.cs
https://github.com/WeihanLi/WeihanLi.Common/tree/dev/src/WeihanLi.Common/Helpers/Hosting
用户评论