美文网首页
添加一个 HttpServer 到桌面程序中

添加一个 HttpServer 到桌面程序中

作者: gruan | 来源:发表于2023-09-29 18:01 被阅读0次

之前在一个 .NET3.5 的桌面程序中,内嵌的 HttpServer 是通过 HttpListener 来实现的, 但是这个东西对 Https 证书 支持的不是很好,找了很多文档,都说是把证书导入到计算机。但是作为一个要分发的应用程序,不可能这样做。为此, 我还用 学了几天的 GO 肝了一个升级版,但毕竟GO不是咱的专长。

在 .NET8 中,ASP.NET Core 提供了一个 SlimBuilder, 这个东西支持 AOT。但是要把这个 SlimBuilder 嵌入到桌面程序中, 还是有个波折的。

WebApplication.CreateSlimBuilder()

RequestDelegateGenerator

这个东西要运行在 AOT 下面, 需要借助 RequestDelegateGenerator, 没错, 又是一个 IIncrementalGenerator, 你可以在新建的 .NET8 ASP.NETCore 项目的分析器里找到它。
但是这个东西只支持 Web项目, 而且当前没有 NUGET 包。

<Project Sdk="Microsoft.NET.Sdk.Web">

要在非Web 项目里使用 WebApplicaton 需要引用框架,但是, 这个 SourceGenerator 会被自动屏蔽掉。

    <ItemGroup>
        <!--WebApplication.CreateSlimBuilder 是框架的功能, 没有 NUGET包, 只能引用框架-->
        <FrameworkReference Include="Microsoft.AspNetCore.App" />
    </ItemGroup>

为了能使用上这个 SourceGenerator 只能祭上反编译工具, 导出代码,在放到自己的项目中去。

<ProjectReference Include="..\Microsoft.AspNetCore.Http.RequestDelegateGenerator\RequestDelegateGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

注意, 分析器项目必须指定:
OutputItemType="Analyzer" ReferenceOutputAssembly="false"

ConfigureHttpJsonOptions

即使在这个 HttpServer 中, 输出的都是文本, 也需要加上Json 的 SerializeOption, 不然AOT报错。

var builder = WebApplication.CreateSlimBuilder();

builder.Services.ConfigureHttpJsonOptions(options =>
{
    //特意加的, 不然 AOT 报错
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
。。。
});
。。。
internal record Todo(int Id, string? Title, DateOnly? DueBy = null, bool IsComplete = false);

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

不必在意这个 Todo ,它只是为了调用 System.Text.Json 的 SourceGenerator.

日志

由于这个 WebApplicaton 是嵌入到桌面程序中的, 所以不能直观的看到它抛出的异常, 不方便定位问题, 所以需要一个可以输出到界面上的 LogProvider.
注意:Log4Net 目前不支持 AOT。

    internal sealed class ObservableCollectionLoggerProvider : ILoggerProvider
    {

        private readonly ObservableCollection<string> collection;

        public ObservableCollectionLoggerProvider(ObservableCollection<string> collection)
        {
            this.collection = collection;
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new ObservableCollectionLogger(this.collection, categoryName);
        }


        private class ObservableCollectionLogger : ILogger
        {
            private readonly ObservableCollection<string> collection;

            private readonly string categoryName;

            public ObservableCollectionLogger(ObservableCollection<string> collection, string categoryName)
            {
                this.collection = collection;
                this.categoryName = categoryName;
            }

            public IDisposable? BeginScope<TState>(TState state) where TState : notnull
            {
                return new NoopDisposable();
            }

            public bool IsEnabled(LogLevel logLevel)
            {
                return true;
            }

            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
            {
                string message = "";
                if (formatter != null)
                {
                    message += formatter(state, exception);
                    //formatter 没有打印 exception 的具体信息
                    //目前不了解如何自定义 formatter, 用如下简单方法处理
                    if (exception != null)
                    {
                        message = $"{message}\r\n{exception.Message}\r\n{exception.StackTrace}";
                    }
                }

                this.collection.Add($"{DateTime.Now:HH:mm:ss}\t{logLevel}\t{this.categoryName}\r\n{message}\r\n");
            }
        }

        private sealed class NoopDisposable : IDisposable
        {
            public void Dispose()
            {
            }
        }

        #region dispose
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~ObservableCollectionLoggerProvider()
        {
            this.Dispose(false);
        }


        private bool isDisposed = false;
        private void Dispose(bool flag)
        {
            if (!isDisposed)
            {
                if (flag)
                {
                }
                isDisposed = true;
            }
        }
        #endregion        
    }

这里使用了 ObservableCollection<T> ,用它可以监控日志变化,实时的更新到界面上去。

完整示例

    [Regist<ISlimHttpServer>(RegistMode.Singleton)]
    public class SlimHttpServer : ISlimHttpServer, IDisposable
    {
        private readonly ILogger<SlimHttpServer> logger;
        private readonly IEnumerable<ISilmHttpServerModule> modules;
        private CancellationTokenSource? cts;
        private readonly ObservableCollectionLoggerProvider logProvider;
        public ObservableCollection<string> Logs { get; } = new();

        public bool Started { get; private set; }

        public SlimHttpServer(ILogger<SlimHttpServer> logger, IEnumerable<ISilmHttpServerModule> modules)
        {
            this.logger = logger;
            this.modules = modules;

            //文本框显示日志
            this.logProvider = new ObservableCollectionLoggerProvider(this.Logs);
        }

        public bool Start()
        {
            lock (this)
            {
                if (Started)
                    return true;

                this.cts?.Dispose();
                this.cts = new CancellationTokenSource();
                try
                {
                    Task.Factory.StartNew(async () =>
                    {
                        var builder = WebApplication.CreateSlimBuilder();

                        builder.Services.ConfigureHttpJsonOptions(options =>
                        {
                            //特意加的, 不然 AOT 报错
                            options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);

                            foreach (var m in this.modules)
                            {
                                if (m is ISilmHttpServerModule.IJson json)
                                    options.SerializerOptions.TypeInfoResolverChain.Add(json.JsonTypeInfoResolver);
                            }
                        });

                        //文件日志
                        builder.Logging.AddSimpleFile();
                        
                        builder.Logging.AddProvider(this.logProvider);

                        var certificate = new X509Certificate2(Resources.localhost, "CNBOOKING");

                        builder.WebHost.UseKestrel(o =>
                        {
                            o.ListenLocalhost(9998, oo => oo.UseHttps(certificate));
                            o.ListenLocalhost(9999);
                        });

                        //开启CORS
                        builder.Services.AddCors(c => c.AddDefaultPolicy(cp =>
                            cp.AllowAnyOrigin()
                            .AllowAnyHeader()
                            .AllowAnyMethod()
                        ));

                        var app = builder.Build();

                        app.UseCors();

                        foreach (var m in modules)
                        {
                            var methods = new List<string>() { m.HttpMethod.Method };
                            if (m is ISilmHttpServerModule.IContent content)
                            {
                                app.MapMethods(m.Pattern, methods, async () =>
                                {
                                    var rst = await content.Delegate();
                                    return Results.Content(rst, content.ContentType, content.Encoding);
                                });
                            }
                            else if (m is ISilmHttpServerModule.IHttpContext http)
                            {
                                app.MapMethods(m.Pattern, methods, (h) => http.RequestDelegate.Invoke(h));
                            }
                            else if (m is ISilmHttpServerModule.IJson json)
                            {
                                app.MapMethods(m.Pattern, methods, () => json.Delegate.DynamicInvoke());
                            }
                        }

                        cts.Token.Register(async () =>
                        {
                            certificate?.Dispose();
                            await app.DisposeAsync();
                        });

                        await app.RunAsync();

                    }, this.cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);

                    this.Started = true;
                    return true;
                }
                catch (Exception ex)
                {
                    this.Started = false;
                    this.logger.LogError(ex, ex.Message);
                    return false;
                }
            }
        }

        public bool Stop()
        {
            this.cts?.Cancel();
            this.Started = false;
            return true;
        }

        #region dispose
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~SlimHttpServer()
        {
            this.Dispose(false);
        }

        private bool isDisposed = false;
        private void Dispose(bool flag)
        {
            if (!isDisposed)
            {
                if (flag)
                {
                    this.cts?.Dispose();
                    this.logProvider.Dispose();
                }
                isDisposed = true;
            }
        }
        #endregion
    }

接口定义:

    public interface ISilmHttpServerModule
    {
        string Pattern { get; }
        HttpMethod HttpMethod { get; }

        public interface IJson : ISilmHttpServerModule
        {
            IJsonTypeInfoResolver JsonTypeInfoResolver { get; }
            Delegate Delegate { get; }
        }

        public interface IContent : ISilmHttpServerModule
        {
            string? ContentType { get; }
            Encoding? Encoding { get; }
            Func<Task<string>> Delegate { get; }
        }

        public interface IHttpContext : ISilmHttpServerModule
        {
            Func<HttpContext, Task> RequestDelegate { get; }
        }
    }

Json Module 示例

   [Regist<ISilmHttpServerModule>(RegistMode.Singleton)]
   public class Json : ISilmHttpServerModule.IJson
   {
       public IJsonTypeInfoResolver JsonTypeInfoResolver => AppJsonSerializerContext.Default;
       public Delegate Delegate => () => new List<Item>() {
           new Item() { ID = 1, Name = "Xling"}
       };
       public string Pattern => "/test/3";
       public HttpMethod HttpMethod => HttpMethod.Get;
   }


   public class Item
   {
       public int ID { get; set; }
       public string? Name { get; set; }
   }

   [JsonSerializable(typeof(IEnumerable<Item>))]
   internal partial class AppJsonSerializerContext : JsonSerializerContext
   {
   }

HttpCotnext Module 示例

    [Regist<ISilmHttpServerModule>(RegistMode.Singleton)]
    public class WithHttpContext : ISilmHttpServerModule.IHttpContext
    {
        public string Pattern => "/test/2";
        public HttpMethod HttpMethod => HttpMethod.Get;
        public Func<HttpContext, Task> RequestDelegate => async (h) => await h.Response.WriteAsync(DateTime.Now.ToString());
    }
~~~
a

相关文章

  • 写一个简单的WEB框架

    前面都是从httpserver中添加功能,下面希望把添加web功能写成框架的形式,如果进行开发,只需要响应的功能模...

  • NodeMCU作为网页服务器

    借鉴 这篇文章 做了一个简单的服务程序。 程序总共5个文件:httpServer.lua,index.html,i...

  • 微信小程序中跳转其他微信小程序

    首先拿到需要跳转的小程序的appId, 方法如下 打开微信电脑版 选择需要跳转的小程序,右键添加到桌面 右键桌面图...

  • 学习内容汇总

    桌面 Linux下,为应用程序添加桌面图标(ubuntu18.4) Markdown 语法手册 (完整整理版)ma...

  • MAC-小技巧

    1:如何复制桌面1的文件到桌面2.桌面的文件都是一样,不过四指推上,选择某一个程序到某一个桌面是可以的。 2: F...

  • Android开发刷新问题!求高手解答!

    我在软件里边添加一个web页面,不过当我离开这个程序(不是退出)是去别的程序,或者回到桌面,再重新进去这个程序的时...

  • 2017-06-28

    这个厉害了。可以直接添加写文章到桌面。

  • 第十五章——相机【译】

    在本章中,您将要添加照片到 Homepwner 应用程序。 您将呈现一个 UIImagePickerControl...

  • 初识小程序

    关注了挺久的小程序,今天终于如期而至。IOS需要更新到最新版本才可体验。安卓系统可支持添加小程序到桌面。这里附上小...

  • 新人Kali之旅005 - 添加桌面快捷方式

    Windows系统中,添加桌面快捷方式非常方便,右键添加即可。而在Kali和Ubuntu这些Linux系统中,添加...

网友评论

      本文标题:添加一个 HttpServer 到桌面程序中

      本文链接:https://www.haomeiwen.com/subject/rdbrbdtx.html